Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
"""Tests for nested subtasks in the todo tool (parent field).
|
||||
|
||||
Covers: validation of the parent field, dangling/cyclic parent
|
||||
sanitization, merge-mode parent updates, tree-aware post-compression
|
||||
injection, and the flat-only reorder guard.
|
||||
"""
|
||||
|
||||
import json
|
||||
|
||||
from tools.todo_tool import TodoStore, todo_tool
|
||||
|
||||
|
||||
def _item(i, status="pending", parent=None, content=None):
|
||||
d = {"id": i, "content": content or f"task {i}", "status": status}
|
||||
if parent is not None:
|
||||
d["parent"] = parent
|
||||
return d
|
||||
|
||||
|
||||
class TestParentValidation:
|
||||
def test_parent_preserved(self):
|
||||
store = TodoStore()
|
||||
items = store.write([_item("a"), _item("a1", parent="a")])
|
||||
assert items[1]["parent"] == "a"
|
||||
|
||||
def test_self_parent_dropped(self):
|
||||
store = TodoStore()
|
||||
items = store.write([_item("a", parent="a")])
|
||||
assert "parent" not in items[0]
|
||||
|
||||
def test_dangling_parent_dropped(self):
|
||||
store = TodoStore()
|
||||
items = store.write([_item("a", parent="ghost")])
|
||||
assert "parent" not in items[0]
|
||||
|
||||
def test_cycle_broken(self):
|
||||
store = TodoStore()
|
||||
items = store.write([_item("a", parent="b"), _item("b", parent="a")])
|
||||
# At least one link removed; no item can walk a loop
|
||||
by_id = {i["id"]: i for i in items}
|
||||
for item in items:
|
||||
seen = set()
|
||||
node = item
|
||||
while node.get("parent"):
|
||||
assert node["parent"] not in seen
|
||||
seen.add(node["id"])
|
||||
node = by_id[node["parent"]]
|
||||
|
||||
def test_empty_parent_omitted(self):
|
||||
store = TodoStore()
|
||||
items = store.write([_item("a", parent="")])
|
||||
assert "parent" not in items[0]
|
||||
|
||||
|
||||
class TestMergeParent:
|
||||
def test_merge_sets_parent(self):
|
||||
store = TodoStore()
|
||||
store.write([_item("a"), _item("b")])
|
||||
items = store.write([{"id": "b", "parent": "a"}], merge=True)
|
||||
by_id = {i["id"]: i for i in items}
|
||||
assert by_id["b"]["parent"] == "a"
|
||||
|
||||
def test_merge_clears_parent_with_empty_string(self):
|
||||
store = TodoStore()
|
||||
store.write([_item("a"), _item("b", parent="a")])
|
||||
items = store.write([{"id": "b", "parent": ""}], merge=True)
|
||||
by_id = {i["id"]: i for i in items}
|
||||
assert "parent" not in by_id["b"]
|
||||
|
||||
def test_merge_new_child_appended(self):
|
||||
store = TodoStore()
|
||||
store.write([_item("a")])
|
||||
items = store.write([_item("a2", parent="a")], merge=True)
|
||||
assert items[-1] == {"id": "a2", "content": "task a2", "status": "pending", "parent": "a"}
|
||||
|
||||
|
||||
class TestNestedInjection:
|
||||
def test_children_indented(self):
|
||||
store = TodoStore()
|
||||
store.write([
|
||||
_item("wp1", status="in_progress"),
|
||||
_item("t1", parent="wp1"),
|
||||
_item("t2", parent="wp1"),
|
||||
])
|
||||
text = store.format_for_injection()
|
||||
assert text is not None
|
||||
lines = text.split("\n")
|
||||
assert lines[1].startswith("- [>] wp1.")
|
||||
assert lines[2].startswith(" - [ ] t1.")
|
||||
assert lines[3].startswith(" - [ ] t2.")
|
||||
|
||||
def test_completed_parent_kept_when_child_active(self):
|
||||
store = TodoStore()
|
||||
store.write([
|
||||
_item("wp1", status="completed"),
|
||||
_item("t1", status="pending", parent="wp1"),
|
||||
])
|
||||
text = store.format_for_injection()
|
||||
assert text is not None
|
||||
assert "wp1" in text # parent context survives
|
||||
assert "[x]" in text
|
||||
assert " - [ ] t1." in text
|
||||
|
||||
def test_finished_subtree_omitted(self):
|
||||
store = TodoStore()
|
||||
store.write([
|
||||
_item("wp1", status="completed"),
|
||||
_item("t1", status="completed", parent="wp1"),
|
||||
_item("wp2", status="pending"),
|
||||
])
|
||||
text = store.format_for_injection()
|
||||
assert text is not None
|
||||
assert "wp1" not in text
|
||||
assert "t1" not in text
|
||||
assert "wp2" in text
|
||||
|
||||
def test_all_finished_returns_none(self):
|
||||
store = TodoStore()
|
||||
store.write([
|
||||
_item("a", status="completed"),
|
||||
_item("a1", status="cancelled", parent="a"),
|
||||
])
|
||||
assert store.format_for_injection() is None
|
||||
|
||||
def test_flat_injection_unchanged(self):
|
||||
store = TodoStore()
|
||||
store.write([_item("a", status="in_progress"), _item("b")])
|
||||
text = store.format_for_injection()
|
||||
assert text is not None
|
||||
assert text.split("\n")[1] == "- [>] a. task a (in_progress)"
|
||||
|
||||
|
||||
class TestOrderGuard:
|
||||
def test_nested_list_keeps_authored_order(self):
|
||||
store = TodoStore()
|
||||
items = store.write([
|
||||
_item("a", status="pending"),
|
||||
_item("b", status="in_progress"),
|
||||
_item("b1", parent="b"),
|
||||
])
|
||||
assert [i["id"] for i in items] == ["a", "b", "b1"]
|
||||
|
||||
def test_flat_list_still_reordered(self):
|
||||
store = TodoStore()
|
||||
items = store.write([
|
||||
_item("a", status="pending"),
|
||||
_item("b", status="in_progress"),
|
||||
])
|
||||
assert [i["id"] for i in items] == ["b", "a"]
|
||||
|
||||
|
||||
class TestToolRoundTrip:
|
||||
def test_parent_survives_json(self):
|
||||
store = TodoStore()
|
||||
out = todo_tool(todos=[_item("a"), _item("a1", parent="a")], store=store)
|
||||
data = json.loads(out)
|
||||
assert data["todos"][1]["parent"] == "a"
|
||||
assert data["summary"]["total"] == 2
|
||||
|
||||
def test_hydration_replay_preserves_parent(self):
|
||||
# Simulate _hydrate_todo_store: write the previous tool result's
|
||||
# todos array back into a fresh store in replace mode.
|
||||
store = TodoStore()
|
||||
out = json.loads(todo_tool(todos=[_item("a"), _item("a1", parent="a")], store=store))
|
||||
fresh = TodoStore()
|
||||
replayed = fresh.write(out["todos"], merge=False)
|
||||
assert replayed[1]["parent"] == "a"
|
||||
Reference in New Issue
Block a user