7.7 KiB
OpenSpec Change: testing-msw-carve-out
- Change ID: testing-msw-carve-out
- Type: policy + tooling carve-out
- Status: PROPOSED (deferred — activate when first React/R3F-tier consumer needs it)
- Date: 2026-05-11
- Affects:
CLAUDE.md §1 #2— "NEVER use mocks" rule@suaveplan/testing-dom✅ — gains an MSW adapter subpath- Future
@suaveplan/testing-msw(NEW package, optional install)
- Related:
network-transport-family(2026-05-11) — Wave 7 (R3F + React demos) is the activation trigger
Summary
Carve a narrow exception into the repo-wide "real services only" rule (CLAUDE.md §1 #2) for browser-tier React / R3F packages whose test surface is fetch / XHR / WebSocket. Those tests may use MSW (Mock Service Worker) at the request boundary. Server-tier, universal-tier, and any package where booting a real local service is feasible MUST continue to use real services.
This is a policy proposal, not an implementation. It's scoped narrowly, defines hard boundaries, and ships a small @suaveplan/testing-msw adapter so consumers get a uniform integration that matches the rest of the testing-infra family.
Why this is needed (eventually)
The network-family work scheduled for Wave 7 — r3f-networking, react-network-libp2p-demo, and downstream React dashboards — will run tests in a browser environment (jsdom / happy-dom / Playwright) where the existing genesis testing tools fall short for HTTP-layer assertions:
| Existing tool | Works for | Fails for |
|---|---|---|
Injected fetchFn callback |
Universal-tier libs (@suaveplan/hibp, oauth-client, webhook-client, i18n-backend-http) |
React components that hard-code fetch(...) and can't accept an injection without API churn |
Real ws WebSocketServer (network-transport-websockets pattern) |
Node/server tests | Browser-environment tests where the server-side socket would need a sidecar process |
@suaveplan/testing-integration (testcontainers) |
Server-process tests with a real backend | jsdom/Playwright suites where the cost of a sidecar is disproportionate |
| Storybook interaction tests | UI workflows | Anything that needs a 503 + Retry-After: 600 reproduction |
MSW intercepts at the request-handler layer — the same JS that issues fetch() still runs through Browser networking stack and reaches a handler that returns a real Response. It's a closer simulation than vi.mock("axios") and doesn't touch module loading. But it IS a fake response, and the spirit of §1 #2 (test against real implementations) bends here. So the carve-out has to be explicit and bounded.
What the carve-out allows
Allowed (with this proposal in effect):
- Browser-tier React or R3F packages (
packages/react/**,packages/r3f/**) MAY use MSW handlers to interceptfetch/XHR/WebSocketissued by components under test. - Storybook interaction stories that need to demo a specific HTTP response (
MockedHandlers). - Playwright E2E tests that need to reproduce a pathological server state (slow body, 5xx with specific headers, malformed
Content-Type) where coding that into a real server would take more lines than the MSW handler.
Forbidden (rule still applies — no MSW):
- Universal-tier packages (
packages/network/network-bootstrap,service-discovery, everycrypto-*,i18n-*,vault*, etc.). These MUST keep injectingfetchFn/Clock/Rngand exercising real code paths. - Server-tier packages (
packages/**declaring"engines": { "node": ">=20" }and nobrowserfield). These MUST use@suaveplan/testing-integrationand real testcontainers. - Browser packages where a real local server is straightforward to stand up (e.g. anything that currently uses real
wsWebSocketServer— likenetwork-transport-websockets✅ ). MSW must not regress those tests. - Production code paths — MSW is a test-time-only tool. No
process.env.NODE_ENVchecks at runtime. vi.mock("module")/jest.mock("module")— still banned. MSW is request-layer interception, not module mocking, and the distinction is load-bearing.
Always required (even when MSW is in play):
- Conformance test against the real protocol at least once per public surface, so an MSW-only regression can't silently land. E.g. a React component that uses MSW for happy-path / error-path simulation MUST also have at least one Playwright E2E run against a real server.
- MSW handler files MUST be co-located with tests (
<feature>.handlers.tsnext to<feature>.test.ts) — no centralmocks/folder, no__mocks__subdir (still banned bynetwork-discovery-hybridprecedent).
What ships with this proposal
Two artifacts. Neither lands until a real consumer needs them.
1. @suaveplan/testing-msw (new package, layer-0 testing-infra)
Mirrors the existing testing-infra family (@suaveplan/testing, testing-dom, testing-e2e, testing-integration). Zero @suaveplan/* runtime deps; depends on msw and @suaveplan/types (dev-only for Clock-style fake hooks).
Surface:
createMswServer(handlers, options?)— wrapssetupServerwith a deterministic-clock hook andAbortSignalintegration so tests can fail fast on hung handlers.createMswWorker(handlers, options?)— browser-tier equivalent for Storybook + Playwright.expectRequest(server, predicate, opts?)— async helper that waits for a matching request with timeout + abort.requestRecorder()— captures every intercepted request for ordered-assertion tests.runMswConformance(label, makeFactory)— shared conformance harness (matches thei18n-shared/testingpattern).
2. CLAUDE.md §1 #2a — the boundary text
A new sub-rule added immediately under #2:
#2a — MSW carve-out. Browser-tier React/R3F packages MAY use MSW for boundary-level HTTP/WebSocket interception in tests, but ONLY through
@suaveplan/testing-msw. Server-tier and universal-tier packages MUST NOT use MSW. Every MSW-tested surface MUST also have at least one conformance test against a real protocol implementation.vi.mock/jest.mockremain banned everywhere.
Parity Checklist
Not a migration. No parity surface.
Acceptance gate
The carve-out is "active" once:
@suaveplan/testing-mswpackage exists atpackages/testing/testing-msw/with 100/100/100/100 coverage on its own surface.CLAUDE.md §1 #2ais added.- The
openspec/specs/testing/spec.mdcapability (or sibling) gains a## ADDED Requirementssection codifying the rule. - At least one consumer ships under the carve-out so the harness is exercised end-to-end. Likely first consumer:
react-network-libp2p-demo(Wave 7) or a future React HIBP-status indicator.
Out of scope
- A blanket relaxation of the no-mocks rule.
- Server-side MSW (the
msw/nodeadapter — out of scope; server tests stay on testcontainers). - Replacing existing real-
ws/ real-httptest setups in already-shipped packages. They keep working as-is. - Recording / replaying production traffic — separate proposal if ever needed.
Decision log (for future maintainers)
We considered three options:
| Option | Verdict |
|---|---|
| Adopt MSW broadly across genesis | Rejected — conflicts with §1 #2 spirit; weakens universal-tier tests that already pass with injection. |
| Forbid MSW entirely | Rejected — Wave 7 React/R3F tests can't realistically stand up sidecar servers in jsdom/Playwright for every HTTP scenario. |
| Narrow carve-out at the React/R3F tier, conformance test required | Chosen. Keeps universal/server tests strict; gives React tier a pragmatic tool; conformance requirement prevents drift. |
The conformance requirement is the load-bearing constraint. Without it, the carve-out becomes a backdoor.