Initial Commit
This commit is contained in:
121
openspec/changes/testing-msw-carve-out/proposal.md
Normal file
121
openspec/changes/testing-msw-carve-out/proposal.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# 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 (`<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.
|
||||
Reference in New Issue
Block a user