## Context `@suaveplan/testing` sits at the bottom of the genesis dependency graph. Every other `@suaveplan/*` package uses it to write unit/integration tests. The archived `2026-04-08-testing-libraries` change on main established the current shape — dual-runner conditional exports (vitest default / bun condition), sub-path exports for `./config` / `./utils` / `./polyfills`, full mock API, `createTestConfig` factory absorbing WSL2 pool + polyfill settings. That shape is load-bearing and must survive. Constraints specific to genesis: - **Rule #9** — no `@suaveplan/*` workspace deps. External catalog deps only. - **Layer 0** — nothing else on genesis has been built yet. No consumers to migrate. The audit phase is surface-preservation against main's source, not consumer churn. - **Version** — starts at `0.1.0`; stays there until graduation cut. - **Enterprise bar** — `enterprise/testing.md` §6 acceptance criteria is the gate for marking the package ✅ Enterprise. ## Goals / Non-Goals **Goals:** - Deliver every capability from `enterprise/testing.md` §3 and §4 within the genesis-rule constraints. - Preserve the full public surface inventoried below; no signature shrinkage. - Make the package a reference implementation for the enterprise checklist in `openspec/project.md` — so subsequent genesis packages can copy its patterns (error shape, Zod schema layout, OTEL wrapping, Clock/Rng/Signal DI, property tests, benchmarks). - Dual-runner condition (vitest default, bun-test as a peer) remains intact. **Non-Goals:** - Introducing new user-facing APIs. The §4 "feature-rich" items (plugin interface on `FixtureManager`, `traits` on `Factory`) are additive to the existing shape, not replacements. - Supporting any runner beyond Vitest and bun-test. - Shipping a `@suaveplan/*` adapter for schemas / errors / OTEL — those integrations land when each matching package reaches enterprise on a later layer. - Re-routing public messages through i18n — English-only by design (rule parallel to utils D10). ## Surface Inventory (public exports that MUST survive) From `/mnt/s/web/suaveplan/packages/universal/testing/src/` — every name below is preserved unchanged in type shape; internals are rewritten. ### Runner (`src/runner/`) Conditional export routing via `package.json`: - `vitest` condition → `src/runner/vitest.ts` (re-exports vitest primitives) - `bun` condition → `src/runner/bun-test.ts` (bun-test shim) - default → `src/runner/index.ts` (barrel for the vitest path) Exports (from the default condition): `afterAll`, `afterEach`, `beforeAll`, `beforeEach`, `describe`, `expect`, `fakeClock`, `fn`, `it`, `mock`, `restoreMocks`, `restoreTimers`, `spy`, `stub`, `test`, `vi`. ### Builders (`src/builders/`) - `RequestBuilder` class; quick helpers `get`, `post`, `put`, `patch`, `del`. - `ResponseBuilder` class; quick helpers `ok`, `created`, `noContent`, `badRequest`, `unauthorized`, `notFound`, `serverError`. - Types: `RequestBuilderPreset`, `RequestBuilderState`, `ResponseBuilderPreset`, `ResponseBuilderState`, `TestRequest`, `TestResponse`, `HttpMethod`, `HttpHeaders`, `QueryParams`. ### Clock (`src/clock/`) - `FakeClock` class (implements the local `Clock` interface). - Types: `ClockSnapshot`, `FakeClockOptions`, `ScheduledTimer`. ### Factories (`src/factories/`) - `defineFactory(definition)`, `Factory` class. - Types: `FactoryBuildOptions`, `FactoryDefaults`, `FactoryDefinition`, `BatchBuildResult`. ### Fixtures (`src/fixtures/`) - `FixtureManager` class. - Types: `FixtureDefinition`, `FixtureState`, `SetupAllResult`, `TeardownAllResult`. ### Storage (`src/storage/`) - `MemoryStore` class. - Types: `MemoryStoreEvent`, `MemoryStoreStats`, `MemoryStoreOptions`, `StoreEntry`. ### Helpers (`src/helpers/`) - `assertThrows`, `assertThrowsAsync`, `createCollector`, `deferred`, `measureTime`, `resetTestIdCounter`, `sleep`, `uniqueTestId`, `waitFor`. - Types: `WaitForOptions`. ### Constants (`src/constants/`) - `DEFAULT_CLOCK_OPTIONS`, `DEFAULT_HTTP_STATUS`, `DEFAULT_RESPONSE_LATENCY_MS`, `DEFAULT_STORE_OPTIONS`, `DEFAULT_WAIT_OPTIONS`, `MAX_FIXTURES`, `MAX_STORE_ENTRIES`, `MAX_TIMERS`. ### Config subpath (`@suaveplan/testing/config`) - `createTestConfig(options?)` async factory. - Types: `TestConfigOptions`, `TestRunner`, `TestEnvironment`, `VitestConfigOverrides`, `BunTestConfig`, `BunTestConfigOverrides`. ### Utils subpath (`@suaveplan/testing/utils`) Re-exports every non-runner symbol for callers that want a single import without pulling the runner surface. ### Polyfills subpath (`@suaveplan/testing/polyfills`) Side-effecting module loaded by `setupFiles` in consumers' vitest configs. Installs whatever global shims (e.g. `crypto`) the happy-dom environment needs on WSL2. ## Decisions ### D1: Local `Clock` and `Rng` interfaces in `src/context/` ```ts export interface Clock { now(): number; // ms since epoch hrtime(): bigint; // nanoseconds, monotonic } export interface Rng { nextUint32(): number; // [0, 2^32) nextFloat(): number; // [0, 1) } export const defaultClock: Clock; export const cryptoRng: Rng; export function seededRng(seed: number): Rng; ``` Rationale: matches the shape `@suaveplan/datetime` will eventually expose, so a later genesis layer can ship an adapter without changing call sites. Keeps genesis rule #9 clean. `FakeClock` implements `Clock` so callers can swap seamlessly. ### D2: Local `TestingError` hierarchy ```ts export type TestingErrorCode = | "TESTING.CLOCK.MAX_TIMERS_EXCEEDED" | "TESTING.CLOCK.NEGATIVE_ADVANCE" | "TESTING.FIXTURES.SETUP_FAILED" | "TESTING.FIXTURES.TEARDOWN_FAILED" | "TESTING.FIXTURES.MAX_FIXTURES_EXCEEDED" | "TESTING.FACTORY.BUILD_FAILED" | "TESTING.FACTORY.TRAIT_NOT_FOUND" | "TESTING.STORE.CAPACITY_EXCEEDED" | "TESTING.STORE.KEY_NOT_FOUND" | "TESTING.HELPERS.WAIT_TIMEOUT" | "TESTING.HELPERS.ASSERTION_FAILED" | "TESTING.BUILDER.INVALID_STATE" | "TESTING.CONFIG.UNKNOWN_RUNNER" | "TESTING.ABORTED"; export class TestingError extends Error { readonly code: TestingErrorCode; readonly context?: Record; constructor(code: TestingErrorCode, message: string, options?: { cause?: unknown; context?: Record; }); } ``` Subclasses per domain (`ClockError`, `FixturesError`, `FactoryError`, `StoreError`, `HelpersError`, `BuilderError`, `ConfigError`) are thin wrappers narrowing the code union — callers can branch on `err instanceof ClockError`. All still satisfy `err instanceof TestingError`. Rationale: shape-compatible with eventual `@suaveplan/error`; per-domain subclasses let TypeScript narrow the code; stable strings as machine contract. ### D3: Zod schemas in sibling `*.schemas.ts` Every implementation file `foo/foo.ts` has a peer `foo/foo.schemas.ts` exporting input/output schemas under a stable namespace. Example: ```ts // src/builders/request-builder/request-builder.schemas.ts import z from "zod"; export const RequestBuilderSchemas = { preset: z.enum(["get", "post", "put", "patch", "delete"]), input: z.object({ method: z.string(), url: z.string(), headers: z.record(z.string()).optional(), query: z.record(z.string()).optional(), body: z.unknown().optional(), }), output: z.object({ /* … */ }), } as const; ``` Schemas are NOT pulled in by the functional entry points — they are consumed only when a caller opts in (`RequestBuilderSchemas.input.parse(…)`). Verified by the tree-shake test. Rationale: respects the enterprise requirement (Zod on public types) without forcing runtime validation on every call. Stable names mean a future `@suaveplan/schema-registry` adapter can enumerate them without modifying this package. ### D4: OTEL via `@opentelemetry/api`, no-op by default ```ts // src/observability/tracer.ts import { trace, metrics, type Span, type Counter } from "@opentelemetry/api"; const tracer = trace.getTracer("@suaveplan/testing"); const meter = metrics.getMeter("@suaveplan/testing"); ``` Every public entrypoint is wrapped via a local `withObservability(name, fn)` helper. When no SDK is registered (default for Vitest/Bun-test runs), `trace.getTracer()` returns a NoopTracer; overhead is a single function call and object allocation per invocation — immeasurable in practice. Counters: `testing...count`, `testing...errors{code}`. Histograms: `testing...duration_ms`. Spans: only for iterative operations (`waitFor`, `FixtureManager.setupAll`, `Factory.buildBatch`). Rationale: `@opentelemetry/api` is the stable public spec — 2 KB, zero transitive Node deps, safe in browsers. Writing against it means a future `@suaveplan/opentelemetry` package that merely bundles an exporter + resource config will just work. ### D5: Dual-runner conditional exports preserved exactly `package.json` retains: ```json "exports": { ".": { "vitest": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, "bun": { "types": "./dist/runner/bun-test.d.ts", "default": "./dist/runner/bun-test.js" }, "default": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } }, "./config": { … }, "./utils": { … }, "./polyfills": "./dist/setup/polyfills.js", "./package.json": "./package.json" } ``` Rationale: established shape from the archived change. Swapping a package from vitest to bun-test is a single-condition change with no test-source rewrites. ### D6: AbortSignal at bounded checkpoints `waitFor` checks `signal?.aborted` each poll cycle. `FixtureManager.setupAll` / `teardownAll` check between fixtures. `Factory.buildBatch(n)` checks every 256 items. `MemoryStore` eviction sweeps check every 1024 entries. On abort, throws `TestingError("TESTING.ABORTED", …, { cause: signal.reason })`. Rationale: deterministic cancellation with predictable cost. Enterprise doc §3 "Backpressure & cancellation". ### D7: Plugin interface on `FixtureManager` (feature-rich) ```ts export interface FixtureManagerPlugin { name: string; beforeEach?(ctx: { fixtureName: string; state: FixtureState }): void | Promise; afterEach?(ctx: { fixtureName: string; state: FixtureState }): void | Promise; onError?(ctx: { fixtureName: string; error: TestingError }): void | Promise; } ``` Rationale: enterprise doc §4 "plugin / extension interface". Downstream genesis packages can contribute fixture-level concerns (e.g. database seeding for a `surrealdb` package) without forking `FixtureManager`. ### D8: Benchmarks in `src/__benchmarks__/.bench.ts` Every domain has a bench file. `benchmarks/baselines.json` stores the baseline. CI runs benchmarks 3×, takes the fastest, compares to baseline, fails if any op regresses >10%. Baseline updates land in their own commit with reviewer approval. Rationale: enterprise doc §4 "Performance". Baselines taken against the genesis implementation, not retrofit. ### D9: `createTestConfig()` absorbs WSL2 realities The factory handles: - `pool: "forks"` (vmThreads/threads fail on WSL2) - `setupFiles: ["@suaveplan/testing/polyfills"]` - `fileParallelism: false` for browser envs - React plugin loaded only when `environment === "happy-dom"` and `@vitejs/plugin-react` is installed (optional peer) - bun-test path when caller's `package.json` / env says so Preserved exactly from the archived 2026-04-08 change. ## Risks / Trade-offs - **OTEL overhead on hot paths.** `trace.getTracer().startSpan()` in a noop tracer is ~5 ns — measurable in tight benchmarks. Mitigation: benchmark suite asserts overhead stays below 5% vs a no-OTEL baseline. - **Zod bundle cost on `@suaveplan/testing` root bundle.** Mitigation: schemas in peer `*.schemas.ts` files, not imported by functional entry points. Tree-shake test enforces. - **Local `TestingError` diverging from `@suaveplan/error`.** Mitigation: shape locked to `{ code, cause, context }` today. A future adapter is a one-file bridge. - **`@opentelemetry/api` version churn.** Mitigation: pin via catalog. The API spec itself has been stable across major versions of the SDK. - **fast-check timeouts on WSL2.** Mitigation: cap `numRuns: 100` in CI; larger runs nightly only. Generator sizes capped per-test. - **Benchmark flakiness in CI.** Mitigation: mitata stabilization (min samples, outlier trimming), 3-strike retry, 10% absolute floor. - **Node-only APIs sneaking in (fs/path/url in `createTestConfig`).** Mitigation: `createTestConfig` is explicitly a Node-side factory (runs in vitest's Node context), not a browser-runnable helper. Tree-shake test verifies that importing a browser-runnable helper (e.g. `FakeClock`) does NOT pull in `createTestConfig`'s Node deps.