fix: update tests to match action node schema (next_node_id, not children)

- Update _make_valid_tree() in test_ai_tree_validator to use next_node_id
  on action nodes (solution is a sibling, not a child)
- Fix test_dead_end_action_node → test_dead_end_decision_node (action nodes
  don't have child-based dead ends; dead ends are decision nodes with no children)
- Add test_action_missing_next_node_id for the new validation rule
- Update BRANCH_DETAIL_JSON in test_ai_endpoints to use next_node_id pattern
- Update test_draft_trees.py to use "title" field for action/solution nodes
  (tree_validation.py was updated this branch to require "title" not "action"/"solution")

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chihlasm
2026-02-22 23:18:10 -05:00
parent 339486f555
commit 5acf94b6c2
3 changed files with 59 additions and 52 deletions

View File

@@ -36,15 +36,14 @@ BRANCH_DETAIL_JSON = json.dumps({
"title": "Check Event Logs", "title": "Check Event Logs",
"description": "Check Windows Event Viewer for errors.", "description": "Check Windows Event Viewer for errors.",
"commands": ["Get-EventLog -LogName Application -Newest 20"], "commands": ["Get-EventLog -LogName Application -Newest 20"],
"children": [ "next_node_id": "svc-logs-resolved",
{ },
"id": "svc-logs-resolved", {
"type": "solution", "id": "svc-logs-resolved",
"title": "Issue Found in Logs", "type": "solution",
"description": "Error identified and resolved.", "title": "Issue Found in Logs",
"resolution_steps": ["Fix the error", "Restart service"], "description": "Error identified and resolved.",
} "resolution_steps": ["Fix the error", "Restart service"],
],
}, },
{ {
"id": "svc-restart", "id": "svc-restart",
@@ -52,15 +51,14 @@ BRANCH_DETAIL_JSON = json.dumps({
"title": "Restart Service", "title": "Restart Service",
"description": "Attempt to restart the service.", "description": "Attempt to restart the service.",
"commands": ["Restart-Service -Name 'TestService'"], "commands": ["Restart-Service -Name 'TestService'"],
"children": [ "next_node_id": "svc-restart-ok",
{ },
"id": "svc-restart-ok", {
"type": "solution", "id": "svc-restart-ok",
"title": "Service Restored", "type": "solution",
"description": "Service is running after restart.", "title": "Service Restored",
"resolution_steps": ["Verify connectivity", "Document in ticket"], "description": "Service is running after restart.",
} "resolution_steps": ["Verify connectivity", "Document in ticket"],
],
}, },
], ],
}) })

View File

