Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
# TestEnvironment
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`TestEnvironment` is the orchestrator for integration test suites that need several external services running concurrently. It accepts a named map of `ServiceFixture` instances, starts them all in parallel during `setup()`, records which ones failed to start, and stops the successful ones in reverse order during `teardown()`. This reverse-teardown order prevents cascading failures in services that depend on each other (e.g. stopping a connection pool before closing its database).
## Features
- **Parallel startup** — all fixtures are started with `Promise.allSettled`, so a slow service does not block faster ones.
- **Graceful failure handling** — if a fixture's `start()` throws, its name is added to the `failed` list rather than propagating the error; tests can then use `skipIfUnavailable` to opt out.
- **Reverse-order teardown** — fixtures are stopped in the reverse of their start-success order, respecting implicit dependency chains.
- **Swallowed stop errors** — `stop()` errors are caught and discarded so a single broken fixture does not prevent the rest from cleaning up.
- **Safe empty teardown** — calling `teardown()` before `setup()` (or when all fixtures failed) is a no-op.
- **Protected internals** — `fixtures`, `options`, and `started` are `protected` to allow subclassing.
## Usage: Basic Example
```ts
import { TestEnvironment } from "@suaveplan/testing-integration/environment/test-environment";
const env = new TestEnvironment({
redis: redisFixture,
postgres: postgresFixture,
});
const { configs, failed } = await env.setup();
if (failed.length > 0) {
console.warn("Some fixtures failed to start:", failed);
}
console.log(configs.redis); // { url: "redis://localhost:6380" }
console.log(configs.postgres); // { host: "localhost", port: 5433 }
await env.teardown();
```
## Usage: Advanced Example
Using `TestEnvironment` with `skipIfUnavailable` via the global-setup bridge:
```ts
// vitest.config.ts
import { createIntegrationSetup } from "@suaveplan/testing-integration/setup/create-integration-setup";
import { redisFixture } from "./fixtures/redis.js";
import { postgresFixture } from "./fixtures/postgres.js";
const { globalSetup, inject } = createIntegrationSetup({
redis: redisFixture,
postgres: postgresFixture,
});
export default defineConfig({
test: {
globalSetup: [globalSetup],
},
});
// my.test.ts
import { beforeEach } from "vitest";
import { skipIfUnavailable } from "@suaveplan/testing-integration/helpers/skip-if-unavailable";
beforeEach(skipIfUnavailable("redis"));
it("stores a value in redis", async () => {
// guaranteed: redis was available when setup ran
});
```
## API Reference
### `TestEnvironment`
**Constructor:** `new TestEnvironment(fixtures, options?)`
| Parameter | Type | Description |
|---|---|---|
| `fixtures` | `Record<string, ServiceFixture<…>>` | Named map of fixtures to manage. |
| `options` | `TestEnvironmentOptions` | Optional configuration (currently `timeoutMs` reserved). |
**Methods:**
| Method | Signature | Description |
|---|---|---|
| `setup` | `() => Promise<TestEnvironmentSetupResult>` | Starts all fixtures in parallel. Returns configs and failed list. |
| `teardown` | `() => Promise<void>` | Stops all started fixtures in reverse order. |
### `TestEnvironmentSetupResult`
| Property | Type | Description |
|---|---|---|
| `configs` | `Record<string, Record<string, unknown>>` | Config objects keyed by fixture key for successful fixtures. |
| `failed` | `string[]` | Names of fixtures whose `start()` threw. |
### `TestEnvironmentOptions`
| Property | Type | Description |
|---|---|---|
| `timeoutMs` | `number?` | Reserved for future per-fixture start timeout. |
### `TestEnvironmentError`
Re-exported from `health-poller`. Use `instanceof TestEnvironmentError` to discriminate environment errors.
## Implementation Notes
`Promise.allSettled` is used instead of `Promise.all` so that one failing fixture does not prevent others from starting. The index of each settled result maps 1:1 with the `entries` array, so the correct fixture name can be attributed to each failure.
The `started` array tracks fixture **names** (not keys) in insertion order. During teardown, names are reversed and matched against `Object.values(this.fixtures)` by name. This means duplicate names across different keys would cause a teardown ambiguity — callers should ensure fixture names are unique within a `TestEnvironment` instance.

