feat: add withMockedFetch and withMockedCommand to swamp-testing#968
feat: add withMockedFetch and withMockedCommand to swamp-testing#968
Conversation
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]>
There was a problem hiding this comment.
Code Review
Clean, well-structured PR. The mock primitives are well-designed with proper cleanup semantics, good error messages, and thorough test coverage. The conformance suite fixes are solid improvements.
Blocking Issues
None.
Suggestions
-
mock_fetch.ts:100—Requestconstructor consumesinitwheninputis already aRequest: Wheninputis aRequestandinitis also provided (e.g.fetch(existingRequest, { headers: ... })), the current code ignoresinit. This is a narrow edge case since theRequest(input, init)constructor already merges them correctly, but worth noting thatinput instanceof Request ? input : new Request(input, init)skips theinitmerge for existingRequestobjects. Considernew Request(input, init)unconditionally, which the spec handles correctly for both cases. -
mock_command.ts:113—options?.args as string[]cast: Theas string[]cast on options args bypasses type checking. Since theCommandOptionstype already declaresargs?: string[], this cast is safe in practice, but the[key: string]: unknownindex signature on the options type is what forces it. Minor — no action needed. -
DDD perspective: The mock primitives sit cleanly in the testing infrastructure layer and don't leak into domain code. The
with*pattern (resource acquisition + guaranteed cleanup) is the right choice for global state replacement. The conformance suite changes correctly tighten the behavioral contracts for locks and vaults, which strengthens the domain boundary enforcement for extension implementations.
There was a problem hiding this comment.
Adversarial Review
Critical / High
None found.
Medium
-
packages/testing/mock_fetch.ts:100—initsilently dropped wheninputis aRequestThe mock does:
const request = input instanceof Request ? input : new Request(input, init);
The real
fetch(request, init)API mergesinitoverrides (headers, method, signal, etc.) into the request. The mock ignoresinitentirely wheninputis aRequest, so tests usingfetch(existingRequest, { headers: { "X-Extra": "val" } })will silently lose the override. The capturedCapturedFetchCall.headerswill also be wrong.Breaking example: An extension does
fetch(req, { signal })and the test asserts on captured headers from the init — assertion passes in mock but the header was never on the request.Suggested fix:
const request = input instanceof Request ? new Request(input, init) : new Request(input, init);
(Or simply:
const request = new Request(input, init);— theRequestconstructor already handles both overloads.) -
Both mocks are unsafe under concurrent test execution
withMockedFetchandwithMockedCommandreplace globals (globalThis.fetch,Deno.Command). Deno runs tests concurrently by default. If two tests usewithMockedFetchsimultaneously, Test B captures Test A's mock asoriginalFetch, then restores it after Test A already cleaned up — leaving a stale mock permanently installed.This is a well-known limitation of global-replacement mocking and may be acceptable for a testing library, but it should be documented (e.g., "use
Deno.test(..., { sanitizeOps: false })or run tests with--jobs=1when multiple test files use these mocks").
Low
-
packages/testing/mock_command.ts:158—outputSync()throws but callers may expect sync outputThe mock throws on
outputSync(). If extension code under test usesoutputSync()(reasonable for simple CLI wrappers), the mock will fail with a confusing error rather than returning mock data. This limits the mock's coverage but is clearly documented in the error message, so it's not a real bug — just a limitation to be aware of.
Verdict
PASS — The code is well-structured with proper cleanup in finally blocks, good error messages, and correct sequential/handler dispatch. The conformance test fixes (vault keys.includes(), datastore per-lock map, report Uint8Array clone, lock nonce assertions) are all genuine improvements. The fetch(Request, init) issue (Medium #1) is worth fixing but is not a blocker since it only affects an uncommon calling pattern.
Summary
Add two runtime mock primitives that test extension code on the exact same code path as production — no injection or alternative paths needed:
withMockedFetch(handler|responses[], fn)— replacesglobalThis.fetchfor the callback's duration. Covers any HTTP-based SDK (AWS, Azure, GCP, REST APIs).withMockedCommand(handler|outputs[], fn)— replacesDeno.Commandfor the callback's duration. Covers any CLI-tool-wrapping extension (1password'sop, etc.).Both support two modes:
All calls are recorded for inspection after the test.
Example: testing an AWS vault extension
Also fixes review issues from #963
vault_conformance: usekeys.includes()instead ofassertStringIncludeson joined keysvault_conformance: re-read key1 after writing key2 to detect single-variable clobberingdatastore_test_context: use per-lockMapinstead of sharedlockHeldbooleandatastore_conformance: requirenonceinLockInfo, verify lock still held after wrong-nonceforceReleasereport_test_context: cloneUint8ArrayingetContentto prevent mutationexecutemethod andoutputsfieldwithLockmethod115 tests pass (97 existing + 18 new mock tests).
Test Plan
deno checkpassesdeno lintpassesdeno fmt --checkpassesdeno run test packages/testing/— 115 tests pass🤖 Generated with Claude Code