# 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)