Files
suaveplan/openspec/archive/2026-04-27-testing-integration-spec-and-generics-hardening/proposal.md
2026-05-27 23:29:53 +01:00

5.0 KiB

testing-integration: spec authoring + generics hardening

Package: @suaveplan/testing-integration and the six variant fixtures (@suaveplan/testing-integration-{surrealdb,redis,postgres,mailpit,typesense,garage-s3}) Status: PROPOSED Type: spec-conformance + refactor

Why

The seven testing-integration* packages shipped without folded specs and with several latent issues uncovered by the Slice-3 audit:

  • Rule 8 violation — every variant listed @suaveplan/testing-integration in dependencies, not peerDependencies. The core's API is meant to be brought by the consumer; treating it as a hard dep duplicates copies in user node_modules and makes the instanceof TestEnvironmentError checks fail across realms.
  • vitest runtime import in core, devDep only — create-integration-setup.ts imported inject from vitest. With vitest only in devDependencies, an installed-from-registry consumer would load a broken module.
  • Generic erasure — TestEnvironment.setup(), HealthPoller.poll(), and skipIfUnavailable() typed their fixture surface as Record<string, unknown>, throwing away precise per-fixture config types.
  • assertMail() cast-based parsing — the Mailpit response shape was as { messages?: ... }-cast; a server payload regression silently surfaced as a runtime TypeError deep in m.To.some(...).
  • GarageS3Fixture.waitForHealth reimplemented HealthPoller — divergent timeout semantics + bespoke error type.
  • _fixtureRegistry and _createIntegrationSetupWithInject exported from the public barrel — leading-underscore naming is convention only; nothing prevented downstream from importing them and pinning the package shape.

This change folds the seven packages' contracts into seven openspec/specs/* files with Verified by: lines on every requirement, fixes the plumbing, and threads precise generics end-to-end.

What changes

  • ADDED: openspec/specs/testing-integration/spec.md — folded spec for the core.
  • ADDED: openspec/specs/testing-integration-{surrealdb,redis,postgres,mailpit,typesense,garage-s3}/spec.md — six folded specs for the variants.
  • ADDED: src/internal/ subdirectory (NOT in package.json exports) housing fixtureRegistry, createIntegrationSetupWithInject, and createSkipIfUnavailableWithInject. Public barrel no longer re-exports them.
  • ADDED: src/environment/test-environment.spec-conformance.test.ts — pins generic threading on TestEnvironment.setup(), HealthPoller.poll(), createIntegrationSetup's inject(), and skipIfUnavailable().
  • MODIFIED: TestEnvironment is now generic over TFixtures extends DefaultFixtureMap; setup() returns { configs: { [K in keyof TFixtures]?: InferConfig<TFixtures[K]> }; failed: string[] }. Defaults preserve compat.
  • MODIFIED: HealthPoller.poll() is now generic over TConfig and accepts Pick<ServiceFixture<TConfig>, "name" | "healthCheck"> instead of widening to ServiceFixture<Record<string, unknown>>.
  • MODIFIED: skipIfUnavailable<TName extends string = string>() is now generic over the fixture-name keyset for typed-registry pairing.
  • MODIFIED: assertMail() validates Mailpit responses with a Zod schema and throws TestEnvironmentError on shape mismatch, non-2xx, or timeout.
  • MODIFIED: GarageS3Fixture.waitForHealth() delegates to HealthPoller.poll(this).
  • MODIFIED: every variant package.json moves @suaveplan/testing-integration and testcontainers from dependencies to peerDependencies (and adds them to devDependencies so workspace symlinks resolve in the variant's own tests).
  • MODIFIED: core package.json adds vitest to peerDependencies.
  • REMOVED: _fixtureRegistry, _createIntegrationSetupWithInject, and _createSkipIfUnavailableWithInject from the public barrel. Replaced by the package-private internal/ modules.

Out of scope

  • Adding new fixture variants (Kafka, ClickHouse, etc.) — deferred.
  • Changing the Vitest global-setup contract — deferred until Vitest 5 lands.
  • Migrating assertMail to a generic mail-API client — deferred.

Verification approach

Each requirement in every spec carries a **Verified by:** line naming an existing it(...) title. Every claim about a generic is pinned by an expectTypeOf assertion in src/environment/test-environment.spec-conformance.test.ts. Coverage stays at 100/100/100/100 across all seven packages.

Migration impact

  • Variant packages (six fixtures) — consumers who imported _fixtureRegistry / _createIntegrationSetupWithInject / _createSkipIfUnavailableWithInject from @suaveplan/testing-integration must migrate to the public createIntegrationSetup / skipIfUnavailable API. The leading-underscore symbols were never part of the supported surface (their JSDoc explicitly said "for testing only"); no in-repo consumer imports them.
  • TestEnvironment and HealthPoller callers — non-breaking; the new generics default to the prior erased contract.