Relationships
#2863 Tests: serve pollers make a peer's writes, deletes and grants visible with a real catalog (before Phase 5)
Opened by stack72 · 9/30/2026
Deferred: needed before Phase 5 (serve leader and commit feed), not before Phase 1.
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
Pin serve's pollers against a real catalog and a peer's writes. A poll that pulls a peer's change must make it visible to queries and reload access policy. The later serve-leader phase replaces these pollers with a commit feed, and it has to match this behaviour.
Priority: needed before the serve-leader phase (Phase 5), not before Phase 1. It is filed now so the full baseline is tracked in one place.
Current state
- ConfigPoller (
src/serve/config_poller.ts) pullsconfig, invalidates when the pull returns>0or void (:178-187), then checks the lockfile hash (:196-221). - AccessDataPoller (grant and group subdirs, :35-40) invalidates and reloads policy on
>0; void counts as 0 (:127). - RuntimeDataPoller (
data) invalidates on>0; void counts as 0 (:113-118). That is inconsistent with ConfigPoller andhydrateLocalCache. - Wiring:
src/cli/commands/serve.ts:2747-2792(config) and:3470-3489(access and runtime, onlyif (syncService)). - Sync gate (
src/serve/sync_gate.ts):withSyncGate(:224) is exclusive forSYNC_GATED_REQUESTS(:166-196);withSharedSyncGate(:240) is for run pushes;gatedPull(:334-370) escalates after 3 skips.
- Unit tests use a mock sync service and a spy
catalogInvalidatewith no real CatalogStore:config_poller_test.ts,access_data_poller_test.ts,runtime_data_poller_test.ts, andsync_gate_test.ts, which includes a resurrection test at :439/:464 for swamp-club#2247. integration/serve_request_harness.tscreateServeCtxwires no sync service, gate or pollers.
Needs
The in-memory remote from the fake-remote issue. The optional syncService / syncGate parameters on createServeCtx are added in the use-case characterisation issue; add them here if that issue hasn't landed.
Test
integration/serve_poller_catalog_test.ts
- Runtime data. Serve context S on the in-memory remote with a real
repoContext.catalogStore. Its catalog is populated. A peer repo P writes data and pushes. After oneRuntimeDataPollertick on S,DataQueryService.queryandgetLatestRecordon S return P's row. - Delete. P deletes it and pushes. After one tick, S no longer returns it. Then a handler write on S, under the gate, followed by a push does not bring it back.
- Access. P adds a grant and pushes. After one
AccessDataPollertick, S's policy allows the new access. P revokes it, and after one tick S denies it. - Config. P changes a definition and pushes. After one
ConfigPollertick, S returns the new definition. A lockfile change triggers the extension reload path. - Void results. When the fake's pull returns void, pin today's inconsistency: Config invalidates, Access and Runtime do not.
- Gate. A poll that overlaps a gated write request waits or is skipped as
gatedPullspecifies, and after 3 skips it escalates.
Done when
The scenarios pass with real catalogs and policy objects, and the void inconsistency is pinned with a comment.
Dependencies
Blocked by: swamp-club#2854, swamp-club#2860
Blocks: nothing
Full dependency graph and waves: swamp-club#2865.
Related
- Tracking issue: swamp-club#2865 (https://swamp-club.com/lab/2865).
- Reuses the
createServeCtxsync parameters added in swamp-club#2860. - End-to-end counterpart: swamp-uat#491 (deferred).
Open
No activity in this phase yet.
Sign in to post a ripple.