View File

@@ -0,0 +1,101 @@
# ServiceFixture
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
`ServiceFixture<TConfig>` is the foundational contract for integration test service fixtures in Suaveplan. It defines a three-phase lifecycle — `start`, `healthCheck`, and `stop` — that allows `TestEnvironment` to manage any number of external services (containers, mock servers, in-process stores, etc.) uniformly. By coding against this interface rather than concrete implementations, consumers can swap fixture strategies without changing test setup code.
## Features
- **Generic configuration output** — `start()` returns a caller-defined `TConfig` (e.g. `{ url: string; port: number }`) so consuming tests receive strongly-typed connection details.
- **Three-phase lifecycle** — explicit `start` / `stop` / `healthCheck` methods map directly to `TestEnvironment`'s parallel startup and reverse-order teardown algorithms.
- **`InferConfig<F>` utility type** — extracts the `TConfig` type parameter from any `ServiceFixture` implementation at compile time, enabling generic helper functions.
- **Zero runtime code** — the module is types-only; no JavaScript is emitted for this file, so it contributes nothing to bundle size.
- **Idempotent stop contract** — implementations are required to tolerate repeated `stop()` calls without throwing.
## Usage: Basic Example
```ts
import type { ServiceFixture } from "@suaveplan/testing-integration/fixture/service-fixture";
interface RedisConfig {
url: string;
port: number;
}
class MemoryRedisFixture implements ServiceFixture<RedisConfig> {
readonly name = "memory-redis";
async start(): Promise<RedisConfig> {
// spin up an in-process redis-compatible store
return { url: "redis://localhost", port: 6380 };
}
async stop(): Promise<void> {
// flush and close the store
}
async healthCheck(): Promise<boolean> {
return true; // in-process is always ready
}
}
```
## Usage: Advanced Example
Using `InferConfig` to write generic helpers that work with any fixture:
```ts
import type {
InferConfig,
ServiceFixture,
} from "@suaveplan/testing-integration/fixture/service-fixture";
interface PostgresConfig {
host: string;
port: number;
database: string;
}
interface PostgresFixture extends ServiceFixture<PostgresConfig> {
readonly runMigrations: () => Promise<void>;
}
// InferConfig pulls PostgresConfig out of PostgresFixture at compile time
type Config = InferConfig<PostgresFixture>; // { host: string; port: number; database: string }
function buildConnectionString<F extends ServiceFixture<Record<string, unknown>>>(
config: InferConfig<F> & { host: string; port: number },
): string {
return `${config.host}:${config.port}`;
}
```
## API Reference
### `ServiceFixture<TConfig>`
| Member | Type | Description |
|---|---|---|
| `name` | `readonly string` | Human-readable identifier used in error messages and the fixture registry. |
| `start()` | `() => Promise<TConfig>` | Starts the service and returns connection config. Called by `TestEnvironment.setup()`. |
| `stop()` | `() => Promise<void>` | Stops the service and releases resources. Must be idempotent. |
| `healthCheck()` | `() => Promise<boolean>` | Returns `true` when the service is ready; `false` otherwise. Must not throw. |
### `InferConfig<F>`
Extracts `TConfig` from `ServiceFixture<TConfig>`. Returns `never` for any type that is not a `ServiceFixture` subtype.
**Type parameters:**
- `F` — the concrete fixture type to inspect
## Implementation Notes
`TConfig` is constrained to `Record<string, unknown>` rather than a plain `object` to ensure callers always return a plain serialisable map — not a class instance or primitive. This constraint is enforced at the type level only; no runtime validation is performed here (validation lives in `TestEnvironment`).
The `healthCheck` method is intentionally separate from `start` to support fixtures that need to start a process first and then wait for it to become healthy (e.g. a Docker container). `HealthPoller` uses this method in a polling loop with configurable timeout and interval.
`InferConfig` is implemented as a conditional type (`F extends ServiceFixture<infer C> ? C : never`) rather than a mapped type to keep inference simple and to return `never` for non-fixture types — a common pattern when building generic test helpers.

