From 20ff03ed8e658420161b8c0fef71ab70064152bc Mon Sep 17 00:00:00 2001 From: Research Assistant Date: Mon, 18 May 2026 21:53:14 +0800 Subject: [PATCH] fix(prune): protect dir iteration, add OSError handling, improve test coverage --- paperforge/worker/prune.py | 13 ++++++++++--- tests/unit/worker/test_prune.py | 20 +++++++++++++++++++- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/paperforge/worker/prune.py b/paperforge/worker/prune.py index c308dfa3..c6d8deb8 100644 --- a/paperforge/worker/prune.py +++ b/paperforge/worker/prune.py @@ -18,7 +18,12 @@ def _collect_orphan_candidates(lit_dir: Path, fresh_keys: set[str]) -> list[dict for domain_dir in sorted(lit_dir.iterdir()): if not domain_dir.is_dir(): continue - for sub in sorted(domain_dir.iterdir()): + try: + entries = sorted(domain_dir.iterdir()) + except OSError as exc: + logger.warning("prune: cannot scan %s: %s", domain_dir, exc) + continue + for sub in entries: if not sub.is_dir(): continue parts = sub.name.split(" - ", 1) @@ -89,12 +94,14 @@ def prune_orphan_papers( ocr = c["ocr_dir"] if ocr and ocr.exists(): shutil.rmtree(ocr, ignore_errors=True) - counts["ocr"] += 1 + if not ocr.exists(): + counts["ocr"] += 1 ws = c["workspace_dir"] if ws.exists(): shutil.rmtree(ws, ignore_errors=True) - counts["workspace"] += 1 + if not ws.exists(): + counts["workspace"] += 1 try: n = delete_paper_vectors(vault, key) diff --git a/tests/unit/worker/test_prune.py b/tests/unit/worker/test_prune.py index 036739e9..0cae42bf 100644 --- a/tests/unit/worker/test_prune.py +++ b/tests/unit/worker/test_prune.py @@ -8,6 +8,7 @@ import pytest from paperforge.worker.prune import ( _collect_orphan_candidates, + _resolve_ocr_dir, prune_orphan_papers, ) @@ -65,6 +66,14 @@ class TestCollectOrphanCandidates: assert returned_keys == {"key2", "key3"} +class TestResolveOcrDir: + """_resolve_ocr_dir(vault, key)""" + + def test_resolves_ocr_dir_name_is_key(self, tmp_path: Path) -> None: + result = _resolve_ocr_dir(tmp_path, "testkey123") + assert result.name == "testkey123" + + class TestPruneOrphanPapers: """prune_orphan_papers(vault, fresh_index, dry_run)""" @@ -124,7 +133,16 @@ class TestPruneOrphanPapers: prune_orphan_papers(tmp_path, fresh_index=fresh_index, dry_run=True) assert calls == [] - def test_orphan_not_in_fresh_index_is_skipped(self, tmp_path: Path) -> None: + def test_empty_fresh_index_does_not_crash(self, tmp_path: Path) -> None: + result = prune_orphan_papers(tmp_path, fresh_index={}, dry_run=True) + assert result["preview"] == [] + + def test_handles_missing_lit_dir(self, tmp_path: Path) -> None: + no_lit = tmp_path / "NoLiterature" + result = prune_orphan_papers(no_lit, fresh_index={"schema_version": "3", "items": []}, dry_run=False) + assert result["deleted"] == [] + + def test_active_papers_are_not_deleted(self, tmp_path: Path) -> None: lit = tmp_path / "Resources" / "Literature" / "CS" ws = lit / "key1 - Active Paper" ws.mkdir(parents=True)