# Capability Spec Delta: testing (testing-generics-hardening)
**Package**: `@suaveplan/testing`
**Status**: PROPOSED
## ADDED Requirements
### Requirement: Builder presets thread `
` through `.build()`
Every `RequestBuilder` and `ResponseBuilder` preset factory SHALL accept an optional `` type parameter. The returned builder MUST be `RequestBuilder` / `ResponseBuilder` and `.build()` MUST return `TestRequest` / `TestResponse`. Callers who omit the parameter MUST observe the same `unknown` defaults that pre-dated this change.
#### Scenario: post threads body type
- **WHEN** `post<{ name: string }>("/x", { name: "a" }).build()` is called
- **THEN** the inferred return type is `TestRequest<{ name: string } | undefined>`
#### Scenario: ok threads body type
- **WHEN** `ok<{ id: string }>({ id: "1" }).build()` is called
- **THEN** the inferred return type is `TestResponse<{ id: string } | undefined>`
#### Scenario: zero-arg preset preserves unknown default
- **WHEN** `get("/x").build()` or `ok().build()` is called without a type argument
- **THEN** the inferred return types are `TestRequest` and `TestResponse` respectively
**Verified by:** `src/builders/request-builder/request-builder.spec-conformance.test.ts::"post(url, body).build() returns TestRequest"` AND `src/builders/request-builder/request-builder.spec-conformance.test.ts::"get(url).build() returns TestRequest"` AND `src/builders/request-builder/request-builder.spec-conformance.test.ts::"put, patch, del, head, options all thread TBody"` AND `src/builders/request-builder/request-builder.spec-conformance.test.ts::"zero-arg presets default TBody to unknown"` AND `src/builders/request-builder/request-builder.spec-conformance.test.ts::"RequestBuilder exposes typed body field on state and build"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"ok(body).build() returns TestResponse"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"created, badRequest, unauthorized, notFound, serverError thread TBody"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"noContent() returns ResponseBuilder"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"zero-arg presets default TBody to unknown"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"ResponseBuilder exposes typed body on state and build"`
---
### Requirement: `FixtureManager.get` accepts a typed value parameter
`FixtureManager.get` SHALL accept an optional `` type parameter and return `FixtureState | undefined`. The narrowing SHALL extend through `state.value` so callers see `T | null` without a follow-up cast. Internally the manager MAY perform a single boundary cast; consumers MUST NOT need any.
#### Scenario: typed get returns typed state
- **WHEN** `mgr.get("db")` is called
- **THEN** the inferred return type is `FixtureState | undefined` AND the inferred type of `state.value` (after `state !== undefined`) is `DbFixture | null`
#### Scenario: untyped get preserves unknown default
- **WHEN** `mgr.get("db")` is called without a type argument
- **THEN** the inferred return type is `FixtureState | undefined`
**Verified by:** `src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts::"get(name) returns FixtureState | undefined"` AND `src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts::"get(name) narrows state.value to T | null"` AND `src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts::"get(name) without explicit T defaults to FixtureState"`
---
### Requirement: `assertThrows` returns the caller-narrowed error type
`assertThrows` and `assertThrowsAsync` SHALL accept an optional `` type parameter and return `TError` / `Promise` respectively. Internally the helper MAY cast at the boundary; the runtime fallback (wrap a non-`Error` throw in `new Error(String(value))`) MUST remain.
#### Scenario: typed assertThrows returns typed error
- **WHEN** `assertThrows(fn)` is called and `fn` throws a `TestingError`
- **THEN** the inferred return type is `TestingError` AND the caller can read `.code` without a follow-up `instanceof` narrowing
#### Scenario: untyped assertThrows defaults to Error
- **WHEN** `assertThrows(fn)` is called without a type argument
- **THEN** the inferred return type is `Error`
#### Scenario: async variant matches sync contract
- **WHEN** `assertThrowsAsync(asyncFn)` is awaited
- **THEN** the inferred awaited type is `TestingError`
**Verified by:** `src/helpers/test-helpers.spec-conformance.test.ts::"assertThrows() returns TError"` AND `src/helpers/test-helpers.spec-conformance.test.ts::"assertThrows() without explicit TError defaults to Error"` AND `src/helpers/test-helpers.spec-conformance.test.ts::"assertThrowsAsync() returns Promise"` AND `src/helpers/test-helpers.spec-conformance.test.ts::"assertThrowsAsync() without explicit TError defaults to Error"`
## MODIFIED Requirements
### Requirement: Coverage and docs gates
Istanbul statement + branch + function + line coverage across `packages/dev/testing/src/` (excluding `*.bench.ts` and `*.schemas.ts`) MUST be 100%. Every implementation file (`*.ts` other than `*.test.ts`, `*.bench.ts`, `*.schemas.ts`, inner-dir `*.types.ts`, barrel `index.ts`) MUST have a sibling `.md` file of ≥ 200 words (excluding code fences) with a simple and an advanced usage example.
#### Scenario: Coverage gate in CI
- **WHEN** `bunx turbo test:coverage --filter=@suaveplan/testing` runs
- **THEN** reported statement, branch, function, and line coverage are each 100%
#### Scenario: Docs coverage enforced
- **WHEN** a script enumerates implementation files under `src/` (excluding the stated exclusions) and checks for sibling `.md` files
- **THEN** every implementation file has a sibling `.md` AND every `.md` is ≥ 200 non-code words
**Verified by:** `vitest.config.ts` (coverage thresholds set to 100/100/100/100) AND `bunx turbo test:coverage --filter=@suaveplan/testing` (CI run) AND `bun run docs:check` (sibling-`.md` enumeration script)