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:
@@ -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"],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user