From 0e7151ceae1d7abfec09725df976c12aac08dbc2 Mon Sep 17 00:00:00 2001 From: LeonSGP43 Date: Tue, 9 Jun 2026 23:19:32 +0800 Subject: [PATCH] fix(todo): keep active step ahead of pending rows --- tests/tools/test_todo_tool.py | 37 ++++++++++++++++++++++++++++++++--- tools/todo_tool.py | 31 +++++++++++++++++++++++++++-- 2 files changed, 63 insertions(+), 5 deletions(-) diff --git a/tests/tools/test_todo_tool.py b/tests/tools/test_todo_tool.py index 1dc19b88c7..c7b41f09c7 100644 --- a/tests/tools/test_todo_tool.py +++ b/tests/tools/test_todo_tool.py @@ -14,8 +14,9 @@ class TestWriteAndRead: ] result = store.write(items) assert len(result) == 2 - assert result[0]["id"] == "1" - assert result[1]["status"] == "in_progress" + assert result[0]["id"] == "2" + assert result[0]["status"] == "in_progress" + assert result[1]["id"] == "1" def test_write_deduplicates_duplicate_ids(self): @@ -26,8 +27,21 @@ class TestWriteAndRead: {"id": "1", "content": "Latest version", "status": "in_progress"}, ]) assert result == [ - {"id": "2", "content": "Other task", "status": "pending"}, {"id": "1", "content": "Latest version", "status": "in_progress"}, + {"id": "2", "content": "Other task", "status": "pending"}, + ] + + def test_write_moves_active_item_before_earlier_pending_step(self): + store = TodoStore() + result = store.write([ + {"id": "1", "content": "Already done", "status": "completed"}, + {"id": "2", "content": "Verify freed space", "status": "pending"}, + {"id": "3", "content": "Move archives to Trash", "status": "in_progress"}, + ]) + assert result == [ + {"id": "1", "content": "Already done", "status": "completed"}, + {"id": "3", "content": "Move archives to Trash", "status": "in_progress"}, + {"id": "2", "content": "Verify freed space", "status": "pending"}, ] @@ -91,6 +105,23 @@ class TestMergeMode: items = store.read() assert len(items) == 2 + def test_merge_reorders_active_item_ahead_of_earlier_pending_step(self): + store = TodoStore() + store.write([ + {"id": "1", "content": "Completed", "status": "completed"}, + {"id": "2", "content": "Verify freed space", "status": "pending"}, + {"id": "3", "content": "Move archives to Trash", "status": "pending"}, + ]) + result = store.write( + [{"id": "3", "status": "in_progress"}], + merge=True, + ) + assert result == [ + {"id": "1", "content": "Completed", "status": "completed"}, + {"id": "3", "content": "Move archives to Trash", "status": "in_progress"}, + {"id": "2", "content": "Verify freed space", "status": "pending"}, + ] + class TestTodoToolFunction: def test_read_mode(self): diff --git a/tools/todo_tool.py b/tools/todo_tool.py index 13b5fd4aad..1eea334f43 100644 --- a/tools/todo_tool.py +++ b/tools/todo_tool.py @@ -67,7 +67,9 @@ class TodoStore: """ if not merge: # Replace mode: new list entirely - self._items = [self._validate(t) for t in self._dedupe_by_id(todos)] + self._items = self._normalize_order( + [self._validate(t) for t in self._dedupe_by_id(todos)] + ) else: # Merge mode: update existing items by id, append new ones existing = {item["id"]: item for item in self._items} @@ -97,7 +99,7 @@ class TodoStore: if current["id"] not in seen: rebuilt.append(current) seen.add(current["id"]) - self._items = rebuilt + self._items = self._normalize_order(rebuilt) # Bound total item count so a replayed/oversized list can't grow the # re-injection block without limit. Keep the highest-priority head # (list order is priority). @@ -200,6 +202,31 @@ class TodoStore: last_index[item_id] = i return [todos[i] for i in sorted(last_index.values())] + @staticmethod + def _normalize_order(items: List[Dict[str, str]]) -> List[Dict[str, str]]: + """Lift the active step ahead of any earlier unfinished placeholders.""" + active_index = next( + (i for i, item in enumerate(items) if item["status"] == "in_progress"), + None, + ) + if active_index is None: + return items + + pending_index = next( + ( + i for i, item in enumerate(items[:active_index]) + if item["status"] == "pending" + ), + None, + ) + if pending_index is None: + return items + + normalized = items.copy() + active_item = normalized.pop(active_index) + normalized.insert(pending_index, active_item) + return normalized + def todo_tool( todos: Optional[List[Dict[str, Any]]] = None,