5.1 KiB
5.1 KiB
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:
FixtureManager.get(name)returnedFixtureState | undefined— callers who registeredFixtureDefinition<MyShape>lostMyShapeon read.RequestBuilder/ResponseBuilderpreset factories (get,post,ok,created, …) returned the bare class withbody: unknown, defeating the whole point of typed test request/response builders.assertThrows/assertThrowsAsyncreturnedError, so callers who matched onTestingErrorhad to follow up with aninstanceofnarrowing before reading.code.- The
typecheckscript inpackage.jsoninvokedbunx tsc— a Rule 1 violation (no directtsc).
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:
RequestBuilderis nowRequestBuilder<TBody = unknown>.body()acceptsTBody | undefined,statereturnsRequestBuilderState<TBody>,build()returnsTestRequest<TBody | undefined>. - MODIFIED:
ResponseBuilderis nowResponseBuilder<TBody = unknown>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<TBody = unknown>and returns the typed builder. - MODIFIED:
TestRequestandTestResponseare nowTestRequest<TBody = unknown>andTestResponse<TBody = unknown>. - MODIFIED:
FixtureManager.getis nowget<T = unknown>(name): FixtureState<T> | undefined. The internal cast is confined to the impl. - MODIFIED:
assertThrows<TError extends Error = Error>(fn, matcher?): TErrorand the async variant. Internal cast at the boundary. - MODIFIED:
package.jsonscripts.typecheckswitched frombunx tsc --noEmit --composite false -p tsconfig.lib.jsontotsc --project tsconfig.lib.json --noEmit, matching the canonical pattern in non-dev packages and dropping thebunxwrapper. - MODIFIED:
openspec/specs/testing/spec.md"Coverage and docs gates" requirement: bumped from ≥ 95% to 100% across statements, branches, functions, and lines (matches the wiredvitest.config.tsthresholds and CLAUDE.md mandate). Path string also corrected frompackages/universal/testing/topackages/dev/testing/(folder reorg leftover). - MODIFIED:
tests/spec-conformance.spec-conformance.test.tsALLOWED_NONDETERMINISMextended to includesrc/helpers/test-helpers.ts(measureTime) andsrc/observability/with-observability.ts(histogram duration recording). Both legitimately readperformance.now()at the wrapper boundary; injecting aClockwould 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—expectTypeOfpins 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 outsidepackages/dev/testing/andopenspec/{specs,changes}/testing/are touched. MANIFEST.md, rootbun.lock, andCLAUDE.mdare 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 viaexpectTypeOf.src/builders/response-builder/response-builder.spec-conformance.test.ts— same pattern forok/created/noContent/badRequest/unauthorized/notFound/serverErrorand the class.src/fixtures/fixture-manager/fixture-manager.spec-conformance.test.ts— pinsget<T>overload, narrowing ofstate.value, and the unknown-default fallback.src/helpers/test-helpers.spec-conformance.test.ts— pinsassertThrows<TError>andassertThrowsAsync<TError>returningTError/Promise<TError>.- 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<MyBody>(…), mgr.get<MyShape>("name"), or assertThrows<MyError>(…).