Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,86 @@
# Capability Spec Delta: testing (testing-generics-hardening)
**Package**: `@suaveplan/testing`
**Status**: PROPOSED
## ADDED Requirements
### Requirement: Builder presets thread `<TBody>` through `.build()`
Every `RequestBuilder` and `ResponseBuilder` preset factory SHALL accept an optional `<TBody = unknown>` type parameter. The returned builder MUST be `RequestBuilder<TBody>` / `ResponseBuilder<TBody>` and `.build()` MUST return `TestRequest<TBody | undefined>` / `TestResponse<TBody | undefined>`. Callers who omit the parameter MUST observe the same `unknown` defaults that pre-dated this change.
#### Scenario: post<TBody> 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<TBody> 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<unknown>` and `TestResponse<unknown>` respectively
**Verified by:** `src/builders/request-builder/request-builder.spec-conformance.test.ts::"post<TBody>(url, body).build() returns TestRequest<TBody | undefined>"` AND `src/builders/request-builder/request-builder.spec-conformance.test.ts::"get<TBody>(url).build() returns TestRequest<TBody | undefined>"` AND `src/builders/request-builder/request-builder.spec-conformance.test.ts::"put<TBody>, patch<TBody>, del<TBody>, head<TBody>, options<TBody> 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<TBody> exposes typed body field on state and build"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"ok<TBody>(body).build() returns TestResponse<TBody | undefined>"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"created<TBody>, badRequest<TBody>, unauthorized<TBody>, notFound<TBody>, serverError<TBody> thread TBody"` AND `src/builders/response-builder/response-builder.spec-conformance.test.ts::"noContent<TBody>() returns ResponseBuilder<TBody>"` 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<TBody> exposes typed body on state and build"`
---
### Requirement: `FixtureManager.get` accepts a typed value parameter
`FixtureManager.get` SHALL accept an optional `<T = unknown>` type parameter and return `FixtureState<T> | 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<DbFixture>("db")` is called
- **THEN** the inferred return type is `FixtureState<DbFixture> | 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<unknown> | undefined`
**Verified by:** `src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts::"get<T>(name) returns FixtureState<T> | undefined"` AND `src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts::"get<T>(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<unknown>"`
---
### Requirement: `assertThrows` returns the caller-narrowed error type
`assertThrows` and `assertThrowsAsync` SHALL accept an optional `<TError extends Error = Error>` type parameter and return `TError` / `Promise<TError>` 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<TestingError>(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<TestingError>(asyncFn)` is awaited
- **THEN** the inferred awaited type is `TestingError`
**Verified by:** `src/helpers/test-helpers.spec-conformance.test.ts::"assertThrows<TError>() 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<TError>() returns Promise<TError>"` 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)