From fb6452934fffbe96fb0df75da62fed6fc81f2296 Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Fri, 30 Oct 2026 16:54:07 -0700 Subject: [PATCH] test(trash): classify the Needs review link route and prove it refuses a trashed file POST /review/items/{id}/link needs a capture of the item's bytes. A capture is refused for a path in a trash folder, and a capture taken earlier stops working when its file moves to a trash folder or its folder becomes a link to one, so no code change is needed. The decision route takes the same path, so it moves to the same audit group. --- tests/test_api_review_link.py | 53 +++++++++++++++++++++++++++++++++++ tests/trash_route_audit.py | 3 +- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/tests/test_api_review_link.py b/tests/test_api_review_link.py index c07a9c0c..f9e5a28c 100644 --- a/tests/test_api_review_link.py +++ b/tests/test_api_review_link.py @@ -6,6 +6,8 @@ records provenance that cannot pose as what the file says. """ import json +import os +import unittest try: from tests.test_api_review import ITEMS_URL, ReviewRoutesTestCase @@ -339,3 +341,54 @@ def detail_version(case, item): return conn.execute( "SELECT resolver_version FROM media_review_items WHERE id=?", (item["id"],) ).fetchone()[0] + + +class TestLinkNeverActsOnATrashedFile(LinkTestCase): + """#543's gate: a link needs a capture, and neither way to one reaches a trash.""" + + TRASH = ".Trash-1000" + + async def _captured_in_sub(self): + path = self._ambiguous("sub/two.png") + self._record(path) + item, detail, observed = await self._open() + positive_id, pair_id = self._pair() + return path, item, self._link_body(detail, observed, positive_id, pair_id) + + async def _assert_refused(self, item, body): + before = self._snapshot() + status, data = await self._link(item["id"], body) + self.assertGreaterEqual(status, 400, data) + self.assertFalse(data.get("success"), data) + self.assertEqual(self._snapshot(), before) + + async def test_a_file_moved_to_a_trash_folder_after_its_capture_is_refused(self): + path, item, body = await self._captured_in_sub() + trash = self.output_dir / self.TRASH / "files" + trash.mkdir(parents=True) + os.rename(path, trash / path.name) + await self._assert_refused(item, body) + + async def test_a_folder_swapped_for_a_link_into_a_trash_folder_is_refused(self): + path, item, body = await self._captured_in_sub() + trash = self.output_dir / self.TRASH / "files" + trash.parent.mkdir(parents=True) + os.rename(path.parent, trash) + try: + os.symlink(trash, path.parent, target_is_directory=True) + except OSError as exc: + raise unittest.SkipTest(f"no symlinks here: {exc}") + self.assertTrue(path.exists()) + await self._assert_refused(item, body) + + async def test_an_item_in_a_trash_folder_cannot_get_a_capture(self): + path = self._ambiguous(f"{self.TRASH}/files/two.png") + self._record(path) + item = (await self._list())["items"][0] + status, data = await self._json( + "POST", + "/prompt_manager/image-annotations/observe", + json={"target": item["target"]}, + ) + self.assertEqual(status, 404, data) + self.assertFalse(data.get("capture"), data) diff --git a/tests/trash_route_audit.py b/tests/trash_route_audit.py index a18e2387..f55c027f 100644 --- a/tests/trash_route_audit.py +++ b/tests/trash_route_audit.py @@ -53,6 +53,8 @@ ROUTE_AUDIT = { "GET /prompt_manager/collections/membership", "PUT /prompt_manager/images/category", "DELETE /prompt_manager/images/category", + "POST /prompt_manager/review/items/{item_id}/decision", + "POST /prompt_manager/review/items/{item_id}/link", ), "walker: utils/media_folders (listed files only)": ( "GET /prompt_manager/images/output", @@ -94,7 +96,6 @@ ROUTE_AUDIT = { "GET /prompt_manager/history/runs/{run_id}", "GET /prompt_manager/review/items", "GET /prompt_manager/review/items/{item_id}", - "POST /prompt_manager/review/items/{item_id}/decision", "POST /prompt_manager/maintenance/preview", "POST /prompt_manager/maintenance/apply", ),