- test_tree_validation.py: replace "action"/"solution" content fields with "title" - test_procedural_flows.py: update solution node fixtures to use "title" - test_save_session_as_tree.py: update fixtures and assertions for "title" field - session_to_tree.py: generate "title" instead of "action"/"solution" on converted nodes; fall back to legacy field names when reading from old tree snapshots for compatibility Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
228 lines
7.2 KiB
Python
228 lines
7.2 KiB
Python
"""Unit tests for tree validation logic.
|
|
|
|
Tests validate_tree_structure, can_publish_tree, and edge cases.
|
|
No database required.
|
|
"""
|
|
|
|
from app.core.tree_validation import (
|
|
TreeValidationError,
|
|
can_publish_tree,
|
|
validate_tree_structure,
|
|
)
|
|
|
|
|
|
class TestValidateTreeStructure:
|
|
|
|
def test_valid_solution_tree(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "solution", "title": "Done"
|
|
})
|
|
assert valid
|
|
assert errors == []
|
|
|
|
def test_valid_decision_tree_with_branches(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root",
|
|
"type": "decision",
|
|
"question": "Is it on?",
|
|
"children": [
|
|
{"id": "yes", "type": "solution", "title": "Great"},
|
|
{"id": "no", "type": "action", "title": "Turn it on"},
|
|
],
|
|
})
|
|
assert valid
|
|
assert errors == []
|
|
|
|
def test_empty_structure_fails(self):
|
|
valid, errors = validate_tree_structure({})
|
|
assert not valid
|
|
assert any("empty" in e["message"].lower() for e in errors)
|
|
|
|
def test_missing_id_on_root(self):
|
|
valid, errors = validate_tree_structure({"type": "solution", "title": "X"})
|
|
assert not valid
|
|
assert any("id" in e["field"] for e in errors)
|
|
|
|
def test_missing_type_on_root(self):
|
|
valid, errors = validate_tree_structure({"id": "root"})
|
|
assert not valid
|
|
assert any("type" in e["field"] for e in errors)
|
|
|
|
def test_decision_missing_question(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "decision"
|
|
})
|
|
assert not valid
|
|
assert any("question" in e["message"].lower() for e in errors)
|
|
|
|
def test_decision_with_empty_question(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "decision", "question": ""
|
|
})
|
|
assert not valid
|
|
|
|
def test_decision_with_one_child_fails(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root",
|
|
"type": "decision",
|
|
"question": "Q?",
|
|
"children": [
|
|
{"id": "only", "type": "solution", "title": "S"},
|
|
],
|
|
})
|
|
assert not valid
|
|
assert any("2 branches" in e["message"] for e in errors)
|
|
|
|
def test_decision_with_zero_children_passes(self):
|
|
"""Decision with no children is valid (leaf decision)."""
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "decision", "question": "Q?"
|
|
})
|
|
assert valid
|
|
|
|
def test_action_missing_title_field(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "action"
|
|
})
|
|
assert not valid
|
|
assert any("title" in e["message"].lower() for e in errors)
|
|
|
|
def test_action_with_empty_title(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "action", "title": ""
|
|
})
|
|
assert not valid
|
|
|
|
def test_solution_missing_title_field(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "solution"
|
|
})
|
|
assert not valid
|
|
assert any("title" in e["message"].lower() for e in errors)
|
|
|
|
def test_solution_with_empty_title(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "solution", "title": ""
|
|
})
|
|
assert not valid
|
|
|
|
def test_unknown_node_type(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root", "type": "banana"
|
|
})
|
|
assert not valid
|
|
assert any("unknown" in e["message"].lower() for e in errors)
|
|
|
|
def test_child_missing_id(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root",
|
|
"type": "decision",
|
|
"question": "Q?",
|
|
"children": [
|
|
{"type": "solution", "title": "S1"},
|
|
{"id": "c2", "type": "solution", "title": "S2"},
|
|
],
|
|
})
|
|
assert not valid
|
|
assert any("id" in e["field"] for e in errors)
|
|
|
|
def test_child_missing_type(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root",
|
|
"type": "decision",
|
|
"question": "Q?",
|
|
"children": [
|
|
{"id": "c1"},
|
|
{"id": "c2", "type": "solution", "title": "S2"},
|
|
],
|
|
})
|
|
assert not valid
|
|
assert any("type" in e["field"] for e in errors)
|
|
|
|
def test_deeply_nested_validation(self):
|
|
"""Validates 3 levels deep."""
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root",
|
|
"type": "decision",
|
|
"question": "Level 1?",
|
|
"children": [
|
|
{
|
|
"id": "l2a",
|
|
"type": "decision",
|
|
"question": "Level 2?",
|
|
"children": [
|
|
{"id": "l3a", "type": "solution", "title": "Deep"},
|
|
{"id": "l3b", "type": "solution"}, # Missing title
|
|
],
|
|
},
|
|
{"id": "l2b", "type": "solution", "title": "Shallow"},
|
|
],
|
|
})
|
|
assert not valid
|
|
assert any("l3b" in str(e) or "children[1]" in e["field"] for e in errors)
|
|
|
|
def test_multiple_errors_collected(self):
|
|
valid, errors = validate_tree_structure({
|
|
"id": "root",
|
|
"type": "decision",
|
|
"question": "Q?",
|
|
"children": [
|
|
{"id": "c1", "type": "solution"}, # missing title
|
|
{"id": "c2", "type": "action"}, # missing title
|
|
],
|
|
})
|
|
assert not valid
|
|
assert len(errors) >= 2
|
|
|
|
|
|
class TestCanPublishTree:
|
|
|
|
def test_valid_tree_can_publish(self):
|
|
can, errors = can_publish_tree(
|
|
{"id": "root", "type": "solution", "title": "Done"},
|
|
"My Tree"
|
|
)
|
|
assert can
|
|
assert errors == []
|
|
|
|
def test_empty_name_cannot_publish(self):
|
|
can, errors = can_publish_tree(
|
|
{"id": "root", "type": "solution", "title": "Done"},
|
|
""
|
|
)
|
|
assert not can
|
|
assert any("name" in e["field"] for e in errors)
|
|
|
|
def test_whitespace_name_cannot_publish(self):
|
|
can, errors = can_publish_tree(
|
|
{"id": "root", "type": "solution", "title": "Done"},
|
|
" "
|
|
)
|
|
assert not can
|
|
|
|
def test_none_name_cannot_publish(self):
|
|
can, errors = can_publish_tree(
|
|
{"id": "root", "type": "solution", "title": "Done"},
|
|
None
|
|
)
|
|
assert not can
|
|
|
|
def test_invalid_structure_cannot_publish(self):
|
|
can, errors = can_publish_tree({}, "My Tree")
|
|
assert not can
|
|
assert len(errors) >= 1
|
|
|
|
def test_both_name_and_structure_errors(self):
|
|
can, errors = can_publish_tree({}, "")
|
|
assert not can
|
|
assert len(errors) >= 2 # name error + structure error
|
|
|
|
|
|
class TestTreeValidationError:
|
|
|
|
def test_exception_attributes(self):
|
|
err = TreeValidationError("field.name", "is required")
|
|
assert err.field == "field.name"
|
|
assert err.message == "is required"
|
|
assert "field.name" in str(err)
|