6.4 KiB
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 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()orok().build()is called without a type argument - THEN the inferred return types are
TestRequest<unknown>andTestResponse<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> | undefinedAND the inferred type ofstate.value(afterstate !== undefined) isDbFixture | 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 andfnthrows aTestingError - THEN the inferred return type is
TestingErrorAND the caller can read.codewithout a follow-upinstanceofnarrowing
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/testingruns - 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.mdfiles - THEN every implementation file has a sibling
.mdAND every.mdis ≥ 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)