From 4a76f6ba41142b1c586ca70dadb7e1d0b505907f Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 3 Mar 2026 15:41:58 -0800 Subject: [PATCH] =?UTF-8?q?fix(avocet):=20undo=20=E2=80=94=20commit-then-c?= =?UTF-8?q?lear=20order,=20empty-records=20guard,=20skip=20dedup,=20strong?= =?UTF-8?q?er=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api.py | 34 +++++++++++++++++++--------------- tests/test_api.py | 3 +++ 2 files changed, 22 insertions(+), 15 deletions(-) diff --git a/app/api.py b/app/api.py index 86a3e12..ccbefd1 100644 --- a/app/api.py +++ b/app/api.py @@ -134,28 +134,32 @@ def delete_undo(): if not _last_action: raise HTTPException(404, "No action to undo") action = _last_action - _last_action = None - item = action["item"] # always the original clean queue item + # Perform file operations FIRST — only clear _last_action on success if action["type"] == "label": - # Remove last entry from score file records = _read_jsonl(_score_file()) + if not records: + raise HTTPException(409, "Score file is empty — cannot undo label") _write_jsonl(_score_file(), records[:-1]) - elif action["type"] == "discard": - # Remove last entry from discarded file - records = _read_jsonl(_discarded_file()) - _write_jsonl(_discarded_file(), records[:-1]) - elif action["type"] == "skip": - # Item is at back of queue — move it to front items = _read_jsonl(_queue_file()) - reordered = [item] + [x for x in items if x["id"] != item["id"]] - _write_jsonl(_queue_file(), reordered) - return {"undone": {"type": action["type"], "item": item}} + _write_jsonl(_queue_file(), [item] + items) + elif action["type"] == "discard": + records = _read_jsonl(_discarded_file()) + if not records: + raise HTTPException(409, "Discarded file is empty — cannot undo discard") + _write_jsonl(_discarded_file(), records[:-1]) + items = _read_jsonl(_queue_file()) + _write_jsonl(_queue_file(), [item] + items) + elif action["type"] == "skip": + items = _read_jsonl(_queue_file()) + # Remove the item wherever it sits (guards against duplicate insertion), + # then prepend it to the front — restoring it to position 0. + items = [item] + [x for x in items if x["id"] != item["id"]] + _write_jsonl(_queue_file(), items) - # For label and discard: restore item to front of queue - items = _read_jsonl(_queue_file()) - _write_jsonl(_queue_file(), [item] + items) + # Clear AFTER all file operations succeed + _last_action = None return {"undone": {"type": action["type"], "item": item}} diff --git a/tests/test_api.py b/tests/test_api.py index 0a9ea23..2d5b8bc 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -116,6 +116,9 @@ def test_undo_label_removes_from_score(client, queue_with_items): assert data["undone"]["type"] == "label" score = api_module._read_jsonl(api_module._score_file()) assert score == [] + # Item should be restored to front of queue + queue = api_module._read_jsonl(api_module._queue_file()) + assert queue[0]["id"] == "id0" def test_undo_discard_removes_from_discarded(client, queue_with_items): from app import api as api_module