diff --git a/haiku_rag_slim/haiku/rag/ingester/pollers/fs.py b/haiku_rag_slim/haiku/rag/ingester/pollers/fs.py index d619e8e1..7e68f38a 100644 --- a/haiku_rag_slim/haiku/rag/ingester/pollers/fs.py +++ b/haiku_rag_slim/haiku/rag/ingester/pollers/fs.py @@ -131,7 +131,12 @@ class FSPoller(BasePoller): return if change in (Change.added, Change.modified): - revision = str(path.stat().st_mtime_ns) if path.exists() else None + try: + revision = str(path.stat().st_mtime_ns) + except FileNotFoundError: + # File was deleted between the watchfiles event and our + # stat() call. Skip — the deletion event will handle it. + return await self._jobs.enqueue( self.source_id, uri, diff --git a/tests/ingester/test_pollers.py b/tests/ingester/test_pollers.py index 3aa4b2b9..0ee158e7 100644 --- a/tests/ingester/test_pollers.py +++ b/tests/ingester/test_pollers.py @@ -484,6 +484,22 @@ async def test_watch_deleted_then_added_enqueues_upsert(tmp_path, jobs, sync): assert queued[0].op is JobOp.UPSERT +@pytest.mark.asyncio +async def test_watch_added_file_deleted_before_stat_does_not_crash(tmp_path, jobs, sync): + """If a file is deleted between the watchfiles event and the stat() + call, the handler should return silently instead of raising + FileNotFoundError and killing the watch loop.""" + from watchfiles import Change + + poller = _fs_poller(tmp_path, jobs, sync) + missing = tmp_path / "vanished.md" + # File doesn't exist — simulate Change.added arriving after deletion. + await poller._handle_watch_change(Change.added, missing) + + # No job should be enqueued, and no exception should have propagated. + assert await jobs.list_jobs(source_id="local") == [] + + # --- FSPoller end-to-end smoke ---