mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
core/agent+tools+cli: number todos by listing order, not the internal id
This commit is contained in:
@@ -66,6 +66,29 @@ def test_ids_reuse_after_pop() -> None:
|
||||
assert g3.items[0].id == "t3"
|
||||
|
||||
|
||||
def test_render_numbers_follow_listing_order() -> None:
|
||||
"""Numbers are listing positions (TOP group first), never the stored ids."""
|
||||
stack = TodoStack()
|
||||
root = stack.push_group(["root", "sibling"]) # stored ids t1, t2
|
||||
_ = stack.push_group(["child"]) # stored id t3
|
||||
listing = stack.render()
|
||||
assert " [ ] 1. child" in listing
|
||||
assert " [ ] 2. root" in listing
|
||||
assert " [ ] 3. sibling" in listing
|
||||
assert "t1" not in listing
|
||||
assert "t3" not in listing
|
||||
|
||||
# update(1) is the first item the listing shows — the one stored as t3.
|
||||
updated = stack.update(1, status="done")
|
||||
assert updated.id == "t3"
|
||||
assert updated.title == "child"
|
||||
assert updated.status == "done"
|
||||
assert root.items[0].status == "pending"
|
||||
|
||||
with pytest.raises(ValueError, match="no todo item 4"):
|
||||
stack.update(4, status="done")
|
||||
|
||||
|
||||
def test_push_is_group_not_per_task_stack() -> None:
|
||||
"""Multi-title push is one group; pop removes the whole group."""
|
||||
stack = TodoStack()
|
||||
@@ -94,11 +117,16 @@ def test_dfs_breakdown_with_groups() -> None:
|
||||
root = stack.push_group(["T1", "T2"])
|
||||
children = stack.push_group(["T1.1", "T1.2"])
|
||||
assert stack.depth == 2
|
||||
stack.update(children.items[0].id, status="done")
|
||||
stack.update(children.items[1].id, status="done")
|
||||
# Numbers come from the listing: the pushed children are the TOP group → 1, 2.
|
||||
stack.update(1, status="done")
|
||||
stack.update(2, status="done")
|
||||
assert [i.status for i in children.items] == ["done", "done"]
|
||||
_ = stack.pop()
|
||||
assert stack.depth == 1
|
||||
stack.update(root.items[0].id, status="done")
|
||||
# Children gone: the root group is the listing again → 1 is T1, 2 is T2.
|
||||
stack.update(1, status="done")
|
||||
assert root.items[0].status == "done"
|
||||
assert root.items[1].status == "pending"
|
||||
_ = stack.push_group(["T2.1"])
|
||||
assert stack.depth == 2
|
||||
assert stack.top_group is not None
|
||||
@@ -119,14 +147,14 @@ def test_single_title_still_one_group() -> None:
|
||||
def test_todo_stack_needs_review() -> None:
|
||||
stack = TodoStack()
|
||||
assert not stack.needs_review()
|
||||
item = stack.push("work")
|
||||
_ = stack.push("work")
|
||||
stack.begin_turn()
|
||||
# Non-empty + untouched → nag (open or hygiene).
|
||||
assert stack.needs_review()
|
||||
# Any todo_* access this turn suppresses end-of-turn nag, even with open items.
|
||||
stack.mark_touched()
|
||||
assert not stack.needs_review()
|
||||
stack.update(item.id, status="done")
|
||||
stack.update(1, status="done")
|
||||
stack.begin_turn()
|
||||
# Terminal-only stack: same gate (untouched only).
|
||||
assert stack.needs_review()
|
||||
@@ -212,7 +240,7 @@ def test_inject_todo_nag_strategies() -> None:
|
||||
|
||||
def test_todo_prompts_signal_undone_work() -> None:
|
||||
stack = TodoStack()
|
||||
item = stack.push("open work")
|
||||
_ = stack.push("open work")
|
||||
reminder = stack.turn_reminder_prompt()
|
||||
assert "[TODO OPEN WORK]" in reminder
|
||||
assert "Stack not empty" in reminder
|
||||
@@ -223,9 +251,9 @@ def test_todo_prompts_signal_undone_work() -> None:
|
||||
assert "[TODO OPEN WORK]" in review
|
||||
assert "undone work" in review.lower() or "open item" in review.lower()
|
||||
assert "open work" in review
|
||||
assert "t1:open work" in review or "open work" in review
|
||||
assert "1. open work" in review # the review embeds the numbered listing
|
||||
|
||||
stack.update(item.id, status="done")
|
||||
stack.update(1, status="done")
|
||||
terminal_review = stack.review_prompt()
|
||||
assert "[TODO HYGIENE]" in terminal_review
|
||||
assert "done/cancelled" in terminal_review
|
||||
|
||||
@@ -174,28 +174,28 @@ async def test_pretty_todo_push(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
)
|
||||
)
|
||||
result = (
|
||||
"pushed group (depth=1) items=[a1]\n"
|
||||
"pushed group (depth=1) items=[T1]\n"
|
||||
"(LIFO of groups: depth=1; TOP group = current breakdown level)\n"
|
||||
"group d=0 TOP:\n"
|
||||
" [ ] a1: T1"
|
||||
" [ ] 1. T1"
|
||||
)
|
||||
await render_events(_aiter([call, _result(result)]))
|
||||
out = capsys.readouterr().out
|
||||
assert "* Todo Push:" in out
|
||||
assert " [ ] a1: T1" in out
|
||||
assert " [ ] 1. T1" in out
|
||||
assert "[tool]" not in out
|
||||
|
||||
|
||||
async def test_pretty_todo_batch_merges_to_one_stack(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""A batch of todo calls: one summary line, then the last result's stack."""
|
||||
stack = "(LIFO of groups: depth=1; TOP group = current breakdown level)\ngroup d=0 TOP:\n [ ] a1: T1"
|
||||
stack = "(LIFO of groups: depth=1; TOP group = current breakdown level)\ngroup d=0 TOP:\n [ ] 1. T1"
|
||||
await render_events(
|
||||
_aiter(
|
||||
[
|
||||
_pretty_call("todo_push", '{"titles": ["T1"]}'),
|
||||
_pretty_call("todo_update", '{"item_id": "a1", "status": "done"}'),
|
||||
_result(f"pushed group (depth=1) items=[a1]\n{stack}"),
|
||||
_result(f"updated a1 → done: T1\n{stack}"),
|
||||
_pretty_call("todo_update", '{"number": 1, "status": "done"}'),
|
||||
_result(f"pushed group (depth=1) items=[T1]\n{stack}"),
|
||||
_result(f"updated 1 → done: T1\n{stack}"),
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -206,14 +206,14 @@ async def test_pretty_todo_batch_merges_to_one_stack(capsys: pytest.CaptureFixtu
|
||||
# Only the last result is printed, so the stack renders once.
|
||||
assert out.count("group d=0 TOP:") == 1
|
||||
assert "pushed group" not in out
|
||||
assert "updated a1 → done: T1" in out
|
||||
assert "updated 1 → done: T1" in out
|
||||
|
||||
|
||||
async def test_pretty_todo_batch_mixed_with_other_tool_stays_per_call(
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""A batch holding a non-todo call keeps one line (and stack) per todo call."""
|
||||
stack = "group d=0 TOP:\n [ ] a1: T1"
|
||||
stack = "group d=0 TOP:\n [ ] 1. T1"
|
||||
await render_events(
|
||||
_aiter(
|
||||
[
|
||||
@@ -867,13 +867,13 @@ async def test_pretty_vcs_kind_branch_and_diff(capsys: pytest.CaptureFixture[str
|
||||
|
||||
|
||||
async def test_pretty_todo_list_pop_and_clear(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
stack = " [ ] a1: T1\n [x] a2: T2"
|
||||
stack = " [ ] 1. T1\n [x] 2. T2"
|
||||
await render_events(_aiter([_pretty_call("todo_list", "{}"), _result(stack)]))
|
||||
out = capsys.readouterr().out
|
||||
assert "* Todo List:" in out
|
||||
assert " [ ] a1: T1" in out
|
||||
assert " [ ] 1. T1" in out
|
||||
|
||||
await render_events(_aiter([_pretty_call("todo_pop", "{}"), _result(f"popped TOP group (a1:T1)\n{stack}")]))
|
||||
await render_events(_aiter([_pretty_call("todo_pop", "{}"), _result(f"popped TOP group (T1)\n{stack}")]))
|
||||
out = capsys.readouterr().out
|
||||
assert "* Todo Pop:" in out
|
||||
|
||||
|
||||
@@ -194,9 +194,9 @@ def test_complete_slash_args_from_registry(tmp_path: object) -> None:
|
||||
|
||||
state.todo_stack = TodoStack()
|
||||
_ = state.todo_stack.push_group(["alpha", "beta"])
|
||||
ids = complete_slash_args(state, "/todos", "t", prior_args=["done"])
|
||||
assert "t1" in ids
|
||||
assert "t2" in ids
|
||||
numbers = complete_slash_args(state, "/todos", "", prior_args=["done"])
|
||||
assert numbers == ["1", "2"]
|
||||
assert complete_slash_args(state, "/todos", "2", prior_args=["done"]) == ["2"]
|
||||
|
||||
state.remember_session_ids([3, 12, 40])
|
||||
assert complete_slash_args(state, "/resume", "1") == ["12"]
|
||||
|
||||
@@ -757,3 +757,26 @@ async def test_btw_read_notice_states_read_only_scope(
|
||||
assert "read-only" in notice.body
|
||||
assert "not saved" in notice.body
|
||||
assert "[notice] aside: read tools" in capsys.readouterr().out
|
||||
|
||||
|
||||
async def test_todos_update_takes_a_listing_number(state: ReplState, capsys: pytest.CaptureFixture[str]) -> None:
|
||||
assert await handle_slash(state, "/todos push first; second") is True
|
||||
out = capsys.readouterr().out
|
||||
assert "pushed group depth=1 items=[first, second]" in out
|
||||
assert " [ ] 1. first" in out
|
||||
assert " [ ] 2. second" in out
|
||||
assert "t1" not in out
|
||||
|
||||
assert await handle_slash(state, "/todos done 2") is True
|
||||
out = capsys.readouterr().out
|
||||
assert "updated 2 → done" in out
|
||||
assert " [x] 2. second" in out
|
||||
|
||||
# The stored id is not an address: only listing numbers are.
|
||||
assert await handle_slash(state, "/todos done t1") is True
|
||||
assert "takes an item number from the list" in capsys.readouterr().out
|
||||
assert await handle_slash(state, "/todos done 7") is True
|
||||
assert "no todo item 7" in capsys.readouterr().out
|
||||
|
||||
assert await handle_slash(state, "/todos pop") is True
|
||||
assert "popped TOP group (first, second)" in capsys.readouterr().out
|
||||
|
||||
@@ -101,6 +101,33 @@ async def test_session_on_todo_change_fires() -> None:
|
||||
assert hits == ["session"]
|
||||
|
||||
|
||||
async def test_todo_update_uses_listing_numbers() -> None:
|
||||
store = MemoryViewStore({})
|
||||
session = SessionState(session_id="numbers", data=session_data_view(store=store))
|
||||
registry = ToolRegistry(list(TODO_TOOLS), session_state=session)
|
||||
|
||||
out = await registry.execute("todo_push", '{"titles": ["root"]}')
|
||||
assert "pushed group (depth=1) items=[root]" in out
|
||||
out = await registry.execute("todo_push", '{"titles": ["child"]}')
|
||||
assert "pushed group (depth=2) items=[child]" in out
|
||||
# TOP group first; the stored ids stay out of every result.
|
||||
assert " [ ] 1. child" in out
|
||||
assert " [ ] 2. root" in out
|
||||
assert "t1" not in out and "t2" not in out
|
||||
|
||||
numbered = await registry.execute("todo_update", '{"number": 2, "status": "done"}')
|
||||
assert "updated 2 → done: root" in numbered
|
||||
assert " [x] 2. root" in numbered
|
||||
|
||||
stale = await registry.execute("todo_update", '{"number": 9, "status": "done"}')
|
||||
assert stale.startswith("error: no todo item 9")
|
||||
shaped = await registry.execute("todo_update", '{"number": "first"}')
|
||||
assert shaped.startswith("error: `todo_update` expects `number`")
|
||||
|
||||
popped = await registry.execute("todo_pop", "{}")
|
||||
assert "popped TOP group (child); new top group=[root]" in popped
|
||||
|
||||
|
||||
async def test_todo_without_session_errors() -> None:
|
||||
registry = ToolRegistry(list(TODO_TOOLS))
|
||||
out = await registry.execute("todo_list", "{}")
|
||||
|
||||
Reference in New Issue
Block a user