# Testing generics hardening **Package**: `@suaveplan/testing` **Status**: PROPOSED **Type**: feature ## Why Slice 1 of the testing-suite hardening sweep. Four type-level cliffs in the public surface forced consumers into `as` casts or eroded inference back to `unknown`: 1. `FixtureManager.get(name)` returned `FixtureState | undefined` — callers who registered `FixtureDefinition` lost `MyShape` on read. 2. `RequestBuilder` / `ResponseBuilder` preset factories (`get`, `post`, `ok`, `created`, …) returned the bare class with `body: unknown`, defeating the whole point of typed test request/response builders. 3. `assertThrows` / `assertThrowsAsync` returned `Error`, so callers who matched on `TestingError` had to follow up with an `instanceof` narrowing before reading `.code`. 4. The `typecheck` script in `package.json` invoked `bunx tsc` — a Rule 1 violation (no direct `tsc`). Plus one spec-coverage drift: `openspec/specs/testing/spec.md` claimed ≥ 95% coverage while `vitest.config.ts` enforces 100% across all four metrics and CLAUDE.md mandates 100%. ## What changes - MODIFIED: `RequestBuilder` is now `RequestBuilder`. `body()` accepts `TBody | undefined`, `state` returns `RequestBuilderState`, `build()` returns `TestRequest`. - MODIFIED: `ResponseBuilder` is now `ResponseBuilder` with the same body-typed contract. - MODIFIED: every preset factory (`get`, `post`, `put`, `patch`, `del`, `head`, `options`, `ok`, `created`, `noContent`, `badRequest`, `unauthorized`, `notFound`, `serverError`) takes `` and returns the typed builder. - MODIFIED: `TestRequest` and `TestResponse` are now `TestRequest` and `TestResponse`. - MODIFIED: `FixtureManager.get` is now `get(name): FixtureState | undefined`. The internal cast is confined to the impl. - MODIFIED: `assertThrows(fn, matcher?): TError` and the async variant. Internal cast at the boundary. - MODIFIED: `package.json` `scripts.typecheck` switched from `bunx tsc --noEmit --composite false -p tsconfig.lib.json` to `tsc --project tsconfig.lib.json --noEmit`, matching the canonical pattern in non-dev packages and dropping the `bunx` wrapper. - MODIFIED: `openspec/specs/testing/spec.md` "Coverage and docs gates" requirement: bumped from ≥ 95% to 100% across statements, branches, functions, and lines (matches the wired `vitest.config.ts` thresholds and CLAUDE.md mandate). Path string also corrected from `packages/universal/testing/` to `packages/dev/testing/` (folder reorg leftover). - MODIFIED: `tests/spec-conformance.spec-conformance.test.ts` `ALLOWED_NONDETERMINISM` extended to include `src/helpers/test-helpers.ts` (`measureTime`) and `src/observability/with-observability.ts` (histogram duration recording). Both legitimately read `performance.now()` at the wrapper boundary; injecting a `Clock` would defeat the contract (callers ask for real elapsed time). The exemption is documented in-test. - ADDED: `src/builders/request-builder/request-builder.spec-conformance.test.ts`, `src/builders/response-builder/response-builder.spec-conformance.test.ts`, `src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts`, `src/helpers/test-helpers.spec-conformance.test.ts` — `expectTypeOf` pins on every generic claim. ## Out of scope - No runtime behavior change. All 295 existing tests continue to pass byte-for-byte; the 17-test increase is purely the new type-level conformance pins. - Slice 2 (`testing-dom`, `testing-e2e`) and Slice 3 (`testing-integration*`) are owned by sibling agents; no files outside `packages/dev/testing/` and `openspec/{specs,changes}/testing/` are touched. - `MANIFEST.md`, root `bun.lock`, and `CLAUDE.md` are intentionally not edited — orchestrator territory. ## Verification approach - `src/builders/request-builder/request-builder.spec-conformance.test.ts` — pins every preset (`get/post/put/patch/del/head/options`) and the class instance to the new generic shape via `expectTypeOf`. - `src/builders/response-builder/response-builder.spec-conformance.test.ts` — same pattern for `ok/created/noContent/badRequest/unauthorized/notFound/serverError` and the class. - `src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts` — pins `get` overload, narrowing of `state.value`, and the unknown-default fallback. - `src/helpers/test-helpers.spec-conformance.test.ts` — pins `assertThrows` and `assertThrowsAsync` returning `TError` / `Promise`. - Existing runtime tests (`request-builder.test.ts`, `response-builder.test.ts`, `fixture-manager.test.ts`, `test-helpers.test.ts`) continue to exercise behavior at runtime; type widening means none of them needed editing. ## Migration impact None — additive only. The default type parameters (`TBody = unknown`, `T = unknown`, `TError extends Error = Error`) preserve the exact pre-change inferred types for any caller that does not pass an explicit type argument. Callers who want the new tightening opt in by writing `post(…)`, `mgr.get("name")`, or `assertThrows(…)`.