diff options
Diffstat (limited to 'packages/meshbay-node/tests/test_indexer.py')
| -rw-r--r-- | packages/meshbay-node/tests/test_indexer.py | 85 |
1 files changed, 85 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_indexer.py b/packages/meshbay-node/tests/test_indexer.py index 4bae620..9aa9fb9 100644 --- a/packages/meshbay-node/tests/test_indexer.py +++ b/packages/meshbay-node/tests/test_indexer.py @@ -2,6 +2,7 @@ import asyncio import os +import threading import time import pytest from pathlib import Path @@ -521,6 +522,90 @@ async def test_walk_root_does_not_stall_the_event_loop(tmp_path, sk_node, gek): @pytest.mark.asyncio +async def test_scanning_flag_is_true_while_the_walk_is_still_running(tmp_path, sk_node, gek): + """ + Regression for the actual bug this was found by (2026-08-25): `scanning` + used to flip on only *after* the walk finished, so a consumer polling + IndexProgress — index-status; the Create Group wizard's own progress + poll, which gives up after a short grace period if it never observes + `scanning: true` — read "not scanning" for however long a large/slow + root's discovery phase took, even though the node was already doing real + work. Confirmed against production logs: a wizard step waiting on this + flag gave up exactly at its grace-period deadline for a root whose walk + was still running. + """ + d = tmp_path / "shared" + d.mkdir() + (d / "f.bin").write_bytes(b"x") + + real_walk = indexer_mod._walk_root + walk_started = threading.Event() + release_walk = threading.Event() + + def slow_walk(root): + walk_started.set() + release_walk.wait(timeout=5) + return real_walk(root) + + indexer_mod._walk_root = slow_walk + indexer = DirectoryIndexer(roots=one_root(d), group_id="g", sk_node=sk_node, gek=gek) + try: + scan_task = asyncio.create_task(indexer.initial_scan()) + # Off-loop wait for the walk to actually start — busy-polling the + # event loop itself here would defeat the point of the test. + await asyncio.get_event_loop().run_in_executor(None, walk_started.wait, 5) + + assert indexer.progress.scanning is True, ( + "scanning must be True the moment the walk starts, not only " + "once it (and the sizing pass) finish") + finally: + release_walk.set() + indexer_mod._walk_root = real_walk + await scan_task + + assert indexer.progress.scanning is False + + +@pytest.mark.asyncio +async def test_scanning_flag_is_true_while_sizing_files_is_still_running( + tmp_path, sk_node, gek): + """ + Same regression, one phase later: sizing (stat()-ing every walked file) + used to be a synchronous loop straight on the asyncio event loop thread — + for a root with many thousands of files (a real personal library, not a + hypothetical) that blocked the entire daemon, and did so before + `scanning` was ever set. Now off-loop (_size_files) and `scanning` is + already true throughout, same principle as the walk phase above. + """ + d = tmp_path / "shared" + d.mkdir() + (d / "f.bin").write_bytes(b"x") + + real_size = indexer_mod._size_files + sizing_started = threading.Event() + release_sizing = threading.Event() + + def slow_size(files): + sizing_started.set() + release_sizing.wait(timeout=5) + return real_size(files) + + indexer_mod._size_files = slow_size + indexer = DirectoryIndexer(roots=one_root(d), group_id="g", sk_node=sk_node, gek=gek) + try: + scan_task = asyncio.create_task(indexer.initial_scan()) + await asyncio.get_event_loop().run_in_executor(None, sizing_started.wait, 5) + + assert indexer.progress.scanning is True + finally: + release_sizing.set() + indexer_mod._size_files = real_size + await scan_task + + assert indexer.progress.scanning is False + + +@pytest.mark.asyncio async def test_reconcile_backoff_grows_with_no_changes_then_caps(shared_dir, sk_node, gek): indexer = DirectoryIndexer(roots=one_root(shared_dir), group_id="g", sk_node=sk_node, gek=gek, reconcile_secs=0.01) |