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.
|
||||
64
openspec/changes/testing-msw-carve-out/tasks.md
Normal file
64
openspec/changes/testing-msw-carve-out/tasks.md
Normal file
@@ -0,0 +1,64 @@
|
||||
# Tasks — testing-msw-carve-out
|
||||
|
||||
This proposal is **deferred until activation**. Activation = the first time a React/R3F-tier package legitimately needs MSW (likely Wave 7).
|
||||
|
||||
## 0. Pre-flight (non-negotiable)
|
||||
|
||||
- [ ] 0.1 Confirm clean working tree (`git status` reports nothing) and no in-flight worktrees with uncommitted work (`git worktree list` then `git status` in each)
|
||||
- [ ] 0.2 If any worktrees exist under `.claude/worktrees/`, account for each one: still-active agent, work-pending-merge, or stale-and-prune. Stale worktrees MUST be removed (`git worktree remove`) before opening a new one — code that has been lost twice on main is the failure mode this guards against.
|
||||
- [ ] 0.3 Confirm the package set in `MANIFEST.md` matches `find packages -maxdepth 3 -name package.json` count. Drift here means a prior change archived without regenerating the manifest.
|
||||
|
||||
## Phase 1: Activation trigger (do nothing until then)
|
||||
|
||||
- [ ] 1.1 A package author files an OpenSpec sub-change requesting MSW under this carve-out, citing the concrete blocker (e.g. "Playwright E2E needs slow-body 503 response that real-server setup can't reproduce in < 50 LOC").
|
||||
- [ ] 1.2 Sub-change is reviewed against the carve-out's "Allowed / Forbidden" tables in `proposal.md`. If it doesn't fit, the answer is "use injection / real server" and this carve-out stays dormant.
|
||||
|
||||
## Phase 2: Build the testing-msw package
|
||||
|
||||
- [ ] 2.1 Create `packages/testing/testing-msw/` with standard testing-infra package shape (zero `@suaveplan/*` runtime deps).
|
||||
- [ ] 2.2 Add `msw` to root catalog under a new `testing-msw` group key.
|
||||
- [ ] 2.3 Author `createMswServer(handlers, options?)` — wraps `setupServer` with injectable `Clock` and `AbortSignal` integration.
|
||||
- [ ] 2.4 Author `createMswWorker(handlers, options?)` — browser-tier equivalent for Storybook + Playwright.
|
||||
- [ ] 2.5 Author `expectRequest(server, predicate, opts?)` — async wait-for-match helper with timeout + abort.
|
||||
- [ ] 2.6 Author `requestRecorder()` — ordered-assertion harness.
|
||||
- [ ] 2.7 Author `runMswConformance(label, makeFactory)` — shared conformance harness exposed under the `./testing` subpath (mirrors `i18n-shared/testing` pattern).
|
||||
- [ ] 2.8 100/100/100/100 coverage on the testing-msw surface itself.
|
||||
|
||||
## Phase 3: Codify the rule
|
||||
|
||||
- [ ] 3.1 Add §1 #2a "MSW carve-out" to `CLAUDE.md`, exact text from `proposal.md`.
|
||||
- [ ] 3.2 Author `openspec/specs/testing/spec.md` (or extend existing testing spec) with `## ADDED Requirements` capturing:
|
||||
- MSW MAY be used only through `@suaveplan/testing-msw`.
|
||||
- Tier restriction (React/R3F only).
|
||||
- Conformance-test requirement.
|
||||
- Handler co-location requirement.
|
||||
- [ ] 3.3 Update `openspec/AGENTS.md` if any §0a parity checklist row would conflict.
|
||||
|
||||
## Phase 4: Ship the first consumer
|
||||
|
||||
- [ ] 4.1 The triggering package (likely `react-network-libp2p-demo` or `r3f-networking`) consumes `@suaveplan/testing-msw` for its first MSW-backed test.
|
||||
- [ ] 4.2 That same package ships at least one Playwright E2E against a real protocol implementation (the conformance requirement).
|
||||
- [ ] 4.3 Both tests demonstrably exercise the same component code path.
|
||||
|
||||
## Phase 5: Verification
|
||||
|
||||
- [ ] 5.1 `bunx turbo lint typecheck test:coverage --filter=@suaveplan/testing-msw` green at 100/100/100/100.
|
||||
- [ ] 5.2 First consumer's tests green.
|
||||
- [ ] 5.3 `bun run docs:check` green on testing-msw.
|
||||
- [ ] 5.4 `STATUS.md` row added for `@suaveplan/testing-msw` (status `❌`-net-new → `✅`).
|
||||
- [ ] 5.5 `CLAUDE.md` and `openspec/specs/testing/spec.md` reflect the new rule.
|
||||
|
||||
## Pre-archive checklist
|
||||
|
||||
- [ ] Row 1 — Phase 5 green.
|
||||
- [ ] Row 2 — A real consumer is using the carve-out (no theoretical adoption).
|
||||
- [ ] Row 3 — Conformance-test requirement demonstrably enforced on the first consumer.
|
||||
- [ ] Row 4 — Folder moved to `openspec/archive/<yyyy-mm-dd>-testing-msw-carve-out/`.
|
||||
|
||||
## Anti-tasks (DO NOT do)
|
||||
|
||||
- [ ] DO NOT add MSW to any universal-tier or server-tier package's test suite.
|
||||
- [ ] DO NOT use MSW to work around an injectable-`fetchFn` design. If the package can be made injectable, do that instead.
|
||||
- [ ] DO NOT use MSW in production code paths.
|
||||
- [ ] DO NOT re-introduce `vi.mock`/`jest.mock` under the cover of "but MSW is fine now". The two are different layers and the second remains banned.
|
||||
- [ ] DO NOT centralise MSW handlers in a top-level `mocks/` directory. Handlers co-locate with tests.
|
||||
Reference in New Issue
Block a user