feat: expand @systeminit/swamp-testing with conformance suites and test factories#963
feat: expand @systeminit/swamp-testing with conformance suites and test factories#963
Conversation
Add test context factories for vaults, datastores, execution drivers, and reports — giving extension authors the same unit testing experience that model authors already have via createModelTestContext(). New factories: - createVaultTestContext: in-memory VaultProvider with operation recording - createDatastoreTestContext: in-memory DatastoreProvider with fake locking, health checks, and optional sync service - createDriverTestContext: well-formed ExecutionRequest builder with callback capture for testing driver implementations - createReportTestContext: fake ReportContext (method/model/workflow scopes) with pre-seeded data and definition repositories Each factory includes: - Extension-author-facing type definitions (mirroring canonical types) - Comprehensive test suite - Compile-time compat test in src/domain/*/testing_package_compat_test.ts - Skill documentation (references/testing.md) Closes #924 Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Add assertVaultExportConformance() and assertVaultConformance() for testing vault provider exports and behavioral contracts (put/get roundtrip, get-missing rejects, list includes stored keys). Add assertDatastoreExportConformance(), assertLockConformance(), and assertVerifierConformance() for testing datastore provider exports and DistributedLock/DatastoreVerifier behavioral contracts. Validated against real extensions in systeminit/swamp-extensions: - aws-sm vault: export conformance replaces 5 hand-written tests - 1password vault: export conformance replaces 7 hand-written tests - azure-kv vault: export conformance replaces 5 hand-written tests - S3 datastore: export conformance replaces 10 hand-written tests - S3 lock: assertLockConformance validates full DistributedLock contract Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
There was a problem hiding this comment.
Code Review
Blocking Issues
None.
Suggestions
- Use import map instead of bare
jsr:specifiers in conformance files:packages/testing/vault_conformance.tsandpackages/testing/datastore_conformance.tsimport fromjsr:@std/[email protected]directly, which requires the// deno-lint-ignore-file no-import-prefixsuppression. Sincedeno.jsonalready maps@std/assert→jsr:@std/[email protected], these files could useimport { ... } from "@std/assert"instead, eliminating the lint ignore entirely.
Notes
- All new
.tsfiles include the AGPLv3 copyright header ✓ - No libswamp import boundary violations ✓
- Test files are co-located with source files (e.g.,
vault_test_context_test.tsnext tovault_test_context.ts) ✓ - Compile-time compat tests in
src/domain/*/testing_package_compat_test.tscorrectly verify structural compatibility with canonical types ✓ - Types follow DDD principles: testing interfaces mirror domain Value Objects/interfaces, conformance suites are stateless service-like utilities, repository fakes abstract persistence properly ✓
- Version bump 0.1.0 → 0.2.0 is appropriate for the scope of new exports ✓
- No security concerns ✓
There was a problem hiding this comment.
Adversarial Review
Critical / High
None.
Medium
-
vault_conformance.ts:225-231—assertStringIncludeson joined keys is a weak assertion. The list conformance check joins all keys with commas and usesassertStringIncludesfor membership. This is a substring match, not element-membership. While the UUID-based key prefix makes false positives very unlikely in practice, the correct assertion isassertEquals(keys.includes(testKey1), true, ...). A vault implementation whoselist()returns a single key containing a comma and both key names as substrings would pass. Suggested fix: replaceassertStringIncludes(keys.join(","), testKey1, ...)withassertEquals(keys.includes(testKey1), true, ...). -
vault_conformance.ts:199-220— Conformance suite never re-reads testKey1 after writing testKey2. A broken vault using a single global variable for all keys (last-write-wins regardless of key) would pass every behavioral assertion. The suite puts key1, verifies it, overwrites key1, verifies overwrite, writes key2, verifies key2 — but never re-reads key1 to confirm it wasn't clobbered. Suggested fix: addassertEquals(await provider.get(testKey1), "conformance-value-updated")after the key2 roundtrip. -
datastore_test_context.ts:89— SharedlockHeldboolean across all lock instances. A singlelet lockHeld = falseis shared across all locks from the samecreateDatastoreTestContext(). If a test creates two locks with different keys, acquiring one and releasing the other corrupts the state:lock1.acquire()→lockHeld=true,lock2.release()→lockHeld=false, nowisLockHeld()reports false even though lock1 is held. TheisLockHeld()helper (line 241) returns this single boolean. Suggested fix: use aMap<string, boolean>keyed by lockKey, or track held state per lock instance. -
datastore_conformance.ts:291-305—forceReleaseconformance silently skipped whennonceis absent. Theif (info && info.nonce)guard means a lock implementation that never populatesnoncein itsLockInfowill skip the entireforceRelease-with-correct-nonce block. The conformance suite passes without ever verifying the primaryforceReleasepath. Suggested fix: assert thatinfo.nonceis a non-empty string (making it a de-facto required field for conformance), or restructure to testforceReleaseeven whennonceis absent. -
datastore_conformance.ts:315-326— Wrong-nonceforceReleasedoesn't verify lock is still held. AfterforceRelease("wrong-nonce-value")returnsfalse, the test doesn't callinspect()to confirm the lock remains held. A buggy implementation could returnfalsebut release the lock as a side effect. Suggested fix: addconst stillHeld = await lock.inspect(); assertExists(stillHeld, "lock must remain held after wrong-nonce forceRelease"). -
report_test_context.ts:190—getContentreturns rawUint8Arrayreference without cloning.findByName(line 175),findAllForModel(line 199), andfindAllGlobal(line 207) all usestructuredClone. ButgetContentreturnsmatch?.contentdirectly. A caller mutating the returned buffer corrupts the internal state. Suggested fix:return Promise.resolve(match?.content ? new Uint8Array(match.content) : null). -
drivers/testing_package_compat_test.ts:107-113— Driver compat test does not verifyexecutemethod. TheExecutionDrivercheck verifiestype,initialize, andshutdownbut omitsexecute— the most important method on the interface. If theexecutesignature diverges between canonical and testing types, this test won't catch it. Suggested fix: addconst _execute: CanonicalExecutionDriver["execute"] = driver.executeto the check function. -
drivers/testing_package_compat_test.ts:95-103— Driver compat test does not verifyoutputsfield onExecutionResult. Theoutputs: DriverOutput[]field is omitted from the compat check. Suggested fix: addconst _outputs: CanonicalExecutionResult["outputs"] = result.outputs. -
datastore/testing_package_compat_test.ts:42-53— Datastore compat test does not verifywithLock. TheDistributedLockcheck verifiesacquire,release,inspect,forceReleasebut omitswithLock— a required method on the canonical interface. Suggested fix: addconst _withLock = lock.withLock(async () => {})with appropriate type annotation.
Low
-
vault_conformance.ts:252-264— Cleanup puts empty string rather than deleting. SinceVaultProviderhas nodelete(), cleanup usesput(key, ""). This is acknowledged in a comment, and keys are namespaced with random prefixes, so this is a known limitation rather than a bug. However, repeated conformance runs against real vault backends (AWS SM, etc.) will accumulate orphaned secrets. -
report_test_context.ts:169-175—findByNamewithout version returns first match, not latest. Whenversionis undefined,Array.findreturns the first element in insertion order. If the canonical implementation returns the latest version by default, this fake has different semantics. Unlikely to cause issues in practice since test data is hand-seeded. -
datastore_test_context.ts:137-144—withLockuses synchronous throw vs.acquire'sPromise.reject. WhenlockAcquireFailsis true,acquire()returnsPromise.reject(...)(line 119) butwithLockdoes a synchronousthrow(line 144). Both are caught byassertRejects, but the inconsistency could matter for callers distinguishing sync/async errors. -
vault_conformance.ts:77/datastore_conformance.ts:74— Type regex makes@prefix optional but error message says@collective/name. The regex^@?[a-z]...acceptsfoo/barwithout@. Either the regex should require@or the error message should document that it's optional.
Verdict
PASS — No critical or high severity issues. The medium findings are real gaps in conformance coverage and compat test completeness, but none would cause data loss or security issues. Most are improvements that would strengthen the testing framework. The assertStringIncludes on joined keys (#1) and the shared lockHeld state (#3) are the most likely to cause real confusion for extension authors, and are worth addressing before or shortly after merge.
Add two runtime mock primitives that intercept at the boundary between extension code and the outside world: - withMockedFetch(handler|responses[], fn) — replaces globalThis.fetch for the duration of the callback. Supports sequential responses (array) or dynamic routing (handler function). Records all calls for inspection. - withMockedCommand(handler|outputs[], fn) — replaces Deno.Command for the duration of the callback. Same two modes. Records command/args. These test the exact same code path as production — no _client injection or alternative code paths needed. Extension authors mock at the network or process boundary, not the SDK layer. Also fixes review issues from #963: - vault_conformance: use keys.includes() instead of assertStringIncludes - vault_conformance: re-read key1 after writing key2 to detect clobbering - datastore_test_context: use per-lock Map instead of shared lockHeld bool - datastore_conformance: require nonce in LockInfo, verify lock held after wrong-nonce forceRelease - report_test_context: clone Uint8Array in getContent - driver compat test: verify execute method and outputs field - datastore compat test: verify withLock method Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Summary
Expands
@systeminit/swamp-testing(v0.1.0 → v0.2.0) from models-only to all extension types, with two categories of tools:Conformance suites (the main value)
Extension authors call one function to validate their implementation satisfies the contract:
assertVaultExportConformance(vault, { validConfigs, invalidConfigs })— replaces 5-7 hand-written structural tests per vault extensionassertVaultConformance(provider)— behavioral contract: put/get roundtrip, get-missing rejects, put overwrites, list includes keys, getName non-emptyassertDatastoreExportConformance(datastore, { validConfigs, invalidConfigs })— replaces 10+ hand-written structural tests per datastore extensionassertLockConformance(lock)— full DistributedLock contract: acquire/release, withLock, inspect, forceReleaseassertVerifierConformance(verifier)— validates DatastoreHealthResult shapeTest context factories (for testing code that receives these interfaces)
createDriverTestContext()— well-formedExecutionRequestbuilder + callback capture for testingExecutionDriverimplementationscreateReportTestContext()— fakeReportContext(method/model/workflow scopes) with pre-seeded repositories for testing reportexecutefunctionscreateVaultTestContext()— in-memoryVaultProvider(also used internally to test conformance suites)createDatastoreTestContext()— in-memoryDatastoreProvider(also used internally to test conformance suites)Validated against real extensions
Tested against all extensions in
systeminit/swamp-extensions:@swamp/aws-smvault — export conformance passes, replaces 5 hand-written tests@swamp/1passwordvault — export conformance passes, replaces 7 hand-written tests@swamp/azure-kvvault — export conformance passes, replaces 5 hand-written tests@swamp/s3-datastore— export + lock conformance passes, replaces 10 structural + validates full lock contract against mock S397 tests total in the testing package (25 existing model + 72 new).
Closes #924
Test Plan
deno checkpassesdeno lintpasses (949 files)deno fmt --checkpasses (1047 files)deno run test packages/testing/— 97 tests passsrc/domain/{vaults,datastore,drivers,reports}/testing_package_compat_test.ts🤖 Generated with Claude Code