## ADDED Requirements ### Requirement: Universal purity The `@suaveplan/testing` package SHALL depend on zero browser-only, Node-only, or React APIs at the module-graph level for its core runtime exports. The `./config` subpath and `./polyfills` subpath MAY reference Node built-ins because they are explicitly Node-side and are never pulled transitively by browser-runnable exports. #### Scenario: Browser-runnable exports avoid Node built-ins - **WHEN** a browser target imports `FakeClock`, `RequestBuilder`, `ResponseBuilder`, `Factory`, `FixtureManager`, `MemoryStore`, or any helper - **THEN** the resolved module graph MUST NOT reference `node:fs`, `node:path`, `node:url`, `process`, `Buffer`, or any Node-only API #### Scenario: Node-side subpaths are segregated - **WHEN** a consumer imports `@suaveplan/testing/config` or `@suaveplan/testing/polyfills` - **THEN** those imports resolve to modules that are allowed to reference Node built-ins AND the core export (`@suaveplan/testing`) MUST NOT transitively re-import them ### Requirement: No `@suaveplan/*` workspace dependencies The `@suaveplan/testing` package `dependencies`, `peerDependencies`, and `devDependencies` MUST NOT list any `@suaveplan/*` scoped package. External catalog deps only (vitest, happy-dom, ws, tslib, zod, `@opentelemetry/api`, fast-check, mitata, `@types/*`, build tooling). #### Scenario: Dep-graph guard - **WHEN** `bunx turbo ls --filter=@suaveplan/testing...` is run - **THEN** no `@suaveplan/*` package appears in the forward closure of `@suaveplan/testing` ### Requirement: Public types validated via Zod Every public function's input and output types SHALL be modeled by a Zod schema exported from a sibling `.schemas.ts` file under a stable namespace (e.g. `RequestBuilderSchemas.input`). Schemas MUST NOT be imported by functional entry points; consumers opt in by importing `*.schemas.ts` directly. #### Scenario: Runtime validation available on opt-in - **WHEN** a consumer imports `FactorySchemas.buildOptions` from `@suaveplan/testing/factories/factory/factory.schemas` and calls `.parse(value)` - **THEN** invalid input is rejected with a `ZodError` whose `issues[].path` points to the offending field #### Scenario: Schemas do not bloat the functional bundle - **WHEN** a build imports only `Factory` from `@suaveplan/testing` - **THEN** the resolved bundle MUST NOT include Zod or any `*.schemas.ts` module ### Requirement: Structured error hierarchy The package SHALL throw only subclasses of `TestingError extends Error` with a stable `code` of the form `TESTING..` (plain uppercase ASCII, dots as separators). Errors MUST carry `{ code, cause?, context? }` fields. The package MUST NOT throw plain `Error` or `TypeError` from any public code path. #### Scenario: Timer overflow throws a coded error - **WHEN** `FakeClock` is asked to schedule more than `MAX_TIMERS` timers - **THEN** it throws `TestingError` with `code === "TESTING.CLOCK.MAX_TIMERS_EXCEEDED"` AND `context.limit === MAX_TIMERS` #### Scenario: Fixture setup failure preserves cause - **WHEN** a fixture's `setup` function throws - **THEN** `FixtureManager.setupAll` throws `TestingError` with `code === "TESTING.FIXTURES.SETUP_FAILED"` AND `cause` is the original thrown value AND `context.fixtureName` is the failing fixture's name #### Scenario: Abort signal triggered mid-wait - **WHEN** `waitFor(predicate, { signal })` runs with a signal that aborts during polling - **THEN** the returned promise rejects with `TestingError` where `code === "TESTING.ABORTED"` AND `cause === signal.reason` ### Requirement: Deterministic clock and RNG via dependency injection Every public function that reads time, randomness, or iterates SHALL accept an optional `{ clock?: Clock; rng?: Rng; signal?: AbortSignal }` options parameter. Zero-config callers MUST get behaviorally stable defaults: `defaultClock` (backed by `Date.now` / `performance.now`), `cryptoRng` (backed by `crypto.getRandomValues`), and no abort. #### Scenario: Injected clock controls observed time - **WHEN** `waitFor(predicate, { clock: fakeClock })` runs and `fakeClock.advance(1000)` is called - **THEN** the helper observes 1000 ms elapsed AND does NOT read `Date.now()` or `performance.now()` directly #### Scenario: Injected RNG produces deterministic factory output - **WHEN** `Factory.build({ rng: seededRng(42) })` is called twice with the same factory definition - **THEN** both calls return byte-identical output (for factories whose definition is otherwise deterministic) #### Scenario: Module-scope non-determinism absent - **WHEN** the built `dist/` is grep'd for `Math.random(` or `Date.now(` or `performance.now(` - **THEN** those symbols appear only inside the `defaultClock` / `cryptoRng` module (and `FakeClock`'s internal simulation), never at module scope in other files ### Requirement: Cooperative cancellation via AbortSignal Every iterative or async helper SHALL check its `signal?.aborted` property at bounded intervals (≤ 256 items or ≤ 1 KB scanned per check) and throw `TestingError("TESTING.ABORTED", …)` on first observed abort without returning partial results. #### Scenario: Batch factory aborts within one tick - **WHEN** `Factory.buildBatch(10000, { signal })` is running and `signal.abort()` is called after 200 ms - **THEN** the promise rejects with `TESTING.ABORTED` within one event-loop tick of the abort #### Scenario: Fixture teardown aborts between fixtures, never mid-fixture - **WHEN** `FixtureManager.teardownAll({ signal })` is mid-teardown and the signal fires - **THEN** the current fixture's teardown completes AND the next fixture is NOT started AND the promise rejects with `TESTING.ABORTED` ### Requirement: Dual-runner conditional exports The package `exports` map SHALL route `@suaveplan/testing` via a `vitest` condition to the vitest-backed barrel, a `bun` condition to the bun-test shim, and a `default` condition to the vitest barrel. The vitest barrel MUST re-export `describe`, `it`, `test`, `expect`, `afterAll`, `afterEach`, `beforeAll`, `beforeEach`, `vi`, `fn`, `mock`, `spy`, `stub`, `fakeClock`, `restoreMocks`, `restoreTimers`. The bun-test shim MUST expose the same names via bun-test primitives. #### Scenario: Vitest consumer resolves vitest primitives - **WHEN** a package with `vitest` in its test runner configuration imports `{ describe, it, expect }` from `@suaveplan/testing` - **THEN** resolution MUST pick the `vitest` condition and the imported symbols MUST be vitest's #### Scenario: Bun-test consumer resolves bun-test primitives - **WHEN** a package configured for `bun test` imports `{ describe, it, expect }` from `@suaveplan/testing` - **THEN** resolution MUST pick the `bun` condition and the imported symbols MUST come from bun's test runner ### Requirement: Observability via `@opentelemetry/api` Every public entrypoint SHALL be instrumented with an OTEL counter (`testing...count`), a histogram (`testing...duration_ms`), and an error counter (`testing...errors` with `code` attribute on error). Spans SHALL be created only for iterative operations (`waitFor`, `FixtureManager.setupAll`/`teardownAll`, `Factory.buildBatch`). When no OTEL SDK is registered, all instrumentation MUST be a no-op at runtime. #### Scenario: Counter increments on every call - **WHEN** `RequestBuilder` methods are called N times during a test run with a registered OTEL SDK - **THEN** the metric `testing.builders.request.count` has recorded N increments #### Scenario: Error counter records code attribute - **WHEN** `FakeClock.schedule` throws `TESTING.CLOCK.MAX_TIMERS_EXCEEDED` - **THEN** the metric `testing.clock.schedule.errors` records an increment with attribute `code = "TESTING.CLOCK.MAX_TIMERS_EXCEEDED"` #### Scenario: Zero overhead without SDK - **WHEN** the benchmark suite runs without any OTEL SDK registered - **THEN** the difference between the OTEL-wrapped public entrypoint and an identical unwrapped baseline MUST be less than 5% per op ### Requirement: Coverage and docs gates Istanbul statement + branch coverage across `packages/universal/testing/src/` (excluding `*.bench.ts` and `*.schemas.ts`) MUST be ≥ 95%. 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 coverage is ≥ 95% AND branch coverage is ≥ 95% #### 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 ### Requirement: No disallowed constructs in `src/` Files under `packages/universal/testing/src/` MUST NOT contain the `any` keyword, `@ts-ignore`, `@ts-expect-error`, `TODO`, `FIXME`, `@deprecated`, `it.skip`, `describe.skip`, `test.skip`, `.only`, or any mock/stub of runtime behaviour intended for production code paths. #### Scenario: Biome scan clean - **WHEN** `bunx biome check src/` runs with the project's strict config - **THEN** the scan reports zero `noExplicitAny` diagnostics AND zero suppressed directives #### Scenario: Grep guard - **WHEN** `grep -rnE "TODO|FIXME|@deprecated|\\.skip\\(|\\.only\\(|ts-ignore|ts-expect-error" src/` runs - **THEN** zero matches are produced ### Requirement: `sideEffects: false` and subpath exports The package MUST declare `"sideEffects": false` in `package.json` AND provide explicit subpath exports for every module intended as a distinct import surface. A tree-shake test MUST prove that importing one submodule does not pull unrelated submodules. #### Scenario: Tree-shake test passes - **WHEN** a test imports only `FakeClock` from `@suaveplan/testing` and reads the resolved bundle text - **THEN** the bundle MUST NOT contain identifiers unique to `RequestBuilder`, `MemoryStore`, `FixtureManager`, or `Factory` ### Requirement: Property-based coverage for pure helpers Pure functions and deterministic state machines in the package (clock arithmetic, builder state transitions, factory deterministic output, memory-store TTL expiry, wait-for polling) SHALL have fast-check property tests covering round-trip, idempotency, associativity, and boundary invariants as applicable. #### Scenario: Builder composition is associative - **WHEN** fast-check runs over arbitrary RequestBuilder presets `a`, `b`, `c` applied in different groupings - **THEN** `(a then b) then c` produces the same final state as `a then (b then c)` #### Scenario: FakeClock advance + schedule commutes within a tick - **WHEN** fast-check runs over sequences of `advance` and `schedule` calls that never cross a timer threshold - **THEN** reordering within a single synchronous batch produces the same observed state ### Requirement: Benchmark gate in CI A `mitata` benchmark suite under `src/__benchmarks__/` SHALL record baselines in `benchmarks/baselines.json`. CI SHALL rerun the benchmarks (3-strike, take fastest), compare to baselines, and fail if any op regresses by more than 10%. Baseline updates MUST land on a dedicated, reviewer-approved commit separate from functional changes. #### Scenario: Regression gate trips - **WHEN** a PR introduces a 15% slowdown on `Factory.build` - **THEN** the CI benchmark step fails the build #### Scenario: Baseline update is intentional - **WHEN** a maintainer intentionally updates `benchmarks/baselines.json` - **THEN** the baseline commit is separate from functional code changes and is reviewed before landing ### Requirement: Plugin surface on FixtureManager `FixtureManager` SHALL accept plugins implementing `{ name: string; beforeEach?; afterEach?; onError? }` and invoke their hooks around each fixture's own setup/teardown lifecycle. Plugins MUST NOT be able to cancel a teardown mid-flight except via `onError`. #### Scenario: Plugin hook fires around each fixture - **WHEN** a `FixtureManager` with a plugin registered runs `setupAll` over two fixtures - **THEN** the plugin's `beforeEach` is called exactly twice in fixture-registration order AND `afterEach` is called exactly twice in reverse order ### Requirement: `createTestConfig()` absorbs WSL2 pool and polyfill concerns The `@suaveplan/testing/config` factory SHALL produce a Vitest config with `pool: "forks"`, `setupFiles: ["@suaveplan/testing/polyfills"]`, and — when the caller requests `happy-dom` — `fileParallelism: false` AND dynamically-loaded `@vitejs/plugin-react` if that optional peer is installed. When the caller's environment requests bun-test, the factory SHALL return an equivalent bun-test config instead. #### Scenario: Happy-dom caller gets React plugin - **WHEN** `createTestConfig({ environment: "happy-dom" })` is awaited and `@vitejs/plugin-react` is installed - **THEN** the returned config includes the React plugin in its `plugins` array AND `pool === "forks"` AND `setupFiles` includes the polyfills path #### Scenario: bun-test caller gets bun config - **WHEN** `createTestConfig({ runner: "bun:test" })` is awaited - **THEN** the returned value is a `BunTestConfig` with `pool` not set (bun-test has no concept of pool) AND any warnings about missing preload hooks are logged once