@@ -5,7 +5,11 @@ from app.core.ai_tree_validator import validate_generated_tree, count_tree_stats
def _make_valid_tree(): def _make_valid_tree():
"""Helper: minimal valid tree for testing.""" """Helper: minimal valid tree for testing.
Action nodes use next_node_id to point to a sibling (not children).
The solution following an action is a sibling under the parent decision.
"""
return { return {
"id": "root", "id": "root",
"type": "decision", "type": "decision",
@@ -44,14 +48,13 @@ def _make_valid_tree():
"title": "Restart the Service", "title": "Restart the Service",
"description": "Restart the service and verify.", "description": "Restart the service and verify.",
"commands": ["Restart-Service -Name 'TestService'"], "commands": ["Restart-Service -Name 'TestService'"],
"children": [ "next_node_id": "service-resolved",
{ },
"id": "service-resolved", {
"type": "solution", "id": "service-resolved",
"title": "Service Restored", "type": "solution",
"description": "Service is running after restart.", "title": "Service Restored",
}, "description": "Service is running after restart.",
],
}, },
], ],
} }
@@ -107,6 +110,12 @@ class TestNodeValidation:
errors = validate_generated_tree(tree) errors = validate_generated_tree(tree)
assert any("at least 2 options" in e for e in errors) assert any("at least 2 options" in e for e in errors)
def test_action_missing_next_node_id(self):
tree = _make_valid_tree()
del tree["children"][1]["next_node_id"]
errors = validate_generated_tree(tree)
assert any("missing 'next_node_id'" in e for e in errors)
class TestReferenceIntegrity: class TestReferenceIntegrity:
def test_option_references_nonexistent_child(self): def test_option_references_nonexistent_child(self):
@@ -156,18 +165,18 @@ class TestGlobalChecks:
{"id": "o1", "label": "A", "next_node_id": "only-solution"}, {"id": "o1", "label": "A", "next_node_id": "only-solution"},
{"id": "o2", "label": "B", "next_node_id": "only-solution"}, {"id": "o2", "label": "B", "next_node_id": "only-solution"},
] ]
# Now restart-service branch has 1 solution, check-logs has 1 = total 2 # Remove the solution that restart-service points to
# Remove one more to get to 1 tree["children"].pop(2) # remove service-resolved
tree["children"][1]["children"] = []
errors = validate_generated_tree(tree) errors = validate_generated_tree(tree)
assert any("solution" in e.lower() for e in errors) assert any("solution" in e.lower() for e in errors)
class TestDeadEndDetection: class TestDeadEndDetection:
def test_dead_end_action_node(self): def test_dead_end_decision_node(self):
"""A decision node with no children is a dead end."""
tree = _make_valid_tree() tree = _make_valid_tree()
# Remove restart-service's children — becomes dead end # Remove children from check-logs decision node — becomes dead end
tree["children"][1]["children"] = [] tree["children"][0]["children"] = []
errors = validate_generated_tree(tree) errors = validate_generated_tree(tree)
assert any("dead end" in e for e in errors) assert any("dead end" in e for e in errors)

View File

