Files
resolutionflow/backend/tests/test_variable_service.py
chihlasm eac6e184ec feat: add dual-mode tree editor with Code Mode, variables, and markdown sync
Implements the full dual-mode tree editor (Plan Phases 1-5):

Backend:
- JSONB↔Markdown bidirectional serializer/parser with mistune
- Markdown validator with line/column error reporting
- 3 API endpoints: export-markdown, import-markdown, validate-markdown
- Variable extraction/resolution service ([USER_INPUT], [VAR], [SAVE_AS])
- Session variables JSONB column (migration 028)
- 39 tree markdown tests + variable service tests (403 total passing)

Frontend:
- Monaco-based Code Mode with custom Monarch tokenizer and dark theme
- Autocomplete for @node_id refs, type values, variable names
- Debounced validation (800ms) with inline Monaco error markers
- Syntax help panel (absolute overlay, toggleable)
- Starter template for new trees with valid cross-references
- Bidirectional metadata sync (name/description/category/tags frontmatter)
- Synchronous tree→markdown serializer (fixes async race condition)
- Pre-save validation blocks save on broken refs or missing tree name
- Mode-aware undo/redo: Monaco native in Code Mode, throttled zundo in Flow Mode
- Variable prompt modal and frontend resolver for session navigation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 09:45:26 -05:00

120 lines
4.1 KiB
Python

"""Tests for variable extraction and resolution service."""
import pytest
from app.services.variable_service import extract_variables, resolve_variables
TREE_WITH_VARIABLES = {
"id": "root",
"type": "decision",
"question": "What is the hostname of [USER_INPUT:hostname]?",
"help_text": "Enter the server hostname",
"options": [
{"id": "opt1", "label": "Check [VAR:hostname]", "next_node_id": "action1"},
],
"children": [
{
"id": "action1",
"type": "action",
"title": "Ping [VAR:hostname]",
"description": "Run diagnostics on [VAR:hostname].\n\n[SAVE_AS:test_results]",
"commands": ["ping [VAR:hostname]"],
"expected_outcome": "Replies from [VAR:hostname]",
"next_node_id": "solution1",
"children": [
{
"id": "solution1",
"type": "solution",
"title": "Resolved",
"description": "Issue on [VAR:hostname] resolved.",
"resolution_steps": [
"Document results for [VAR:hostname]",
"Save as [SAVE_AS:final_result]"
],
"solution": "Resolved",
}
],
}
],
}
TREE_WITHOUT_VARIABLES = {
"id": "root",
"type": "solution",
"title": "Simple Fix",
"description": "Just restart it.",
"solution": "Restart",
}
class TestExtractVariables:
def test_extracts_user_input(self):
variables = extract_variables(TREE_WITH_VARIABLES)
user_inputs = [v for v in variables if v.kind == "user_input"]
assert len(user_inputs) == 1
assert user_inputs[0].name == "hostname"
assert user_inputs[0].node_id == "root"
def test_extracts_var_references(self):
variables = extract_variables(TREE_WITH_VARIABLES)
refs = [v for v in variables if v.kind == "reference"]
assert len(refs) >= 4 # hostname used in multiple places
def test_extracts_save_as(self):
variables = extract_variables(TREE_WITH_VARIABLES)
saves = [v for v in variables if v.kind == "save_as"]
assert len(saves) == 2
names = {v.name for v in saves}
assert "test_results" in names
assert "final_result" in names
def test_no_variables(self):
variables = extract_variables(TREE_WITHOUT_VARIABLES)
assert len(variables) == 0
def test_extracts_from_commands(self):
variables = extract_variables(TREE_WITH_VARIABLES)
cmd_vars = [v for v in variables if v.node_id == "action1" and v.kind == "reference"]
# hostname in title, description, commands, expected_outcome
assert len(cmd_vars) >= 3
class TestResolveVariables:
def test_resolves_var_reference(self):
text = "Ping [VAR:hostname] now"
result = resolve_variables(text, {"hostname": "server01"})
assert result == "Ping server01 now"
def test_resolves_user_input(self):
text = "Server: [USER_INPUT:hostname]"
result = resolve_variables(text, {"hostname": "server01"})
assert result == "Server: server01"
def test_removes_save_as(self):
text = "Done [SAVE_AS:result] here"
result = resolve_variables(text, {})
assert result == "Done here"
def test_unresolved_var_preserved(self):
text = "Server: [VAR:unknown_var]"
result = resolve_variables(text, {})
assert result == "Server: [VAR:unknown_var]"
def test_multiple_variables(self):
text = "[VAR:hostname] ([VAR:ip]) - [USER_INPUT:ticket]"
result = resolve_variables(text, {
"hostname": "server01",
"ip": "10.0.0.1",
"ticket": "INC001",
})
assert result == "server01 (10.0.0.1) - INC001"
def test_empty_variables_dict(self):
text = "No vars here"
result = resolve_variables(text, {})
assert result == "No vars here"
def test_empty_text(self):
result = resolve_variables("", {"hostname": "server01"})
assert result == ""