Files
2026-05-27 23:29:53 +01:00

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 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 (<feature>.handlers.ts next to <feature>.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.