Skip to content

feat: expand @systeminit/swamp-testing with conformance suites and test factories#963

Merged
stack72 merged 2 commits intomainfrom
expand-testing-package
Mar 30, 2026
Merged

feat: expand @systeminit/swamp-testing with conformance suites and test factories#963
stack72 merged 2 commits intomainfrom
expand-testing-package

Conversation

@stack72
Copy link
Copy Markdown
Contributor

@stack72 stack72 commented Mar 30, 2026

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 extension
  • assertVaultConformance(provider) — behavioral contract: put/get roundtrip, get-missing rejects, put overwrites, list includes keys, getName non-empty
  • assertDatastoreExportConformance(datastore, { validConfigs, invalidConfigs }) — replaces 10+ hand-written structural tests per datastore extension
  • assertLockConformance(lock) — full DistributedLock contract: acquire/release, withLock, inspect, forceRelease
  • assertVerifierConformance(verifier) — validates DatastoreHealthResult shape

Test context factories (for testing code that receives these interfaces)

  • createDriverTestContext() — well-formed ExecutionRequest builder + callback capture for testing ExecutionDriver implementations
  • createReportTestContext() — fake ReportContext (method/model/workflow scopes) with pre-seeded repositories for testing report execute functions
  • createVaultTestContext() — in-memory VaultProvider (also used internally to test conformance suites)
  • createDatastoreTestContext() — in-memory DatastoreProvider (also used internally to test conformance suites)

Validated against real extensions

Tested against all extensions in systeminit/swamp-extensions:

  • @swamp/aws-sm vault — export conformance passes, replaces 5 hand-written tests
  • @swamp/1password vault — export conformance passes, replaces 7 hand-written tests
  • @swamp/azure-kv vault — export conformance passes, replaces 5 hand-written tests
  • @swamp/s3-datastore — export + lock conformance passes, replaces 10 structural + validates full lock contract against mock S3

97 tests total in the testing package (25 existing model + 72 new).

Closes #924

Test Plan

  • deno check passes
  • deno lint passes (949 files)
  • deno fmt --check passes (1047 files)
  • deno run test packages/testing/ — 97 tests pass
  • Compat tests: src/domain/{vaults,datastore,drivers,reports}/testing_package_compat_test.ts
  • aws-sm vault extension tests pass with conformance
  • 1password vault extension tests pass with conformance
  • azure-kv vault extension tests pass with conformance
  • S3 datastore + lock extension tests pass with conformance (15 tests)

🤖 Generated with Claude Code

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]>
@stack72 stack72 changed the title feat: expand @systeminit/swamp-testing for all extension types feat: expand @systeminit/swamp-testing with conformance suites and test factories Mar 30, 2026
Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Blocking Issues

None.

Suggestions

  1. Use import map instead of bare jsr: specifiers in conformance files: packages/testing/vault_conformance.ts and packages/testing/datastore_conformance.ts import from jsr:@std/[email protected] directly, which requires the // deno-lint-ignore-file no-import-prefix suppression. Since deno.json already maps @std/assertjsr:@std/[email protected], these files could use import { ... } from "@std/assert" instead, eliminating the lint ignore entirely.

