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",
"description": "Check Windows Event Viewer for errors.",
"commands": ["Get-EventLog -LogName Application -Newest 20"],
"children": [
{
"id": "svc-logs-resolved",
"type": "solution",
"title": "Issue Found in Logs",
"description": "Error identified and resolved.",
"resolution_steps": ["Fix the error", "Restart service"],
}
],
"next_node_id": "svc-logs-resolved",
},
{
"id": "svc-logs-resolved",
"type": "solution",
"title": "Issue Found in Logs",
"description": "Error identified and resolved.",
"resolution_steps": ["Fix the error", "Restart service"],
},
{
"id": "svc-restart",
@@ -52,15 +51,14 @@ BRANCH_DETAIL_JSON = json.dumps({
"title": "Restart Service",
"description": "Attempt to restart the service.",
"commands": ["Restart-Service -Name 'TestService'"],
"children": [
{
"id": "svc-restart-ok",
"type": "solution",
"title": "Service Restored",
"description": "Service is running after restart.",
"resolution_steps": ["Verify connectivity", "Document in ticket"],
}
],
"next_node_id": "svc-restart-ok",
},
{
"id": "svc-restart-ok",
"type": "solution",
"title": "Service Restored",
"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():
"""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 {
"id": "root",
"type": "decision",
@@ -44,14 +48,13 @@ def _make_valid_tree():
"title": "Restart the Service",
"description": "Restart the service and verify.",
"commands": ["Restart-Service -Name 'TestService'"],
"children": [
{
"id": "service-resolved",
"type": "solution",
"title": "Service Restored",
"description": "Service is running after restart.",
},
],
"next_node_id": "service-resolved",
},
{
"id": "service-resolved",
"type": "solution",
"title": "Service Restored",
"description": "Service is running after restart.",
},
],
}
@@ -107,6 +110,12 @@ class TestNodeValidation:
errors = validate_generated_tree(tree)
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:
def test_option_references_nonexistent_child(self):
@@ -156,18 +165,18 @@ class TestGlobalChecks:
{"id": "o1", "label": "A", "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 one more to get to 1
tree["children"][1]["children"] = []
# Remove the solution that restart-service points to
tree["children"].pop(2) # remove service-resolved
errors = validate_generated_tree(tree)
assert any("solution" in e.lower() for e in errors)
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()
# Remove restart-service's children — becomes dead end
tree["children"][1]["children"] = []
# Remove children from check-logs decision node — becomes dead end
tree["children"][0]["children"] = []
errors = validate_generated_tree(tree)
assert any("dead end" in e for e in errors)

View File

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