View File

@@ -0,0 +1,104 @@
# HealthPoller
## Purpose
`HealthPoller` repeatedly calls a `ServiceFixture`'s `healthCheck()` method at a configurable interval until the service reports healthy or a timeout elapses. It is used internally by `TestEnvironment` during the setup phase, but can also be used standalone when you need fine-grained control over the polling strategy for a single fixture. The injectable `sleep` function makes it fully deterministic in unit tests — no real timers are required.
## Features
- **Configurable timeout** — `timeoutMs` (default 120 000 ms) controls the maximum wait window.
- **Configurable interval** — `intervalMs` (default 1 000 ms) controls the delay between successive probes.
- **Injectable sleep** — supply a fake `sleep` function in tests to avoid real delays and assert exact call counts.
- **Structured error** — throws `TestEnvironmentError` (not a generic `Error`) so callers can discriminate on the error type.
- **Zero dependencies** — relies only on `ServiceFixture` from the same package; no external runtime dependencies.
- **Protected members** — `timeoutMs`, `intervalMs`, and `sleep` are `protected` so subclasses can override behavior.
## Usage: Basic Example
```ts
import { HealthPoller } from "@suaveplan/testing-integration/helpers/health-poller";
import type { ServiceFixture } from "@suaveplan/testing-integration/fixture/service-fixture";
const poller = new HealthPoller({ timeoutMs: 30_000, intervalMs: 500 });
// myFixture.healthCheck() is called every 500 ms until true or 30 s elapses
await poller.poll(myFixture);
```
## Usage: Advanced Example
Inject a fake sleep function in tests so polling logic can be verified without real delays:
```ts
import { describe, it, expect } from "vitest";
import { HealthPoller, TestEnvironmentError } from "@suaveplan/testing-integration/helpers/health-poller";
import type { ServiceFixture } from "@suaveplan/testing-integration/fixture/service-fixture";
function makeFixture(responses: boolean[]): ServiceFixture<Record<string, unknown>> {
let i = 0;
return {
name: "db",
start: async () => ({}),
stop: async () => {},
healthCheck: async () => responses[i++] ?? false,
};
}
it("retries until healthy", async () => {
const sleepCalls: number[] = [];
const poller = new HealthPoller({
timeoutMs: 5_000,
intervalMs: 200,
sleep: async (ms) => { sleepCalls.push(ms); },
});
await poller.poll(makeFixture([false, false, true]));
expect(sleepCalls).toEqual([200, 200]);
});
it("throws TestEnvironmentError on timeout", async () => {
const poller = new HealthPoller({
timeoutMs: 400,
intervalMs: 200,
sleep: async () => {},
});
await expect(poller.poll(makeFixture([]))).rejects.toBeInstanceOf(TestEnvironmentError);
});
```
## API Reference
### `HealthPoller`
**Constructor:** `new HealthPoller(options?: HealthPollerOptions)`
| Option | Type | Default | Description |
|---|---|---|---|
| `timeoutMs` | `number` | `120_000` | Maximum milliseconds to wait before throwing. |
| `intervalMs` | `number` | `1_000` | Milliseconds between `healthCheck()` calls. |
| `sleep` | `(ms: number) => Promise<void>` | `setTimeout`-based | Injectable sleep for deterministic testing. |
**Methods:**
| Method | Signature | Description |
|---|---|---|
| `poll` | `(fixture: ServiceFixture<…>) => Promise<void>` | Polls the fixture until healthy or timeout. Throws `TestEnvironmentError` on timeout. |
### `TestEnvironmentError`
Extends `Error`. The `name` property is always `"TestEnvironmentError"`. Thrown by `HealthPoller.poll()` when the timeout elapses before the fixture becomes healthy.
### `HealthPollerOptions`
| Property | Type | Required | Description |
|---|---|---|---|
| `timeoutMs` | `number` | No | Maximum wait time in milliseconds. |
| `intervalMs` | `number` | No | Polling interval in milliseconds. |
| `sleep` | `(ms: number) => Promise<void>` | No | Sleep implementation (injectable for tests). |
## Implementation Notes
The polling loop tracks `elapsed` time by accumulating `intervalMs` on each iteration rather than reading a real wall clock. This makes the poller's behavior fully deterministic when a fake `sleep` is injected — the timeout fires exactly when `elapsed > timeoutMs`, regardless of real time. In production use (default `setTimeout` sleep), elapsed time approximates real time closely enough for integration test purposes.
The timeout check is performed *after* sleeping rather than before to avoid a race where the first call returns unhealthy but elapsed immediately hits the timeout. With the check after sleep, there is always at least one full `intervalMs` of wait before a timeout is declared. The strict `>` comparison (rather than `>=`) means a fixture whose response latency equals exactly `timeoutMs` still gets one final health check after its sleep window, ensuring the timeout budget is truly exhausted before giving up.

