Skip to main content
← Back to list
01Issue
FeatureOpenSwamp CLIPublic
AssigneesNone

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 reaches DatastoreSyncService (src/domain/datastore/datastore_sync_service.ts, interface at :168, the eight-rule markDirty contract 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.json sidecar, _index shards 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, that DataQueryService reads.
  • Serve runs pollers that pull peers' changes: Config, AccessData, RuntimeData.

What changes. The refactor lands on main in phases:

  • Phase 1: a UnitOfWork port 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/pushChanged themselves.
  • 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(), or invalidateType in a finally.
  • No fixed sleeps (use waitFor from @swamp-club/swamp-testing), no wall-clock assertions, and Deno.utime for mtimes.
  • Use @std/path for paths and assertPathEquals for 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) pulls config, invalidates when the pull returns >0 or 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 and hydrateLocalCache.
  • Wiring: src/cli/commands/serve.ts:2747-2792 (config) and :3470-3489 (access and runtime, only if (syncService)).
  • Sync gate (src/serve/sync_gate.ts):
    • withSyncGate (:224) is exclusive for SYNC_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 catalogInvalidate with no real CatalogStore: config_poller_test.ts, access_data_poller_test.ts, runtime_data_poller_test.ts, and sync_gate_test.ts, which includes a resurrection test at :439/:464 for swamp-club#2247.
  • integration/serve_request_harness.ts createServeCtx wires 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

  1. 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 one RuntimeDataPoller tick on S, DataQueryService.query and getLatestRecord on S return P's row.
  2. 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.
  3. Access. P adds a grant and pushes. After one AccessDataPoller tick, S's policy allows the new access. P revokes it, and after one tick S denies it.
  4. Config. P changes a definition and pushes. After one ConfigPoller tick, S returns the new definition. A lockfile change triggers the extension reload path.
  5. Void results. When the fake's pull returns void, pin today's inconsistency: Config invalidates, Access and Runtime do not.
  6. Gate. A poll that overlaps a gated write request waits or is skipped as gatedPull specifies, 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.

  • Tracking issue: swamp-club#2865 (https://swamp-club.com/lab/2865).
  • Reuses the createServeCtx sync parameters added in swamp-club#2860.
  • End-to-end counterpart: swamp-uat#491 (deferred).
02Bog Flow
◉OPEN○TRIAGED○IN PROGRESS○SHIPPED

Open

9/30/2026, 11:13:12 PM

No activity in this phase yet.

03Sludge Pulse

Sign in to post a ripple.