Relationships
#2854 Test infra: shared in-memory remote datastore fake and recording sync service for pre-refactor datastore tests
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
Build a shared, in-memory remote datastore fake with content, plus a shared recording sync service. The other pre-refactor tests build on it, and Phase 1's legacy UnitOfWork adapter will be tested against it.
Current state
- There is no reusable fake. About 17 hand-rolled fake sync services are scattered through test files. Examples:
recordingSyncServiceatsrc/cli/repo_context_test.ts:3406createRecordingSyncServiceatintegration/mark_dirty_hook_sync_test.ts:62createRacingSyncServiceatintegration/server_token_mint_sync_test.ts:201-231
- The closest thing to a remote is
createFakeDatastore()insrc/serve/sync_gate_test.ts:400-437: a remote Set, a local Set and a dirty Set, with push treating a path absent locally as deleted remotely. It tracks paths only, not content. packages/testing/datastore_test_context.ts:91(createDatastoreTestContext) recordsmarkDirtyrelPaths (getSyncOperations()), but its push and pull do nothing and nothing uses it.
Work
- Add
createInMemoryRemote()inpackages/testing/, next tocreateDatastoreTestContext, exported from the package so swamp-extensions can use it too. It returns a remote holding aMap<relPath, Uint8Array>, and aconnect(cacheDir)that returns aDatastoreSyncServicebound to that cache directory. Several instances share one remote, one per simulated machine. - Behaviour of each connected service:
markDirty(relPath?)records the path; a call with no path sets a bulk flag. Follow the contract atdatastore_sync_service.ts:199-257.pushChanged:- for each marked path (or every file under the cache when bulk), upload the file if it exists locally, otherwise delete it remotely;
- clear the dirty set and bulk flag only on success;
- return a count.
pullChanged:- download remote files that are new or changed into the cache;
- delete local files the remote dropped, since the last pull, that are not dirty locally;
- return the count of changed files.
- Make the delete behaviour an option (
pullDeletes: true | false), so tests can reproduce either S3's or GCS's current behaviour. Document which one each extension does today, checked againstswamp-extensions/datastore/s3/extensions/datastores/_lib/s3_cache_sync.tsandgcs_cache_sync.ts.
preparePush/commitPush: two-phase, followingdatastore_sync_service.ts:379/405. Prepare does not clear dirty state; commit applies the change and clears it.capabilities(): match the interface.- Failure injection:
failNext("push" | "pull" | "prepare" | "commit", error?)andoffline(true | false). - Operation log:
ops()returns an ordered list of{instance, op, paths}, so tests can assert what was sent.
- Add
createRecordingSyncService()that recordsmarkDirtycalls, including bare ones, and does nothing else. Replace two or three of the hand-rolled copies with it to prove the API fits. Do not migrate all 17 in this issue. - Registration helper:
registerTestDatastoreType(remote)registers a per-run@test/remote-<uuid>datastore type indatastoreTypeRegistrywhose provider returnsremote.connect(cachePath).requireInitializedRepothen wires it end-to-end, following the pattern atsrc/cli/repo_context_test.ts:1004-1100. It returns a disposer that callsinvalidateType. - Tests for the fake itself (
*_test.tsnext to it):- round-trip between two instances;
- delete propagation;
- a failed push keeps the path dirty and the next push succeeds;
- bulk mark;
- two-phase;
pullDeleteson and off.
Done when
The fake and helpers are exported from packages/testing, tested, and used by at least one existing test in place of a hand-rolled fake.
Dependencies
Blocked by: nothing, so this can start now
Blocks: swamp-club#2855, swamp-club#2857, swamp-club#2859, swamp-club#2860, swamp-club#2861, swamp-club#2863
Full dependency graph and waves: swamp-club#2865.
Related
- Tracking issue: swamp-club#2865 (https://swamp-club.com/lab/2865).
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.