# 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)](https://mswjs.io/) 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 intercept `fetch` / `XHR` / `WebSocket` issued 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`, every `crypto-*`, `i18n-*`, `vault*`, etc.). These MUST keep injecting `fetchFn`/`Clock`/`Rng` and exercising real code paths. - Server-tier packages (`packages/**` declaring `"engines": { "node": ">=20" }` and no `browser` field). These MUST use `@suaveplan/testing-integration` and real testcontainers. - Browser packages where a real local server is straightforward to stand up (e.g. anything that currently uses real `ws` `WebSocketServer` — like `network-transport-websockets` ✅ ). MSW must not regress those tests. - Production code paths — MSW is a test-time-only tool. No `process.env.NODE_ENV` checks 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 (`.handlers.ts` next to `.test.ts`) — no central `mocks/` folder, no `__mocks__` subdir (still banned by `network-discovery-hybrid` precedent). --- ## 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?)` — wraps `setupServer` with a deterministic-clock hook and `AbortSignal` integration 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 the `i18n-shared/testing` pattern). ### 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.mock` remain banned everywhere. --- ## Parity Checklist Not a migration. No parity surface. --- ## Acceptance gate The carve-out is "active" once: 1. `@suaveplan/testing-msw` package exists at `packages/testing/testing-msw/` with 100/100/100/100 coverage on its own surface. 2. `CLAUDE.md §1 #2a` is added. 3. The `openspec/specs/testing/spec.md` capability (or sibling) gains a `## ADDED Requirements` section codifying the rule. 4. 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/node` adapter — out of scope; server tests stay on testcontainers). - Replacing existing real-`ws` / real-`http` test 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.