View File

@@ -0,0 +1,78 @@
# skipIfUnavailable
## Purpose
`skipIfUnavailable` returns a `beforeAll`-compatible callback that marks the current test as skipped when the named service fixture failed to start. It bridges the global-setup phase — where fixture health is determined — to individual test files, so a network problem with one service never causes unrelated tests to fail with cryptic connection errors. Instead, those tests are reported as skipped with a clear signal.
## Features
- **Vitest-native skip** — calls Vitest's built-in `ctx.skip()` so the test runner reports the test as skipped rather than failed.
- **No throw on absent key** — if the `__integration_failed__` provide value is `null` or `undefined` (e.g. when running outside a global-setup context), the helper is a no-op and all tests run normally.
- **Injectable test double** — `_createSkipIfUnavailableWithInject` exposes the internal factory with a replaceable `injectFn`, making the behaviour fully unit-testable without Vitest's global `provide`/`inject` infrastructure.
- **Zero dependencies** — relies only on `vitest`'s `inject` export, which is already a dev dependency of every test file.
- **Single responsibility** — does exactly one thing: check whether a fixture name appears in the failed list, and skip if so.
## Usage: Basic Example
```ts
import { beforeAll } from "vitest";
import { skipIfUnavailable } from "@suaveplan/testing-integration/helpers/skip-if-unavailable";
// Any test in this file is skipped when the "redis" fixture failed to start.
beforeAll(skipIfUnavailable("redis"));
```
## Usage: Advanced Example
Guarding multiple fixtures in one file, and composing with other `beforeAll` hooks:
```ts
import { beforeAll, describe } from "vitest";
import {
skipIfUnavailable,
_createSkipIfUnavailableWithInject,
} from "@suaveplan/testing-integration/helpers/skip-if-unavailable";
// Skip if either dependency is unavailable
beforeAll(skipIfUnavailable("redis"));
beforeAll(skipIfUnavailable("postgres"));
describe("integration suite", () => {
// Unit-testing the helper itself:
it("skips when fixture is in failed list", { timeout: 5000 }, () => {
const cb = _createSkipIfUnavailableWithInject("redis", () => ["redis"]);
const skipFn = vi.fn();
cb({ skip: skipFn });
expect(skipFn).toHaveBeenCalledOnce();
});
});
```
## API Reference
### `skipIfUnavailable(fixtureName: string)`
**Parameters:**
- `fixtureName: string` — the key of the fixture to check against the `__integration_failed__` list.
**Returns:** `(ctx: { skip: () => void }) => void` — a callback suitable for `beforeAll`.
**Behaviour:** reads `__integration_failed__` from Vitest's `inject`. If the array includes `fixtureName`, calls `ctx.skip()`. Otherwise does nothing.
### `_createSkipIfUnavailableWithInject(fixtureName, injectFn)`
Internal factory exposed for unit testing.
**Parameters:**
- `fixtureName: string` — fixture key to check.
- `injectFn: (key: string) => string[] | null | undefined` — replacement for `inject`.
**Returns:** `(ctx: { skip: () => void }) => void`
## Implementation Notes
The `__integration_failed__` key is a convention shared between `global-setup.ts` (which calls `provide("__integration_failed__", failed)`) and `skipIfUnavailable`. The double-underscore prefix signals that this key is an internal implementation detail of the `@suaveplan/testing-integration` package and should not be read directly by consumer code.
The `null ?? []` fallback on the injected value ensures the helper is safe to use even when Vitest's global-setup mechanism is not configured — for example, in unit test suites that import test utilities but do not configure `globalSetup`. In that scenario, `inject("__integration_failed__")` returns `undefined`, the fallback produces an empty array, and no tests are skipped.

