Relationships
↔ sibling #2856#2860 Tests: characterise marks and pushes for every libswamp write use case via CLI and serve compositions
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
Pin, for every libswamp use case that writes to the datastore, which paths get marked and which pushes happen, through both the CLI and serve. Phase 2 moves unit-of-work ownership into these use cases, and these tests prove the signals don't change. This also fills a gap in the ddd skill's rule that application services have integration tests with real dependencies. The end-to-end counterpart is swamp-uat #492.
Current state
- Use-case pattern: each use case has an
XxxDepsinterface, acreateXxxDeps(repoDir, …, markDirty?)factory, and anasync function*taking(ctx, deps, input). Example:src/libswamp/data/delete.ts:84/109/181. - Only the datastore use cases touch sync themselves:
src/libswamp/datastores/sync.ts,setup.ts,migrate_index.tsandnamespace_migrate.ts.- Every other write use case relies on repository hooks, and the caller pushes, through one of the four flush paths listed in the background.
- libswamp unit tests mostly use inline mock deps.
- Sync behaviour is asserted one layer up, in serve handler tests with real repos and a mock sync service:
src/serve/handlers/data_handlers_test.ts:335-504,model_handlers_test.ts:277,access_handlers_test.ts:299/513,admin_handlers_test.ts:446/525andvault_handlers_test.ts:178/205. - Existing integration tests with real repos, which mostly don't check sync:
integration/mark_dirty_hook_sync_test.ts(model delete; checks sync)data_prune_test.tsrun_snapshot_cleanup_test.tsworkflow_datastore_routing_test.tsevaluated_cache_provenance_test.tsgiga_swamp_namespace_migrate_test.tsvault_rename_secrets_test.tsmodel_create_sensitive_test.tsserve_approve_policy_test.tsserve_cancel_suspended_test.ts
- No integration test at all for:
- data delete / rename / gc;
- model edit;
- workflow create / edit;
- vault create / edit / put / delete;
- extension pull / rm / install / update;
- worker prune;
- access token revoke / rotate.
- Known behaviour to pin:
- CLI
workflow approve(src/cli/commands/workflow_approve.ts:134),workflow reject(workflow_reject.ts:122) andworkflow cancel(workflow_cancel.ts:384) never push, while their serve equivalents do. - The
acquireVaultSyncflush is a no-op (repo_context.ts:2034-2039). - The CLI sends bare
syncService.markDirty()calls inaccess_grant.ts:319,access_group.ts:212,access_token_mint.ts:272,worker_token_create.ts:243,worker_token_revoke.ts:168,worker_prune.ts:287,model_create.ts:153anddatastore_config_migrate.ts:158. Serve bans bare marks. YamlVaultConfigRepositoryandLockfileRepositorytake no hook; callers mark by path.
- CLI
Needs
The recording sync service and in-memory remote from the fake-remote issue. Also integration/serve_request_harness.ts, whose createServeCtx (:158-192) currently wires no sync service or gate. Add optional syncService / syncGate parameters.
Test
integration/usecase_sync_characterization_test.ts. Table-driven, one row per use case:
- data delete, rename, gc, prune;
- model create, edit, delete, evaluate;
- workflow create, edit, delete, evaluate, approve, reject, cancel;
- vault create, edit, put, delete, migrate;
- extension install, rm;
- access grant create / revoke, group create;
- token mint / revoke, worker prune;
- datastore config migrate.
Each row is run twice:
- CLI composition: build deps the way the CLI command does, through
requireInitializedRepowith a registered per-run test datastore type, then run the command's flush. - Serve composition: through the serve handler with
createServeCtxgiven the recording sync service and a realSyncGate.
Record for each run:
- the set of marked relPaths, normalised, with any bare marks noted;
- which flush path ran, and whether a push happened;
- whether a second repo on the same in-memory remote sees the change after a pull.
Compare against a pinned expectation table in the test file. Differences between CLI and serve (approve/reject/cancel, bare versus per-path marks) are pinned as today's behaviour, with a comment.
Done when
Every use case listed has a row for CLI and serve, and the pinned table reflects today's behaviour, including the known gaps above.
Dependencies
Blocked by: swamp-club#2854
Blocks: swamp-club#2863
Full dependency graph and waves: swamp-club#2865.
Related
- Tracking issue: swamp-club#2865 (https://swamp-club.com/lab/2865).
- End-to-end counterparts: swamp-uat#492 and swamp-uat#490.
Shipped
Click a lifecycle step above to view its details.
Sign in to post a ripple.