test(extensions): cross-process concurrency stress for W2 lifecycle services (swamp-club#254)#1320
test(extensions): cross-process concurrency stress for W2 lifecycle services (swamp-club#254)#1320
Conversation
…ervices (swamp-club#254)
Adds integration/lifecycle_concurrent_stress_test.ts mirroring the
swamp-club#234 race-regression pattern at integration/data_delete_test.ts.
50 iterations × 4 concurrent CLI subprocesses (extension pull alpha,
pull beta, rm alpha, update) verify the W2 services'
per-extension-atomicity claim under cross-process contention. Five
invariants per iteration: catalog↔lockfile bijection (Indexed rows
match lockfile entries by name+version), no DuplicateTypeError leakage
across distinct fixture types (@stress/alpha-model vs
@stress/beta-model), no lockfile-retry-budget exhaustion or SQLite-busy
contention as benign outcomes, lockfile JSON well-formedness, and no
orphan files under .swamp/pulled-extensions/@stress/...
The harness ships a co-located Deno.serve fixture registry implementing
the four ExtensionApiClient endpoints the install path requires
(/{name}, /{name}/latest, /{name}@{version}/download, /checksum) so
the test runs offline and deterministically. Fixture extensions
deliberately declare distinct model type identifiers — sharing them
would surface a real cross-extension DuplicateTypeError on every
concurrent install of alpha+beta and the test would mis-categorise it
as benign.
Documents the test as the load-bearing concurrency-claim verification
in design/extension.md under "Crash-state recovery", with inline
swamp-club#254 and test-path references.
Verified: 3× back-to-back local green (2m58s, 2m57s, 2m57s) and full
test suite green (5527 passed, 0 failed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…dows CI Windows CI surfaced invariant (iv) firing spuriously: the lockfile records `files[]` with native backslash separators on Windows, while the on-disk walker emitted forward slashes, so set comparison missed matches. Normalise both sides to forward slashes via a new `toForwardSlash` helper before comparison, per CLAUDE.md's path- portability rule. Lockfile-to-FS direction (which uses `join` + `Deno.stat`) was already separator-agnostic and is unchanged. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Windows CI iteration 36 surfaced the W2 documented "Install partially applied — files extracted but the catalog write failed (database is locked)" recovery path under 4-way concurrency. Per design/extension.md "Crash-state recovery", this is the intended behavior: SQLite ROLLBACK rolls the catalog back, FS+lockfile are not auto-rolled-back, and the next pull/update's diff-save reconciles. Updates to invariants: - Drop "SQLITE_BUSY" / "database is locked" from REAL_FAILURE_PATTERNS. Lockfile-retry exhaustion stays a real failure (the budget exists to absorb expected contention; exhausting it means real damage). - Invariant (i) tolerates the documented "lockfile entry without catalog row" transient: when a child this iteration emitted "Install partially applied", skip the lockfile→catalog direction for the contended @stress/* names. The catalog→lockfile direction stays strict — an unmatched catalog row would be a real rollback bug. - After the loop, run a sequential `extension update` reconcile pass and assert the bijection invariants strictly with no tolerated transients. This pins the eventual-consistency contract. Updates the header comment to reflect the now-tolerated SQLite contention path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
Well-written cross-process concurrency stress test. The code is thorough, well-documented, and follows project conventions consistently.
Blocking Issues
None.
DDD Assessment
The test correctly exercises the W2 lifecycle domain services (InstallExtensionService / RemoveExtensionService / UpgradeExtensionService) through the CLI subprocess boundary rather than bypassing domain services directly. Invariant verification reads from the infrastructure layer (ExtensionCatalogStore, lockfile, filesystem) — appropriate for a test asserting cross-layer aggregate consistency. No domain logic violations or anti-patterns.
Conventions Verified
- License header: Present ✓
- No
anytypes: Usesas unknown as Xfor two Deno type-bridge casts, no rawany✓ - No fire-and-forget promises: All promises awaited ✓
@std/pathfor path ops: Usesdirname,fromFileUrl,jointhroughout ✓- Windows compatibility:
toForwardSlashnormalizer for path comparisons,.catch(() => {})on temp dir cleanup ✓ - Import boundary: No libswamp imports — imports from
src/infrastructure/directly, consistent with all other integration tests ✓ - Test naming: Descriptive name with issue reference ✓
deno fmt/deno lint/deno check: PR states all clean ✓
Suggestions
-
The two
as unknown as BufferSource/as unknown as BodyInitdouble-casts (lines 220, 346) are slightly inelegant but pragmatic for Deno's type system gaps — not worth changing. -
The ~50-line file header comment is long but justified here: the calibration note honestly disclaiming N=50's statistical power, and the architecture note explaining why SQLITE_BUSY is tolerated, are both genuinely valuable for future maintainers of a test this nuanced.
-
The design doc addition at
design/extension.md:1010-1019sits naturally in the "Crash-state recovery" section and pins the test as the load-bearing verification — good traceability.
Clean PR. The five invariants are well-defined, the fixture registry is minimal but complete, and the Windows/cross-platform handling is thorough.
Summary
Closes swamp-club#254. Post-merge follow-up to PR #1310 (W2 — lifecycle services).
Adds
integration/lifecycle_concurrent_stress_test.tsthat mirrors the swamp-club#234 race-regression pattern atintegration/data_delete_test.ts. 50 iterations × 4 concurrent CLI subprocesses (extension pull @stress/alpha,pull @stress/beta,rm @stress/alpha,update) verify the W2 services' per-extension-atomicity claim under cross-process contention.Five invariants per iteration:
_extension_catalog.dbrow for@stress/*matches a lockfile entry by name+version, and vice versa.DuplicateTypeErrorleakage across distinct fixture types (@stress/alpha-modelvs@stress/beta-model). Fixtures deliberately declare distinct type ids — sharing them would surface a real cross-extension collision on every concurrent install of alpha+beta and the test would mis-categorise it as benign.Could not acquire lock on upstream_extensions.json) or SQLite-busy contention (SQLITE_BUSY,database is locked). These are real failures, not benign race outcomes.extensions/models/upstream_extensions.jsonis well-formed JSON after every iteration..swamp/pulled-extensions/@stress/...— every file is referenced by some lockfile entry'sfiles[]. Empty leftover directories are tolerated;extension rmonly prunes the deepest empty directory, matchingintegration/extension_rm_test.ts's file-level absence assertion.The harness ships a co-located
Deno.servefixture registry implementing the fourExtensionApiClientendpoints the install path requires (/{name},/{name}/latest,/{name}@{version}/download,/checksum) so the test runs offline and deterministically. Inline comments at each fake endpoint reference the correspondingextension_api_client.tscallsite so future API drift fails this test loudly rather than silently shipping bad production code.design/extension.md"Crash-state recovery" section gains one paragraph pinning this test as the load-bearing concurrency-claim verification, with inlineswamp-club#254and test-path references.Test Plan
deno checkcleandeno lintcleandeno fmt --checkcleandeno run test) green: 5527 passed / 0 failed / 28 ignored (3m15s)deno run compilebuilds binaryNotes for reviewers
RACE_ITERATIONS = 50,CONCURRENCY_PER_ITERATION = 4) are pinned with code-comments referencing swamp-club#254 — please don't shrink them silently.data_delete_test.ts(where it was calibrated against an evidence-base) but this test verifies the absence of a race in code claimed safe — there's no calibration point. The header comment is honest about this; CI's natural soak across the merge train is the longer-tail coverage.🤖 Generated with Claude Code