Relationships
↔ sibling #2856#2858 Tests: characterise stale catalogs when two repos or serve nodes share a filesystem datastore
Opened by stack72 · 9/30/2026· Shipped 10/1/2026
Background (read this first)
swamp is about to rework its datastore layer. The design proposal is design/enablers/datastore-commit-log.md on the datastore-rework branch. It is not merged; §2 of it is a write-path inventory.
How the datastore works today.
- Repositories write files into the repo's
.swamp/directory, or into the datastore cache dir for a custom datastore. They then call a mark hook,markDirty(relPath?), which reachesDatastoreSyncService(src/domain/datastore/datastore_sync_service.ts, interface at :168, the eight-rulemarkDirtycontract at :199-257). - A caller then flushes, through one of four paths:
acquireModelLocks().flush(src/cli/repo_context.ts:1723);- the global coordinator
flushDatastoreSync(src/cli/mod.ts:2251); pushManagedConfigChanges(src/cli/managed_config_sync.ts:45/153);- serve's
pushChangedToRemote(src/serve/handlers/shared.ts:83-97) under the sync gate.
- The S3 and GCS sync implementations live in swamp-extensions, not here. They own the dirty set, the
.datastore-sync-state.jsonsidecar,_indexshards and_meta.json. - A filesystem datastore has no sync service at all (
repo_context.ts:1170-1178). - Each repo has a SQLite catalog,
.swamp/data/_catalog.db, thatDataQueryServicereads. - Serve runs pollers that pull peers' changes: Config, AccessData, RuntimeData.
What changes. The refactor lands on main in phases:
- Phase 1: a
UnitOfWorkport with a legacy adapter, so repositories stage writes instead of calling the hook directly. - Phase 2: libswamp use cases own the unit of work, and CLI commands and serve stop calling
markDirty/pushChangedthemselves. - Later phases: a new commit-log engine behind an opt-in format.
Phases 1 and 2 must not change any behaviour.
Why this issue exists. Before any of that lands, this repo's own tests have to pin today's behaviour, so a dropped mark, a lost delete or a missed catalog refresh fails a test instead of reaching users. An audit in September 2026 found:
- no in-memory remote to test sync against;
- no test that every file a repository changes gets marked, and several real unmarked paths;
- no two-repo propagation test;
- no ratchet on the write seams Phase 1 will move;
- the sync-service conformance suite only checks method shapes.
This issue is one of a set. The tracking issue is listed under Related. End-to-end CLI tests for the same risks are in swamp-club/swamp-uat #481-#485, #487, #490 and #492.
Rules (from AGENTS.md)
- Unit tests are in-process only: no subprocesses, no process-global mutation; use
withMockedEnv. - Integration tests in
integration/wire real components on a temp filesystem and must not spawn the CLI. - Registry-registered test types use a per-run name built with
crypto.randomUUID(), orinvalidateTypein afinally. - No fixed sleeps (use
waitForfrom@swamp-club/swamp-testing), no wall-clock assertions, andDeno.utimefor mtimes. - Use
@std/pathfor paths andassertPathEqualsfor path comparisons, since tests run on Windows too. - New files need the AGPLv3 header (
deno run license-headers). - Pin today's behaviour, including known gaps. When today's behaviour is a bug, assert it as it is, with a comment naming the bug. Prefer an explicit pinned list checked with
assertPinnedSet, so a later fix forces the test to be updated on purpose. Do not fix production code in these issues unless the issue says so.
Goal
Add a characterisation test that pins a known bug. When two repos, or two swamp serve nodes, share one filesystem datastore, a repo whose catalog is already populated never sees the other's writes or deletes through catalog queries. Pinning it now means:
- the refactor doesn't make it worse without anyone noticing;
- the phase that fixes it flips the assertions on purpose;
- swamp-uat #485 and #481 (which add a shared-filesystem backend to the end-to-end suite) have a documented explanation when they hit it.
How it happens
- A filesystem datastore gets no sync service (
src/domain/datastore/datastore_config.ts:167-171,src/cli/repo_context.ts:1170-1178), andrequireInitializedRepoUnlockedhas no invalidation logic. - So serve creates no sync gate and no access or runtime pollers (
src/cli/commands/serve.ts:2286,:3470-3489). The ConfigPoller only checks the lockfile hash. lockResult.syncedis always false, so the per-requestcatalogStore.invalidate()calls never fire.- The catalog is per repo at
{repoDir}/.swamp/data/_catalog.db(src/infrastructure/persistence/repository_factory.ts:104-111), and itspopulatedflag persists (catalog_store.ts:643-670). - Then:
DataQueryService.getLatestRecordreturns null for names the other repo created (data_query_service.ts:256), or an old row for names it already knew (:229-241);querynever backfills (:392-393);- deleted rows are hidden only in
query, throughfilterStaleRows(:659-688).
There is no issue, test or TODO for this today. The only write-up is §4 of the design doc.
Test
integration/filesystem_shared_datastore_catalog_test.ts
- Two temp repos A and B with
{ type: "filesystem", path: <shared dir> }and the same namespace (or none), each built throughrequireInitializedRepoUnlockedas serve builds it. Use thewriteOneItempattern fromintegration/giga_swamp_namespace_layout_test.ts:81-118. - B writes one item and queries, so B's catalog is populated.
- A writes a new name. Assert today's behaviour on B:
getLatestRecordreturns null, andqueryomits A's row. - A writes a new version of a name B knows. Assert B's
getLatestRecordreturns B's older row. - A deletes an item B has in its catalog. Assert
queryon B hides it (filterStaleRows), and record whatgetLatestRecordreturns. - A control case. After
catalogStore.invalidate()on B, B sees A's rows. This proves the only missing piece is invalidation.
Put a comment block at the top naming the source lines above, and say that the new datastore engine is expected to flip steps 3-5.
Done when
The test passes and documents the current behaviour. Also file a separate swamp-club Lab bug for the underlying behaviour if none exists, linked from the test comment.
Dependencies
Blocked by: nothing, so this can start now
Blocks: nothing
Full dependency graph and waves: swamp-club#2865.
Related
- Tracking issue: swamp-club#2865 (https://swamp-club.com/lab/2865).
- Soft link: swamp-uat#485 runs cross-clone deletes on the new filesystem backend (swamp-uat#481) and is expected to hit this bug. Land this first or at the same time.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.