View File

@@ -0,0 +1,49 @@
# `internal/create-integration-setup-internal`
## Purpose
This module exposes `createIntegrationSetupWithInject()` — the dependency-injected core of the public `createIntegrationSetup()` factory. Splitting the implementation into a wired core plus a thin public facade keeps the `vitest` runtime import isolated to one file (`../setup/create-integration-setup.ts`) and lets the unit tests cover the registration / inject hand-off without booting Vitest's global-setup machinery.
It is deliberately under `src/internal/` and not listed in `package.json` `exports`. Consumers MUST NOT import it; the public contract is `createIntegrationSetup()` from the package barrel. Tests in this package import this module directly because they own the implementation; downstream packages do not.
## Features
- Generic over the fixture map; preserves precise per-fixture config types end-to-end via `InferConfig`.
- Pluggable `injectFn` so unit tests can verify the indirection through Vitest's `inject` without importing `vitest` from the test file under inspection.
- Side-effect: mutates `fixtureRegistry` so the Vitest global-setup module sees the fixtures at boot.
## Usage: Basic Example
```ts
// PRIVATE — shown for documentation only. Use createIntegrationSetup() in app code.
import { createIntegrationSetupWithInject } from "./internal/create-integration-setup-internal.js";
const { globalSetup, inject } = createIntegrationSetupWithInject(
{ redis: redisFixture },
(key) => ({ redis: { url: "redis://injected" } })
);
```
## Usage: Advanced Example
The public `createIntegrationSetup` (in `../setup/create-integration-setup.ts`) is exactly this with Vitest's real `inject` wired in:
```ts
import { inject as vitestInject } from "vitest";
import { createIntegrationSetupWithInject } from "../internal/create-integration-setup-internal.js";
export function createIntegrationSetup<TFixtures>(fixtures: TFixtures) {
return createIntegrationSetupWithInject(
fixtures,
vitestInject as (key: string) => Record<string, Record<string, unknown>>
);
}
```
## API Reference
- `createIntegrationSetupWithInject<TFixtures>(fixtures, injectFn)` — returns `{ globalSetup: string; inject: <K extends keyof TFixtures>(key: K) => InferConfig<TFixtures[K]> }`. Writes every entry of `fixtures` into the shared `fixtureRegistry`.
## Implementation Notes
Resolves the global-setup path via `new URL("../setup/global-setup.js", import.meta.url).pathname`. This path is what consumers feed into `test.globalSetup` in their `vitest.config.ts`; using `import.meta.url` makes the reference robust to workspace layout differences.

View File