@@ -20,13 +20,13 @@ class TestTreeValidation:
{ {
"id": "yes", "id": "yes",
"type": "solution", "type": "solution",
"solution": "Server is healthy", "title": "Server is healthy",
"children": [] "children": []
}, },
{ {
"id": "no", "id": "no",
"type": "action", "type": "action",
"action": "Restart the server", "title": "Restart the server",
"children": [] "children": []
} }
] ]
@@ -70,15 +70,15 @@ class TestTreeValidation:
"type": "decision", "type": "decision",
"question": "Test?", "question": "Test?",
"children": [ "children": [
{"id": "child1", "type": "solution", "solution": "Fix"} {"id": "child1", "type": "solution", "title": "Fix"}
] ]
} }
is_valid, errors = validate_tree_structure(tree_structure) is_valid, errors = validate_tree_structure(tree_structure)
assert not is_valid assert not is_valid
assert any("at least 2" in error["message"] for error in errors) assert any("at least 2" in error["message"] for error in errors)
def test_action_node_missing_action(self): def test_action_node_missing_title(self):
"""Test validation when action node has no action.""" """Test validation when action node has no title."""
tree_structure = { tree_structure = {
"id": "root", "id": "root",
"type": "action", "type": "action",
@@ -86,10 +86,10 @@ class TestTreeValidation:
} }
is_valid, errors = validate_tree_structure(tree_structure) is_valid, errors = validate_tree_structure(tree_structure)
assert not is_valid assert not is_valid
assert any("action" in error["field"] for error in errors) assert any("title" in error["field"] for error in errors)
def test_solution_node_missing_solution(self): def test_solution_node_missing_title(self):
"""Test validation when solution node has no solution.""" """Test validation when solution node has no title."""
tree_structure = { tree_structure = {
"id": "root", "id": "root",
"type": "solution", "type": "solution",
@@ -97,7 +97,7 @@ class TestTreeValidation:
} }
is_valid, errors = validate_tree_structure(tree_structure) is_valid, errors = validate_tree_structure(tree_structure)
assert not is_valid assert not is_valid
assert any("solution" in error["field"] for error in errors) assert any("title" in error["field"] for error in errors)
def test_unknown_node_type(self): def test_unknown_node_type(self):
"""Test validation with unknown node type.""" """Test validation with unknown node type."""
@@ -112,14 +112,14 @@ class TestTreeValidation:
def test_can_publish_with_empty_name(self): def test_can_publish_with_empty_name(self):
"""Test can_publish with empty name.""" """Test can_publish with empty name."""
tree_structure = {"id": "root", "type": "solution", "solution": "Fix"} tree_structure = {"id": "root", "type": "solution", "title": "Fix"}
can_publish, errors = can_publish_tree(tree_structure, "", None) can_publish, errors = can_publish_tree(tree_structure, "", None)
assert not can_publish assert not can_publish
assert any("name" in error["field"] for error in errors) assert any("name" in error["field"] for error in errors)
def test_can_publish_valid_tree(self): def test_can_publish_valid_tree(self):
"""Test can_publish with valid tree and name.""" """Test can_publish with valid tree and name."""
tree_structure = {"id": "root", "type": "solution", "solution": "Fix"} tree_structure = {"id": "root", "type": "solution", "title": "Fix"}
can_publish, errors = can_publish_tree(tree_structure, "Valid Tree", "Description") can_publish, errors = can_publish_tree(tree_structure, "Valid Tree", "Description")
assert can_publish assert can_publish
assert len(errors) == 0 assert len(errors) == 0
@@ -157,8 +157,8 @@ class TestDraftTreesAPI:
"type": "decision", "type": "decision",
"question": "Is it working?", "question": "Is it working?",
"children": [ "children": [
{"id": "yes", "type": "solution", "solution": "Great!"}, {"id": "yes", "type": "solution", "title": "Great!"},
{"id": "no", "type": "action", "action": "Fix it"} {"id": "no", "type": "action", "title": "Fix it"}
] ]
}, },
"status": "published" "status": "published"
@@ -193,8 +193,8 @@ class TestDraftTreesAPI:
name="Draft to Published", name="Draft to Published",
description="Test tree", description="Test tree",
tree_structure={"id": "root", "type": "decision", "question": "Test?", "children": [ tree_structure={"id": "root", "type": "decision", "question": "Test?", "children": [
{"id": "yes", "type": "solution", "solution": "Yes"}, {"id": "yes", "type": "solution", "title": "Yes"},
{"id": "no", "type": "solution", "solution": "No"} {"id": "no", "type": "solution", "title": "No"}
]}, ]},
author_id=UUID(test_user["user_data"]["id"]), author_id=UUID(test_user["user_data"]["id"]),
account_id=UUID(test_user["user_data"]["account_id"]), account_id=UUID(test_user["user_data"]["account_id"]),
@@ -252,8 +252,8 @@ class TestDraftTreesAPI:
"type": "decision", "type": "decision",
"question": "Is it working?", "question": "Is it working?",
"children": [ "children": [
{"id": "yes", "type": "solution", "solution": "Great!"}, {"id": "yes", "type": "solution", "title": "Great!"},
{"id": "no", "type": "action", "action": "Fix it"} {"id": "no", "type": "action", "title": "Fix it"}
] ]
}, },
author_id=UUID(test_user["user_data"]["id"]), author_id=UUID(test_user["user_data"]["id"]),
@@ -315,7 +315,7 @@ class TestDraftTreesAPI:
tree = Tree( tree = Tree(
name="Test Tree", name="Test Tree",
description="Test", description="Test",
tree_structure={"id": "root", "type": "solution", "solution": "Fix"}, tree_structure={"id": "root", "type": "solution", "title": "Fix"},
author_id=UUID(test_user["user_data"]["id"]), author_id=UUID(test_user["user_data"]["id"]),
account_id=UUID(test_user["user_data"]["account_id"]), account_id=UUID(test_user["user_data"]["account_id"]),
status='published' status='published'
@@ -337,7 +337,7 @@ class TestDraftTreesAPI:
tree = Tree( tree = Tree(
name="Legacy Tree", name="Legacy Tree",
description="Created before status field", description="Created before status field",
tree_structure={"id": "root", "type": "solution", "solution": "Fix"}, tree_structure={"id": "root", "type": "solution", "title": "Fix"},
author_id=None, author_id=None,
account_id=None account_id=None
) )