diff --git a/Dockerfile b/Dockerfile index b6a3d980..76a476ee 100644 --- a/Dockerfile +++ b/Dockerfile @@ -97,7 +97,7 @@ WORKDIR /workspace CMD ["sleep", "infinity"] # Build frontend on the native builder because the emitted assets are architecture-independent. -FROM --platform=$BUILDPLATFORM node:24.22.1-alpine@sha256:06d7adf3e0ef61a13f1d28682726366f97a15aa1888c934d3ecec86c33918b53 AS frontend-builder +FROM --platform=$BUILDPLATFORM node:24.22.2-alpine@sha256:8a32c8cf649a49915a66e81e7a609223c708d0adabf288ee3f4077d765542f85 AS frontend-builder WORKDIR /app COPY frontend/package*.json ./ COPY frontend/scripts ./scripts diff --git a/backend/tests/test_directory_index_contract.py b/backend/tests/test_directory_index_contract.py index 59e29786..5b625e58 100644 --- a/backend/tests/test_directory_index_contract.py +++ b/backend/tests/test_directory_index_contract.py @@ -4971,12 +4971,15 @@ def failed_hint(parent): @pytest.mark.asyncio +@pytest.mark.parametrize("yield_after_parent", [False, True]) @pytest.mark.parametrize("failed", [False, True]) @pytest.mark.parametrize( "operation,prefix", [("create", ""), ("create", "scoped"), ("archive", "scoped"), ("mixed_archive", "scoped"), ("upload", "scoped")], ) -async def test_smb_mutation_queues_parent_before_dispatch_and_after_exit(sqlite_cache, monkeypatch, failed, prefix, operation): +async def test_smb_mutation_queues_parent_before_dispatch_and_after_exit( + sqlite_cache, monkeypatch, failed, prefix, operation, yield_after_parent +): from contextlib import asynccontextmanager, contextmanager from io import BytesIO from pathlib import PureWindowsPath @@ -4986,10 +4989,12 @@ async def test_smb_mutation_queues_parent_before_dispatch_and_after_exit(sqlite_ from app.api.archive_operations import _ensure_mixed_archive_parent_directories from app.models.file import FileType + from app.services import directory_cache from app.services.archive.extraction import _ensure_live_directory from app.storage.smb import SMBBackend index = sqlite_cache.index + clock = [sqlite_cache._last_access] index.write_paths(["parent"]) assert index.finish_generation(1) before = index.search("parent") @@ -5031,6 +5036,8 @@ def scan(path, **kwargs): parent, _, name = directory.rpartition("/") if parent == relative: yield SimpleNamespace(name=name, smb_info=SimpleNamespace(file_attributes=FileAttributes.FILE_ATTRIBUTE_DIRECTORY)) + if yield_after_parent: + clock[0] += directory_cache.WORKER_POLL_SECONDS @contextmanager def open_writer(path, **kwargs): @@ -5068,7 +5075,14 @@ def write(chunk): sqlite_cache.coordinator.apply_pending() assert set(index.pending_scan_paths(1)) == {relative_path(path).rpartition("/")[0] for path in dispatched} assert index.search("parent") == before - sqlite_cache._reconcile_disk_queue() + monkeypatch.setattr(directory_cache, "time", SimpleNamespace(monotonic=lambda: clock[0], time=directory_cache.time.time)) + # Reconciliation can yield after each parent; include the source root in the turn bound. + for reconciliation_turn in range(len(source_directories) + 1): + sqlite_cache._reconcile_disk_queue() + if index.reconciliation_generation() is None: + break + if yield_after_parent: + assert reconciliation_turn > 0 after = sqlite_cache.search_index("parent new") assert {row.path for row in after.results} == source_directories assert after.total_matches == after.directory_count == len(source_directories)