Notes

  • All new .ts files include the AGPLv3 copyright header ✓
  • No libswamp import boundary violations ✓
  • Test files are co-located with source files (e.g., vault_test_context_test.ts next to vault_test_context.ts) ✓
  • Compile-time compat tests in src/domain/*/testing_package_compat_test.ts correctly 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 ✓

Copy link
Copy Markdown

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial Review

Critical / High

None.

Medium

  1. vault_conformance.ts:225-231assertStringIncludes on joined keys is a weak assertion. The list conformance check joins all keys with commas and uses assertStringIncludes for 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 is assertEquals(keys.includes(testKey1), true, ...). A vault implementation whose list() returns a single key containing a comma and both key names as substrings would pass. Suggested fix: replace assertStringIncludes(keys.join(","), testKey1, ...) with assertEquals(keys.includes(testKey1), true, ...).

  2. 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: add assertEquals(await provider.get(testKey1), "conformance-value-updated") after the key2 roundtrip.

  3. datastore_test_context.ts:89 — Shared lockHeld boolean across all lock instances. A single let lockHeld = false is shared across all locks from the same createDatastoreTestContext(). 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, now isLockHeld() reports false even though lock1 is held. The isLockHeld() helper (line 241) returns this single boolean. Suggested fix: use a Map<string, boolean> keyed by lockKey, or track held state per lock instance.

  4. datastore_conformance.ts:291-305forceRelease conformance silently skipped when nonce is absent. The if (info && info.nonce) guard means a lock implementation that never populates nonce in its LockInfo will skip the entire forceRelease-with-correct-nonce block. The conformance suite passes without ever verifying the primary forceRelease path. Suggested fix: assert that info.nonce is a non-empty string (making it a de-facto required field for conformance), or restructure to test forceRelease even when nonce is absent.

  5. datastore_conformance.ts:315-326 — Wrong-nonce forceRelease doesn't verify lock is still held. After forceRelease("wrong-nonce-value") returns false, the test doesn't call inspect() to confirm the lock remains held. A buggy implementation could return false but release the lock as a side effect. Suggested fix: add const stillHeld = await lock.inspect(); assertExists(stillHeld, "lock must remain held after wrong-nonce forceRelease").

  6. report_test_context.ts:190getContent returns raw Uint8Array reference without cloning. findByName (line 175), findAllForModel (line 199), and findAllGlobal (line 207) all use structuredClone. But getContent returns match?.content directly. A caller mutating the returned buffer corrupts the internal state. Suggested fix: return Promise.resolve(match?.content ? new Uint8Array(match.content) : null).

  7. drivers/testing_package_compat_test.ts:107-113 — Driver compat test does not verify execute method. The ExecutionDriver check verifies type, initialize, and shutdown but omits execute — the most important method on the interface. If the execute signature diverges between canonical and testing types, this test won't catch it. Suggested fix: add const _execute: CanonicalExecutionDriver["execute"] = driver.execute to the check function.

  8. drivers/testing_package_compat_test.ts:95-103 — Driver compat test does not verify outputs field on ExecutionResult. The outputs: DriverOutput[] field is omitted from the compat check. Suggested fix: add const _outputs: CanonicalExecutionResult["outputs"] = result.outputs.

  9. datastore/testing_package_compat_test.ts:42-53 — Datastore compat test does not verify withLock. The DistributedLock check verifies acquire, release, inspect, forceRelease but omits withLock — a required method on the canonical interface. Suggested fix: add const _withLock = lock.withLock(async () => {}) with appropriate type annotation.

Low

  1. vault_conformance.ts:252-264 — Cleanup puts empty string rather than deleting. Since VaultProvider has no delete(), cleanup uses put(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.

  2. report_test_context.ts:169-175findByName without version returns first match, not latest. When version is undefined, Array.find returns 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.

  3. datastore_test_context.ts:137-144withLock uses synchronous throw vs. acquire's Promise.reject. When lockAcquireFails is true, acquire() returns Promise.reject(...) (line 119) but withLock does a synchronous throw (line 144). Both are caught by assertRejects, but the inconsistency could matter for callers distinguishing sync/async errors.

  4. vault_conformance.ts:77 / datastore_conformance.ts:74 — Type regex makes @ prefix optional but error message says @collective/name. The regex ^@?[a-z]... accepts foo/bar without @. 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.

@stack72 stack72 merged commit d2b4ec7 into main Mar 30, 2026
10 checks passed
@stack72 stack72 deleted the expand-testing-package branch March 30, 2026 22:52
stack72 added a commit that referenced this pull request Mar 30, 2026
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]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Expand @systeminit/swamp-testing to cover datastores, vaults, drivers, and reports

1 participant