feat(l1): ai_tree_builder — constrained node generation, validation, normalize
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
58
backend/tests/test_ai_tree_builder.py
Normal file
58
backend/tests/test_ai_tree_builder.py
Normal file
@@ -0,0 +1,58 @@
|
||||
import pytest
|
||||
from app.services import ai_tree_builder as atb
|
||||
|
||||
|
||||
def test_validate_node_rejects_hard_floor_text():
|
||||
node = {"node_type": "instruction", "id": "n1", "text": "Open regedit and change the key", "next": "generate"}
|
||||
with pytest.raises(atb.UnsafeNodeError):
|
||||
atb.validate_node(node)
|
||||
|
||||
|
||||
def test_validate_node_accepts_safe_instruction():
|
||||
node = {"node_type": "instruction", "id": "n1", "text": "Restart the printer.", "next": "generate"}
|
||||
assert atb.validate_node(node)["node_type"] == "instruction"
|
||||
|
||||
|
||||
def test_depth_cap_forces_escalate():
|
||||
walked = [{"node_type": "question", "id": f"n{i}", "text": "?", "answer": "no"} for i in range(atb.MAX_DEPTH)]
|
||||
node = atb.escalate_if_depth_exceeded(walked)
|
||||
assert node is not None and node["node_type"] == "escalate"
|
||||
|
||||
|
||||
def test_normalize_walked_path_builds_valid_tree():
|
||||
walked = [
|
||||
{"node_type": "question", "id": "n1", "text": "Powered on?", "answer": "no"},
|
||||
{"node_type": "instruction", "id": "n2", "text": "Power it on.", "answer": "ack"},
|
||||
{"node_type": "resolved", "id": "n3", "text": "Fixed."},
|
||||
]
|
||||
tree = atb.normalize_walked_path(walked)
|
||||
assert isinstance(tree, dict) and tree.get("id") == "n1"
|
||||
# untraversed 'yes' branch of n1 became a needs_review stub
|
||||
assert any(n["node_type"] == "needs_review" for n in tree["nodes"].values())
|
||||
|
||||
|
||||
def test_normalize_walk_ending_on_question_has_no_none_branches():
|
||||
walked = [
|
||||
{"node_type": "question", "id": "n1", "text": "Powered on?", "answer": "no"},
|
||||
]
|
||||
tree = atb.normalize_walked_path(walked)
|
||||
n1 = tree["nodes"]["n1"]
|
||||
assert n1["yes_next"] is not None and n1["no_next"] is not None
|
||||
# both branches must reference real nodes present in the tree
|
||||
assert n1["yes_next"] in tree["nodes"] and n1["no_next"] in tree["nodes"]
|
||||
|
||||
|
||||
def test_normalize_preserves_escalate_reason_category():
|
||||
walked = [
|
||||
{"node_type": "question", "id": "n1", "text": "On?", "answer": "no"},
|
||||
{"node_type": "escalate", "id": "n2", "text": "Beyond L1.",
|
||||
"reason_category": "exhausted_safe_steps"},
|
||||
]
|
||||
tree = atb.normalize_walked_path(walked)
|
||||
assert tree["nodes"]["n2"]["reason_category"] == "exhausted_safe_steps"
|
||||
|
||||
|
||||
def test_normalize_empty_walk_returns_needs_review_root():
|
||||
tree = atb.normalize_walked_path([])
|
||||
assert tree["id"] in tree["nodes"]
|
||||
assert tree["nodes"][tree["id"]]["node_type"] == "needs_review"
|
||||
Reference in New Issue
Block a user