@@ -0,0 +1,45 @@
# `internal/fixture-registry`
## Purpose
This module exposes the module-level `fixtureRegistry` `Map<string, ServiceFixture<Record<string, unknown>>>` that bridges `createIntegrationSetup()` and the Vitest `global-setup.ts` entry point. It exists because Vitest's global-setup hook cannot accept arguments across the configuration boundary — so the registration step (called from `vitest.config.ts`) and the consumption step (called by Vitest before any test file loads) must communicate via a shared, in-process singleton.
The registry is deliberately placed under `src/internal/` and is **not** listed in `package.json` `exports`. It is private build-system plumbing. Consumers MUST NOT import it; doing so means relying on an unstable surface that the package may rearrange without a SemVer breaking change. The public surface remains `createIntegrationSetup()` + `globalSetup` + `inject()`; everything in `internal/` is implementation detail.
## Features
- Singleton `Map` keyed by user-chosen fixture names.
- Stores the `ServiceFixture<Record<string, unknown>>` instances; the variant fixtures (`@suaveplan/testing-integration-redis`, etc.) all conform to this contract.
- Mutated by `createIntegrationSetup()` (writes) and by `global-setup.ts` (reads + clear).
## Usage: Basic Example
```ts
// PRIVATE — internal/* is not a published subpath. Shown for documentation only.
import { fixtureRegistry } from "./internal/fixture-registry.js";
console.log(fixtureRegistry.size);
```
## Usage: Advanced Example
`global-setup.ts` snapshot the registry into a plain object so the `TestEnvironment` constructor receives a stable view, then clears it so re-runs in watch mode start fresh:
```ts
import { fixtureRegistry } from "./internal/fixture-registry.js";
import { TestEnvironment } from "../environment/test-environment.js";
const fixtures = Object.fromEntries(fixtureRegistry);
const env = new TestEnvironment(fixtures);
const { configs, failed } = await env.setup();
context.provide("__integration_configs__", configs);
context.provide("__integration_failed__", failed);
```
## API Reference
- `fixtureRegistry: Map<string, ServiceFixture<Record<string, unknown>>>` — mutable singleton, lifetime equal to the Node process. Reads and writes are not synchronised because Vitest serialises setup vs test execution.
## Implementation Notes
The registry uses `Record<string, unknown>` rather than the precise per-fixture config because the Vitest bridge erases the type at the cross-module boundary. The strongly-typed projection is restored at the `inject()` call site via the `InferConfig<TFixtures[K]>` mapping in `IntegrationSetupResult`.

View File

@@ -0,0 +1,41 @@
# `internal/skip-if-unavailable-internal`
## Purpose
This module exposes `createSkipIfUnavailableWithInject()` — the dependency-injected core of the public `skipIfUnavailable()` helper. Splitting the implementation lets the public façade (`../helpers/skip-if-unavailable.ts`) hold the only `import { inject } from "vitest"` runtime call site, while unit tests exercise this factory with a fake injector and verify the behaviour without booting Vitest's `provide`/`inject` infrastructure.
It lives under `src/internal/` and is deliberately omitted from `package.json` `exports`. Consumers MUST NOT import it. The public surface is `skipIfUnavailable()` from the package barrel.
## Features
- Generic over `TName extends string` so consumers using a typed `createIntegrationSetup` registry get key auto-completion and compile-time errors when they typo a fixture name.
- Pluggable `injectFn` so unit tests can pass a fake without `vitest`.
- Treats `null` / `undefined` returns from `injectFn` as the empty failed-list (no skip).
## Usage: Basic Example
```ts
// PRIVATE — shown for documentation only. Use skipIfUnavailable() in app code.
import { createSkipIfUnavailableWithInject } from "./internal/skip-if-unavailable-internal.js";
const cb = createSkipIfUnavailableWithInject("redis", (key) => ["redis"]);
cb({ skip: () => console.log("skipped") });
```
## Usage: Advanced Example
The public `skipIfUnavailable()` uses Vitest's `inject` internally. When paired with a typed `createIntegrationSetup` registry, the generic preserves key safety:
```ts
import { skipIfUnavailable } from "@suaveplan/testing-integration/helpers/skip-if-unavailable";
beforeAll(skipIfUnavailable<"redis" | "postgres">("redis"));
// skipIfUnavailable<"redis" | "postgres">("typeo") // compile error — typo caught
```
## API Reference
- `createSkipIfUnavailableWithInject<TName extends string>(fixtureName, injectFn)` — returns `(ctx: { skip: () => void }) => void`. The returned callback reads the failed-fixture list via `injectFn("__integration_failed__")` and calls `ctx.skip()` if `fixtureName` is in it.
## Implementation Notes
The cast `null as unknown as string[]` in tests verifies the `?? []` guard. Production callers never pass a function that returns `null`, but Vitest's `inject` typing is loose, so the guard is load-bearing in real-world consumers.

View File

@@ -0,0 +1,91 @@
# createIntegrationSetup
## Purpose
`createIntegrationSetup` is the primary entry point for wiring integration test fixtures into the Vitest global-setup lifecycle. It accepts a named map of `ServiceFixture` instances, registers them in a module-level registry that `global-setup.ts` reads at startup, and returns two things: the absolute path of the Vitest global-setup module (ready to drop into `test.globalSetup`) and a typed `inject` helper that test files use to retrieve fixture configs. This one-call configuration model means consumers never need to import from `global-setup.ts` directly.
## Features
- **Typed `inject` helper** — the return type of `inject(key)` is inferred from the fixture's `TConfig` type parameter via `InferConfig`, so no type casts are needed in test files.
- **Auto-resolved `globalSetup` path** — the path is derived from `import.meta.url`, so it works regardless of where the consumer's `vitest.config.ts` lives.
- **Additive registry** — multiple calls to `createIntegrationSetup` accumulate fixtures into a shared registry; this allows split configuration across multiple config files.
- **Injectable test double** — `createIntegrationSetupWithInject` (under the package-private `internal/` directory) exposes the factory with a replaceable `injectFn` for unit testing without real Vitest `provide`/`inject` infrastructure.
- **Zero runtime overhead** — the function does no async work; startup cost is deferred entirely to `global-setup.ts`.
## Usage: Basic Example
```ts
// vitest.config.ts
import { defineConfig } from "vitest/config";
import { createIntegrationSetup } from "@suaveplan/testing-integration/setup/create-integration-setup";
import { redisFixture } from "./fixtures/redis.js";
const { globalSetup, inject } = createIntegrationSetup({ redis: redisFixture });
export default defineConfig({
test: {
globalSetup: [globalSetup],
},
});
export { inject };
// redis.test.ts
import { inject } from "../vitest.config.js";
import { beforeEach } from "vitest";
import { skipIfUnavailable } from "@suaveplan/testing-integration/helpers/skip-if-unavailable";
beforeEach(skipIfUnavailable("redis"));
it("connects to redis", async () => {
const { url } = inject("redis"); // typed as RedisConfig
// ...
});
```
## Usage: Advanced Example
Accumulating fixtures across multiple config calls:
```ts
// vitest.config.ts
import { createIntegrationSetup } from "@suaveplan/testing-integration/setup/create-integration-setup";
// Split fixture definitions across modules
const { globalSetup: gsA } = createIntegrationSetup({ redis: redisFixture });
const { globalSetup: gsB, inject } = createIntegrationSetup({ postgres: postgresFixture });
// Both fixture sets are registered in the shared package-private fixtureRegistry
export default defineConfig({
test: {
globalSetup: [gsA], // only one path needed — registry is shared
},
});
```
## API Reference
### `createIntegrationSetup(fixtures)`
**Parameters:**
- `fixtures: Record<string, ServiceFixture<…>>` — named map of fixtures to register.
**Returns:** `IntegrationSetupResult<TFixtures>`
### `IntegrationSetupResult<TFixtures>`
| Property | Type | Description |
|---|---|---|
| `globalSetup` | `string` | Absolute path to `global-setup.js`. Add to `test.globalSetup`. |
| `inject` | `<K extends keyof TFixtures>(key: K) => InferConfig<TFixtures[K]>` | Typed accessor for fixture configs populated by global-setup. |
### Package-private internals
`createIntegrationSetupWithInject(fixtures, injectFn)` and the `fixtureRegistry` `Map` live under `src/internal/` and are deliberately NOT exported via `package.json` `exports`. They are reachable only from within this package's own source tree (used by tests and by the public façade above) and consumers MUST NOT rely on them.
## Implementation Notes
The `globalSetup` path is computed via `new URL("./global-setup.js", import.meta.url).pathname`. This resolves relative to the compiled output location so the path is always valid after `vite build`. It does not depend on `__dirname` or any Node.js path resolution, making it compatible with ES module environments.
The `inject` closure captures `injectFn` at creation time and calls it lazily — only when `inject(key)` is invoked inside a test. This lazy evaluation is necessary because Vitest's `inject` only returns values after the global-setup `provide` calls have run.

View File

@@ -0,0 +1,73 @@
# global-setup
## Purpose
`global-setup.ts` is the Vitest global-setup module that bridges the fixture registry populated by `createIntegrationSetup` with the test environment. It exports `setup()` and `teardown()` lifecycle hooks that Vitest calls before and after the test suite. During `setup()`, it starts all registered fixtures via `TestEnvironment` and makes their configs and failure list available to every test file through Vitest's `provide`/`inject` mechanism. This design means consumers never need to manage container startup/teardown in individual test files.
## Features
- **Automatic fixture orchestration** — reads from `_fixtureRegistry` (populated by `createIntegrationSetup`) and delegates to `TestEnvironment` for parallel startup and reverse-order teardown.
- **Vitest `provide` bridge** — calls `provide("__integration_configs__", configs)` and `provide("__integration_failed__", failed)` so test files can access this data via `inject()` or the `skipIfUnavailable` helper.
- **Graceful partial failure** — uses `TestEnvironment`'s `Promise.allSettled`-based startup so a single failing fixture does not abort the entire suite.
- **Safe teardown** — wraps `env?.teardown()` so teardown is a no-op if `setup()` never ran (e.g. in degenerate configurations).
- **Excluded from library build** — listed in `tsconfig.lib.json`'s `exclude` array and the Vite `external` list so it does not appear in the distributed package.
## Usage: Basic Example
```ts
// vitest.config.ts
import { createIntegrationSetup } from "@suaveplan/testing-integration/setup/create-integration-setup";
import { redisFixture } from "./fixtures/redis.js";
const { globalSetup } = createIntegrationSetup({ redis: redisFixture });
export default {
test: {
globalSetup: [globalSetup], // <-- points to global-setup.js in the dist
},
};
```
## Usage: Advanced Example
The module is used indirectly via `createIntegrationSetup` in almost all cases. However, if you need to import it directly (e.g. for a custom global-setup wrapper), you can do so:
```ts
// custom-global-setup.ts
import { setup as integrationSetup, teardown as integrationTeardown } from "@suaveplan/testing-integration/setup/global-setup";
import { createIntegrationSetup } from "@suaveplan/testing-integration/setup/create-integration-setup";
import { redisFixture } from "./fixtures/redis.js";
createIntegrationSetup({ redis: redisFixture });
export async function setup() {
await integrationSetup();
// additional custom setup...
}
export async function teardown() {
await integrationTeardown();
// additional custom teardown...
}
```
## API Reference
### `setup(): Promise<void>`
Called by Vitest at the start of the test run.
Reads all fixtures from `_fixtureRegistry`, constructs a `TestEnvironment`, calls `setup()`, then:
- Calls `provide("__integration_configs__", configs)` — map of fixture key → config object.
- Calls `provide("__integration_failed__", failed)` — array of fixture names that failed to start.
### `teardown(): Promise<void>`
Called by Vitest at the end of the test run. Delegates to `env.teardown()` if `env` was created. Safe to call when `setup()` was never called.
## Implementation Notes
The `env` variable is module-scoped rather than exported because Vitest loads global-setup modules as ES modules and re-uses the module instance between `setup` and `teardown` calls. This ensures `teardown` always references the same `TestEnvironment` that `setup` created.
This file is intentionally excluded from `tsconfig.lib.json` because it imports `vitest/node` (a server-only Vitest API), which should not appear in a library's type definitions. It is still built by Vite as a separate output module because the Rollup `external` list excludes `vitest/node`, allowing it to be bundled independently and resolved at runtime.