@jterrazz/test 8.0.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +209 -230
  2. package/dist/checker.d.ts +1 -0
  3. package/dist/checker.js +741 -0
  4. package/dist/index.d.ts +1249 -728
  5. package/dist/index.js +3133 -1538
  6. package/dist/intercept.js +129 -299
  7. package/dist/match.js +153 -0
  8. package/dist/oxlint.cjs +2931 -0
  9. package/dist/oxlint.d.cts +150 -0
  10. package/dist/oxlint.d.ts +150 -0
  11. package/dist/oxlint.js +2925 -0
  12. package/package.json +47 -41
  13. package/dist/chunk.cjs +0 -28
  14. package/dist/index.cjs +0 -2264
  15. package/dist/index.cjs.map +0 -1
  16. package/dist/index.d.cts +0 -980
  17. package/dist/index.js.map +0 -1
  18. package/dist/intercept.cjs +0 -323
  19. package/dist/intercept.cjs.map +0 -1
  20. package/dist/intercept.d.cts +0 -115
  21. package/dist/intercept.d.ts +0 -115
  22. package/dist/intercept.js.map +0 -1
  23. package/dist/intercept2.cjs +0 -81
  24. package/dist/intercept2.cjs.map +0 -1
  25. package/dist/intercept2.js +0 -81
  26. package/dist/intercept2.js.map +0 -1
  27. package/dist/mock-of.cjs +0 -32
  28. package/dist/mock-of.cjs.map +0 -1
  29. package/dist/mock-of.d.cts +0 -27
  30. package/dist/mock-of.d.ts +0 -27
  31. package/dist/mock-of.js +0 -19
  32. package/dist/mock-of.js.map +0 -1
  33. package/dist/mock.cjs +0 -4
  34. package/dist/mock.d.cts +0 -2
  35. package/dist/mock.d.ts +0 -2
  36. package/dist/mock.js +0 -2
  37. package/dist/services.cjs +0 -5
  38. package/dist/services.d.cts +0 -2
  39. package/dist/services.d.ts +0 -2
  40. package/dist/services.js +0 -2
  41. package/dist/sqlite.cjs +0 -350
  42. package/dist/sqlite.cjs.map +0 -1
  43. package/dist/sqlite.d.cts +0 -209
  44. package/dist/sqlite.d.ts +0 -209
  45. package/dist/sqlite.js +0 -331
  46. package/dist/sqlite.js.map +0 -1
  47. package/dist/types.d.cts +0 -42
  48. package/dist/types.d.ts +0 -42
package/dist/index.d.ts CHANGED
@@ -1,154 +1,357 @@
1
- import { a as PostgresOptions, c as IsolationStrategy, i as redis, l as DatabasePort, n as sqlite, o as postgres, r as RedisOptions, s as ServiceHandle, t as SqliteOptions } from "./sqlite.js";
2
- import { n as InterceptResponse, r as InterceptTrigger } from "./types.js";
3
- import { i as mockOfDate, n as mockOf, r as MockDatePort, t as MockPort } from "./mock-of.js";
4
-
5
- //#region src/infra/docker/docker.port.d.ts
6
- /** Abstract interface for interacting with a Docker container (exec, inspect, file I/O). */
7
- interface DockerContainerPort {
8
- /** Execute a command inside the container, return stdout */
9
- exec(cmd: string[]): Promise<string>;
10
- /** Read a file from inside the container */
11
- file(path: string): Promise<{
12
- exists: boolean;
13
- content: string;
14
- }>;
15
- /** Check if container is running */
16
- isRunning(): Promise<boolean>;
17
- /** Get container logs */
18
- logs(tail?: number): Promise<string>;
19
- /** Get full docker inspect JSON */
20
- inspect(): Promise<DockerInspectResult>;
21
- /** Check if a file/directory exists */
22
- exists(path: string): Promise<boolean>;
23
- }
24
- /** Normalized subset of `docker inspect` output for assertion use. */
25
- interface DockerInspectResult {
26
- id: string;
27
- name: string;
28
- state: {
29
- running: boolean;
30
- exitCode: number;
31
- status: string;
32
- };
33
- config: {
34
- image: string;
35
- env: string[];
36
- };
37
- hostConfig: {
38
- memory: number;
39
- cpuQuota: number;
40
- networkMode: string;
41
- mounts: Array<{
42
- source: string;
43
- destination: string;
44
- type: string;
45
- }>;
46
- };
47
- networkSettings: {
48
- networks: Record<string, {
49
- gateway: string;
50
- ipAddress: string;
51
- }>;
52
- };
53
- }
54
- //#endregion
55
- //#region src/infra/docker/docker-assertion.d.ts
56
- /**
57
- * Fluent assertion builder for Docker containers.
58
- * Chain async assertions like `toBeRunning()`, `toHaveFile()`, `toHaveMount()`.
59
- */
60
- declare class DockerAssertion {
61
- private container;
62
- constructor(container: DockerContainerPort);
63
- /** Assert the container is running */
64
- toBeRunning(): Promise<this>;
65
- /** Assert the container is NOT running / doesn't exist */
66
- toNotExist(): Promise<this>;
67
- /** Assert a file exists inside the container */
68
- toHaveFile(path: string, opts?: {
69
- containing?: string;
70
- }): Promise<this>;
71
- /** Assert a file does NOT exist */
72
- toNotHaveFile(path: string): Promise<this>;
73
- /** Assert a directory exists */
74
- toHaveDirectory(path: string): Promise<this>;
75
- /** Assert a mount exists */
76
- toHaveMount(destination: string): Promise<this>;
77
- /** Assert network mode */
78
- toHaveNetwork(mode: string): Promise<this>;
79
- /** Assert memory limit */
80
- toHaveMemoryLimit(bytes: number): Promise<this>;
81
- /** Assert CPU quota */
82
- toHaveCpuQuota(quota: number): Promise<this>;
83
- /** Execute a command and return output for custom assertions */
84
- exec(cmd: string[]): Promise<string>;
85
- /** Read a file for custom assertions */
86
- readFile(path: string): Promise<string>;
87
- /** Get logs for custom assertions */
88
- getLogs(tail?: number): Promise<string>;
89
- }
90
- //#endregion
91
- //#region src/infra/orchestrator.d.ts
92
- interface OrchestratorOptions {
93
- services: ServiceHandle[];
94
- mode: 'e2e' | 'integration';
95
- root?: string;
96
- /** Compose project name — used for per-worker stack isolation. */
97
- projectName?: string;
1
+ import { DeepMockProxy } from "vitest-mock-extended";
2
+ //#region src/core/matching/match.d.ts
3
+ /**
4
+ * Code-side dynamic-value matchers (`match.*`) and the capture scope shared
5
+ * by every assertion of a single spec execution.
6
+ *
7
+ * Matchers are used inside `toMatchRows` rows and any structural comparison;
8
+ * their file-side twins are the `{{placeholder}}` forms in `expected/*.http`
9
+ * fixtures (body AND headers), `expected/*.json` files, and text snapshots
10
+ * (`expected/*.txt`). One grammar, one vocabulary (CONVENTIONS D4).
11
+ */
12
+ /** The frozen token vocabulary (CONVENTIONS D4) plus the code-only kinds. */
13
+ type MatcherKind = 'any' | 'base64' | 'date' | 'duration' | 'email' | 'float' | 'hex' | 'int' | 'ip' | 'iso8601' | 'number' | 'path' | 'port' | 'ref' | 'regex' | 'semver' | 'sha' | 'string' | 'time' | 'ulid' | 'url' | 'uuid' | 'workdir';
14
+ /**
15
+ * A dynamic-value matcher. Created via the {@link match} factories — never
16
+ * constructed directly by user code.
17
+ */
18
+ declare class Matcher {
19
+ readonly kind: MatcherKind;
20
+ /** For kind 'ref': the capture to assert inequality against. */
21
+ readonly notRef?: string;
22
+ /** For kind 'ref': the capture name. */
23
+ readonly refName?: string;
24
+ /** For kind 'regex': the pattern the actual value must match. */
25
+ readonly regex?: RegExp;
26
+ constructor(kind: MatcherKind, options?: {
27
+ notRef?: string;
28
+ refName?: string;
29
+ regex?: RegExp;
30
+ });
31
+ /** Placeholder-style rendering used in failure diffs and serialization. */
32
+ toString(): string;
33
+ toJSON(): string;
98
34
  }
99
35
  /**
100
- * Orchestrator for test infrastructure.
101
- * Integration: starts services via testcontainers.
102
- * E2E: runs full docker compose up.
36
+ * Dynamic-value matchers for structural comparisons — the code-side mirror of
37
+ * the `{{token}}` fixture grammar (CONVENTIONS D4).
38
+ *
39
+ * @example
40
+ * await expect(result.table('users', { database: 'db' })).toMatchRows({
41
+ * columns: ['id', 'name'],
42
+ * rows: [[match.uuid(), 'Alice']],
43
+ * });
103
44
  */
104
- declare class Orchestrator {
105
- private services;
106
- private mode;
107
- private root;
108
- private projectName;
109
- private running;
110
- private composeStack;
111
- private composeHandles;
112
- private started;
113
- constructor(options: OrchestratorOptions);
114
- /**
115
- * Start declared services via testcontainers (integration mode).
116
- * Phase 1: start all containers in parallel (the slow part).
117
- * Phase 2: wire connections, healthcheck, and init sequentially (fast).
118
- */
119
- start(): Promise<void>;
120
- /**
121
- * Stop testcontainers (integration mode).
122
- */
123
- stop(): Promise<void>;
124
- /**
125
- * Start full docker compose stack (e2e mode).
126
- * Auto-detects infra services and creates handles for them.
127
- */
128
- startCompose(): Promise<void>;
45
+ declare const match: {
46
+ /** Matches anything. */
47
+ any: () => Matcher;
48
+ /** Matches a base64 string. */
49
+ base64: () => Matcher;
50
+ /** Matches a calendar date (`YYYY-MM-DD`). */
51
+ date: () => Matcher;
52
+ /** Matches a human duration (`12ms`, `1.5s`, `2m`, `3h`). */
53
+ duration: () => Matcher;
54
+ /** Matches an email address. */
55
+ email: () => Matcher;
129
56
  /**
130
- * Stop docker compose stack (e2e mode).
57
+ * Matches a float. In JSON contexts any finite number passes (JSON does
58
+ * not distinguish `42` from `42.0`); in text contexts the decimal part
59
+ * is required (`4.2`, never `42`).
131
60
  */
132
- stopCompose(): Promise<void>;
61
+ float: () => Matcher;
62
+ /** Matches a hexadecimal string. */
63
+ hex: () => Matcher;
64
+ /** Matches an integer (or an integer string in text contexts). */
65
+ int: () => Matcher;
66
+ /** Matches an IPv4 address. */
67
+ ip: () => Matcher;
68
+ /** Matches an ISO-8601 timestamp string. */
69
+ iso8601: () => Matcher;
70
+ /** Matches a number (or a numeric string in text contexts). */
71
+ number: () => Matcher;
72
+ /** Matches a filesystem path (`/...` or `./...`). */
73
+ path: () => Matcher;
74
+ /** Matches a TCP/UDP port number (0-65535). */
75
+ port: () => Matcher;
133
76
  /**
134
- * Get a database service by compose name, or the first one if no name given.
77
+ * Capture-and-compare. The first occurrence of `ref(name)` captures the
78
+ * actual value; every later occurrence must strictly equal the capture.
79
+ * `{ not: other }` additionally asserts inequality with the capture named
80
+ * `other`. Scope: the current spec execution (reset per chain).
135
81
  */
136
- getDatabase(serviceName?: string): DatabasePort | null;
82
+ ref: (name: string, options?: {
83
+ not?: string;
84
+ }) => Matcher;
85
+ /** Matches a string against the given regular expression. */
86
+ regex: (regex: RegExp) => Matcher;
87
+ /** Matches a semantic version (`1.2.3`, `2.0.0-rc.1`). */
88
+ semver: () => Matcher;
89
+ /** Matches a git SHA (7-64 hex chars). */
90
+ sha: () => Matcher;
91
+ /** Matches any string. */
92
+ string: () => Matcher;
93
+ /** Matches a wall-clock time (`HH:MM` or `HH:MM:SS`). */
94
+ time: () => Matcher;
95
+ /** Matches a ULID. */
96
+ ulid: () => Matcher;
97
+ /** Matches an http(s) URL. */
98
+ url: () => Matcher;
99
+ /** Matches a UUID string. */
100
+ uuid: () => Matcher;
101
+ /** Matches the exact working directory of the current spec. */
102
+ workdir: () => Matcher;
103
+ };
104
+ /**
105
+ * Named captures recorded by `match.ref()` / `{{type#ref}}` placeholders.
106
+ * One scope lives on each spec result — every assertion chained off the same
107
+ * result shares it, and a new chain starts fresh.
108
+ */
109
+ declare class CaptureScope {
110
+ private readonly values;
137
111
  /**
138
- * Get all database services keyed by compose name.
112
+ * The exact working directory of the current spec, when the framework
113
+ * knows it (cli mode). Drives the `{{workdir}}` token / `match.workdir()`.
139
114
  */
140
- getDatabases(): Map<string, DatabasePort>;
115
+ workdir?: string;
116
+ constructor(workdir?: string);
117
+ get(name: string): unknown;
118
+ has(name: string): boolean;
119
+ set(name: string, value: unknown): void;
120
+ }
121
+ //#endregion
122
+ //#region src/core/specification/shared/result/directory.d.ts
123
+ /**
124
+ * Read-only accessor for a directory produced by a spec action.
125
+ *
126
+ * Assertions go through `expect()` (async — they walk the disk):
127
+ * `await expect(result.directory('out')).toMatch('scaffold/out')`
128
+ * compares the tree against the fixture directory `expected/<name>/`.
129
+ */
130
+ declare class DirectoryAccessor {
131
+ /** @internal Ref-capture scope shared by the current spec execution. */
132
+ readonly captures: CaptureScope;
133
+ /** @internal Absolute path of the directory under assertion. */
134
+ readonly root: string;
135
+ /** @internal Test-file directory — fixture resolution root for matchers. */
136
+ readonly testDir: string;
137
+ constructor(absPath: string, testDir: string, captures?: CaptureScope);
138
+ /** List all files (recursively) under the directory, sorted. */
139
+ files(options?: {
140
+ ignore?: string[];
141
+ }): Promise<string[]>;
142
+ }
143
+ //#endregion
144
+ //#region src/core/specification/shared/result/filesystem.d.ts
145
+ /**
146
+ * Read-only accessor for the whole temporary working directory used by a
147
+ * command spec.
148
+ *
149
+ * Assertions go through `expect()` (async — they walk the disk):
150
+ * `await expect(result.filesystem).toMatch('scaffolded')` compares the
151
+ * tree against the fixture directory `expected/<name>/`.
152
+ */
153
+ declare class FilesystemAccessor {
154
+ /** @internal Ref-capture scope shared by the current spec execution. */
155
+ readonly captures: CaptureScope;
156
+ /** The absolute path of the temporary working directory. */
157
+ readonly cwd: string;
158
+ /** @internal Test-file directory — fixture resolution root for matchers. */
159
+ readonly testDir: string;
160
+ constructor(cwd: string, testDir: string, captures?: CaptureScope);
161
+ /** List all files (recursively) under the working directory, sorted. */
162
+ files(options?: {
163
+ ignore?: string[];
164
+ }): Promise<string[]>;
165
+ }
166
+ //#endregion
167
+ //#region src/core/specification/shared/result/json.d.ts
168
+ /**
169
+ * Read-only accessor for a JSON payload parsed from a text stream (stdout).
170
+ *
171
+ * Lazily parses the JSON on first use; parse errors are deferred until the
172
+ * value is read so that callers can still read the raw stream text without
173
+ * triggering a throw.
174
+ *
175
+ * ANSI escape sequences are stripped by default before `JSON.parse`
176
+ * (CONVENTIONS D6). When a `transform` is configured on the spec runner, it
177
+ * runs on the stripped text — an escape hatch for application noise not
178
+ * covered by the `{{token}}` grammar.
179
+ *
180
+ * Assertions go through `expect()`: `expect(result.json).toMatch('name.json')`
181
+ * (deep-equal against `expected/<name>`, with `{{placeholder}}` support).
182
+ */
183
+ declare class JsonAccessor {
184
+ /** @internal Ref-capture scope shared by the current spec execution. */
185
+ readonly captures: CaptureScope;
186
+ /** @internal */
187
+ private readonly rawText;
188
+ /** @internal Test-file directory — fixture resolution root for matchers. */
189
+ readonly testDir: string;
190
+ /** @internal */
191
+ private readonly transform?;
192
+ constructor(rawText: string, testDir: string, transform?: (text: string) => string, captures?: CaptureScope);
193
+ /** The parsed JSON value. Throws if the text is not valid JSON. */
194
+ get value(): unknown;
195
+ }
196
+ //#endregion
197
+ //#region src/core/ports/server.port.d.ts
198
+ /**
199
+ * HTTP response returned by a server port, with parsed JSON body.
200
+ */
201
+ interface ServerResponse {
202
+ /** HTTP status code. */
203
+ status: number;
204
+ /** Parsed JSON response body, or null if parsing failed. */
205
+ body: unknown;
206
+ /** Response headers as a flat key-value map. */
207
+ headers: Record<string, string>;
208
+ }
209
+ /**
210
+ * Abstract server interface for specification runners.
211
+ * Integration mode uses an in-process Hono app; E2E mode uses real HTTP via fetch.
212
+ */
213
+ interface ServerPort {
214
+ /** Send an HTTP request and return the parsed response. */
215
+ request: (method: string, path: string, body?: unknown, headers?: Record<string, string>) => Promise<ServerResponse>;
216
+ }
217
+ //#endregion
218
+ //#region src/core/specification/shared/result/response.d.ts
219
+ /**
220
+ * Read-only accessor for an HTTP response.
221
+ *
222
+ * Assertions go through `expect()`:
223
+ * `expect(result.response).toMatch('created.http')` compares status,
224
+ * a subset of headers, and the JSON body against `expected/<name>` —
225
+ * with `{{placeholder}}` support in all three.
226
+ */
227
+ declare class ResponseAccessor {
228
+ /** The parsed JSON response body (null when parsing failed). */
229
+ readonly body: unknown;
230
+ /** @internal Ref-capture scope shared by the current spec execution. */
231
+ readonly captures: CaptureScope;
232
+ /** Response headers as a flat, lower-cased key-value map. */
233
+ readonly headers: Record<string, string>;
234
+ /** The HTTP response status code. */
235
+ readonly status: number;
236
+ /** @internal Test-file directory — fixture resolution root for matchers. */
237
+ readonly testDir: string;
238
+ constructor(response: ServerResponse, testDir: string, captures?: CaptureScope);
239
+ }
240
+ //#endregion
241
+ //#region src/core/ports/database.port.d.ts
242
+ /**
243
+ * Abstract database interface for specification runners.
244
+ * Implement this to plug in your database stack (e.g. Postgres, SQLite).
245
+ */
246
+ interface DatabasePort {
247
+ /** Execute raw SQL (for seeding test data). */
248
+ seed: (sql: string) => Promise<void>;
249
+ /** Query a table and return rows as arrays of values. */
250
+ query: (table: string, columns: string[]) => Promise<unknown[][]>;
251
+ /** Reset database to clean state between tests. */
252
+ reset: () => Promise<void>;
253
+ }
254
+ //#endregion
255
+ //#region src/core/specification/shared/result/table.d.ts
256
+ /**
257
+ * Read-only accessor for a database table after a specification run.
258
+ *
259
+ * Assertions go through `expect()` (async — they query the database):
260
+ *
261
+ * await expect(result.table('users', { database: 'db' })).toMatchRows({
262
+ * columns: ['name'],
263
+ * rows: [['Alice']],
264
+ * });
265
+ * await expect(result.table('users', { database: 'db' })).toBeEmpty();
266
+ */
267
+ declare class TableAccessor {
268
+ /** @internal Ref-capture scope shared by the current spec execution. */
269
+ readonly captures: CaptureScope;
270
+ /** @internal */
271
+ private readonly db;
272
+ /** The table name this accessor reads. */
273
+ readonly name: string;
274
+ constructor(name: string, db: DatabasePort, captures?: CaptureScope);
275
+ /** @internal Query the table — used by the toMatchRows / toBeEmpty matchers. */
276
+ query(columns: string[]): Promise<unknown[][]>;
277
+ }
278
+ //#endregion
279
+ //#region src/core/specification/shared/result/text.d.ts
280
+ /**
281
+ * Read-only accessor for a captured text handle — THE universal one: stdout,
282
+ * stderr, container logs, and file text all surface as a `TextAccessor`.
283
+ *
284
+ * Backed by a primitive string, exposed via {@link text}, but also provides
285
+ * `toString()` / `valueOf()` for string coercion, so common patterns like
286
+ * `String(result.stdout)` and template-literal interpolation still work.
287
+ *
288
+ * ANSI escape sequences are stripped by default before every comparison
289
+ * (CONVENTIONS D6) — `.text` stays raw. The runner `transform` option remains
290
+ * an escape hatch for application noise not covered by the `{{token}}`
291
+ * grammar.
292
+ *
293
+ * Text operations are **closed** over the type: `.grep(pattern)` returns a
294
+ * `TextAccessor` (not a bare string), preserving the `expected/`-resolution
295
+ * context and the `transform`, so results are chainable
296
+ * (`result.stdout.grep(a).grep(b)`) and snapshot-able
297
+ * (`expect(result.stdout.grep('users.ts')).toMatch('block.txt')`).
298
+ *
299
+ * Assertions go through `expect()` matchers: `expect(result.stdout).toContain(...)`
300
+ * and `expect(result.stdout).toMatch('name.txt')` (resolved against
301
+ * `expected/<name>`, flat — with `{{token}}` support, CONVENTIONS D4).
302
+ */
303
+ declare class TextAccessor {
304
+ /** @internal Ref-capture scope shared by the current spec execution. */
305
+ readonly captures: CaptureScope;
306
+ /** @internal Stream label used in failure messages ("stdout" / "stderr"). */
307
+ readonly streamName: string;
308
+ /** @internal Test-file directory — fixture resolution root for matchers. */
309
+ readonly testDir: string;
310
+ /** The raw captured text (never transformed, ANSI preserved). */
311
+ readonly text: string;
312
+ /** @internal Normaliser applied before comparisons, never to fixtures. */
313
+ readonly transform?: (text: string) => string;
314
+ constructor(value: string, streamName: string, testDir: string, options?: {
315
+ captures?: CaptureScope;
316
+ transform?: (text: string) => string;
317
+ });
318
+ /** @internal The text as compared by matchers — ANSI stripped, transform applied. */
319
+ get comparableText(): string;
141
320
  /**
142
- * Get app URL from compose (e2e mode).
321
+ * Keep only the blank-line-separated blocks of the text that contain
322
+ * `pattern` (how linter/compiler output is structured), returned as a new
323
+ * `TextAccessor` — the same subject type, so the result is chainable and
324
+ * snapshot-able. The scalpel for probing large tool outputs; snapshot the
325
+ * whole surface by default, reach for `.grep()` for targeted checks.
143
326
  */
144
- getAppUrl(): null | string;
327
+ grep(pattern: string): TextAccessor;
328
+ toString(): string;
329
+ valueOf(): string;
145
330
  }
331
+ /**
332
+ * Wrap an arbitrary string into a {@link TextAccessor} anchored on the calling
333
+ * test's directory — the same caller-detection the builders use.
334
+ *
335
+ * The product surface of a test framework is its own error messages, checker
336
+ * output, and reports; those deserve the same goldening as any other output.
337
+ * `text()` makes an ad-hoc string a first-class snapshot subject:
338
+ *
339
+ * ```typescript
340
+ * const message = await catchMessage(() => expect(result.response).toMatch('wrong-body.http'));
341
+ * expect(text(message)).toMatch('wrong-body-error.txt'); // resolves to expected/
342
+ * ```
343
+ *
344
+ * ANSI is stripped before every comparison (the raw form stays on `.text`),
345
+ * the `{{token}}` grammar applies to the fixture, and `.grep()` composition
346
+ * works exactly as on any stream accessor.
347
+ */
348
+ declare function text(value: string): TextAccessor;
146
349
  //#endregion
147
- //#region src/spec/modes/cli/command.port.d.ts
350
+ //#region src/core/ports/cli.port.d.ts
148
351
  /**
149
- * Result of executing a CLI command, including exit code and captured output streams.
352
+ * Raw output from a command execution, including exit code and captured output streams.
150
353
  */
151
- interface CommandResult {
354
+ interface CliOutput {
152
355
  /** Process exit code (0 = success). */
153
356
  exitCode: number;
154
357
  /** Captured standard output. */
@@ -157,139 +360,223 @@ interface CommandResult {
157
360
  stderr: string;
158
361
  }
159
362
  /**
160
- * Options for spawning a long-running process.
363
+ * Options for the long-running form of `.exec()` (CONVENTIONS B2). When
364
+ * either option is present the process is spawned and observed: it resolves
365
+ * as soon as `waitFor` appears in stdout/stderr, and is killed when `timeout`
366
+ * elapses (exit code 124).
161
367
  */
162
- interface SpawnOptions {
163
- /** Resolve when stdout/stderr contains this string. */
164
- waitFor: string;
165
- /** Kill the process after this many milliseconds. */
166
- timeout: number;
368
+ interface ExecOptions {
369
+ /** Resolve (exit code 0) when stdout/stderr contains this string. */
370
+ waitFor?: string;
371
+ /** Kill the process after this many milliseconds. Defaults to 10 000. */
372
+ timeout?: number;
167
373
  }
168
374
  /**
169
375
  * Extra environment variables to set for the child process.
170
376
  * Values are merged on top of process.env. A `null` value unsets the variable.
171
377
  */
172
- type CommandEnv = Record<string, null | string>;
378
+ type CliEnv = Record<string, null | string>;
173
379
  /**
174
- * Abstract CLI interface for specification runners.
380
+ * Abstract command interface for specification runners.
175
381
  * Implement this to plug in your command execution strategy.
176
382
  */
177
- interface CommandPort {
178
- /** Execute a CLI command with the given arguments in the given working directory. */
179
- exec(args: string, cwd: string, env?: CommandEnv): Promise<CommandResult>;
180
- /** Spawn a long-running process and wait for a pattern or timeout. */
181
- spawn(args: string, cwd: string, options: SpawnOptions, env?: CommandEnv): Promise<CommandResult>;
383
+ interface CliPort {
384
+ /** Execute a command with the given arguments in the given working directory. */
385
+ exec: (args: string, cwd: string, env?: CliEnv) => Promise<CliOutput>;
386
+ /** Run a long-running process and wait for a pattern or timeout. */
387
+ watch: (args: string, cwd: string, options: ExecOptions, env?: CliEnv) => Promise<CliOutput>;
182
388
  }
183
389
  //#endregion
184
- //#region src/spec/result/directory.d.ts
185
- interface DirectorySnapshotOptions {
186
- ignore?: string[];
187
- update?: boolean;
390
+ //#region src/core/contracts/types.d.ts
391
+ /**
392
+ * The observed outgoing request, reduced to what trigger matchers inspect.
393
+ * Built once per request by the MSW integration and handed to
394
+ * {@link InterceptTrigger.match}.
395
+ */
396
+ interface MatchableRequest {
397
+ /** Parsed JSON body when the payload is JSON, the raw text otherwise, or `null` when absent. */
398
+ body: unknown;
399
+ /** Request headers, keyed by lowercased header name. */
400
+ headers: Record<string, string>;
401
+ /** The fully-qualified request URL (including any query string). */
402
+ url: string;
188
403
  }
189
- declare class DirectoryAccessor {
190
- private absPath;
191
- private testDir;
192
- constructor(absPath: string, testDir: string);
193
- toMatchFixture(name: string, options?: DirectorySnapshotOptions): Promise<void>;
194
- files(options?: {
195
- ignore?: string[];
196
- }): Promise<string[]>;
404
+ /**
405
+ * An intercept trigger describes which HTTP request to match.
406
+ */
407
+ interface InterceptTrigger {
408
+ /** Adapter name - must match the folder prefix in file-based intercepts. */
409
+ adapter: string;
410
+ /** HTTP method to match. */
411
+ method: string;
412
+ /** URL pattern to match (string for exact prefix, RegExp for pattern). */
413
+ url: RegExp | string;
414
+ /** Optional request matcher - the handler only fires if this returns true. */
415
+ match?: (request: MatchableRequest) => boolean;
416
+ /**
417
+ * Transform raw JSON data into a provider-specific response envelope.
418
+ * Called when .intercept(trigger, 'adapter/file.json') loads a file.
419
+ */
420
+ wrap: (data: unknown) => InterceptResponse;
421
+ }
422
+ /**
423
+ * An intercept response describes what to return when the trigger matches.
424
+ */
425
+ interface InterceptResponse {
426
+ /** HTTP status code (default: 200). */
427
+ status?: number;
428
+ /** Response body (will be JSON.stringified). */
429
+ body: unknown;
430
+ /** Response headers. */
431
+ headers?: Record<string, string>;
432
+ /** Delay in ms before responding (for timeout testing). */
433
+ delay?: number;
434
+ }
435
+ /**
436
+ * A dynamic response: computed from the observed request at the moment the
437
+ * intercept is consumed, rather than fixed ahead of time. Handed the same
438
+ * {@link MatchableRequest} the trigger matched on, so the reply can echo or
439
+ * derive from the request body/headers/url.
440
+ */
441
+ type InterceptResponder = (request: MatchableRequest) => InterceptResponse;
442
+ /**
443
+ * What an intercept replies with: either a fixed {@link InterceptResponse} or
444
+ * an {@link InterceptResponder} evaluated per consumed request.
445
+ */
446
+ type InterceptResponseValue = InterceptResponder | InterceptResponse;
447
+ /**
448
+ * A fully resolved intercept entry ready to be registered with MSW.
449
+ */
450
+ interface InterceptEntry {
451
+ trigger: InterceptTrigger;
452
+ response: InterceptResponseValue;
197
453
  }
198
454
  //#endregion
199
- //#region src/spec/result/filesystem.d.ts
455
+ //#region src/core/contracts/contract.d.ts
200
456
  /**
201
- * Accessor for the whole temporary working directory used by a CLI spec.
202
- *
203
- * Generalization of {@link DirectoryAccessor} that snapshots the entire CLI
204
- * working directory into `<test-file-dir>/expected/filesystem/<name>/`.
457
+ * A declared external interaction: what to match and what to reply, together
458
+ * in one named artifact. Contracts live in flat TypeScript files under
459
+ * `contracts/` next to the tests that use them `<name>.<provider>.ts` with
460
+ * `provider { openai, anthropic, http }` (CONVENTIONS C4) — so the business
461
+ * payload (prompts, JSON responses) is visible at a glance while the real
462
+ * HTTP call stays mocked underneath (MSW).
205
463
  */
206
- declare class FilesystemAccessor {
207
- /** The absolute path of the temporary working directory. */
208
- readonly cwd: string;
209
- private readonly testDir;
210
- constructor(cwd: string, testDir: string);
211
- /** List all files (recursively) under the working directory, sorted. */
212
- files(options?: {
213
- ignore?: string[];
214
- }): Promise<string[]>;
464
+ interface InterceptContract {
465
+ trigger: InterceptTrigger;
215
466
  /**
216
- * Assert the working directory matches a convention-based fixture tree:
217
- * `<test-file-dir>/expected/filesystem/<name>/`.
467
+ * The reply a fixed {@link InterceptResponse}, or a function
468
+ * `(request) => InterceptResponse` evaluated per consumed request when the
469
+ * response must derive from the incoming payload.
218
470
  */
219
- toMatch(name: string, options?: DirectorySnapshotOptions): Promise<void>;
471
+ response: InterceptResponseValue;
220
472
  }
473
+ /**
474
+ * Declare an intercept contract. Identity function — its value is the
475
+ * enforced shape and the naming convention:
476
+ *
477
+ * @example
478
+ * // specs/api/reports/contracts/classify-article.openai.ts
479
+ * import { defineContract, openai } from '@jterrazz/test';
480
+ *
481
+ * export default defineContract({
482
+ * trigger: openai.responses({ user: /Report Ingestion/, tools: ['classify'] }),
483
+ * response: openai.reply({ categories: ['TECH'] }),
484
+ * });
485
+ *
486
+ * // Dynamic — the response is computed from the observed request:
487
+ * export default defineContract({
488
+ * trigger: http.post('https://api.example.com/echo'),
489
+ * response: (request) => http.json({ received: request.body }),
490
+ * });
491
+ *
492
+ * // specs/api/reports/reports.test.ts
493
+ * import classifyArticle from '../../spec/intercept/contracts/classify-article.openai.js';
494
+ *
495
+ * const result = await jobs.intercept(classifyArticle).trigger('report-ingestion');
496
+ */
497
+ declare function defineContract(contract: InterceptContract): InterceptContract;
221
498
  //#endregion
222
- //#region src/spec/result/json.d.ts
223
- interface JsonSnapshotOptions {
224
- update?: boolean;
225
- }
499
+ //#region src/core/ports/isolation.port.d.ts
226
500
  /**
227
- * Accessor for a JSON payload parsed from a text stream (stdout).
501
+ * Strategy for isolating service state across parallel test workers.
228
502
  *
229
- * Lazily parses the JSON on first use; parse errors are deferred until an
230
- * assertion is performed so that callers can still read the raw stream text
231
- * without triggering a throw.
503
+ * Each service handle provides an isolation strategy. The framework
504
+ * calls `acquire()` once per vitest worker, `reset()` before each
505
+ * `spec.run()`, and `release()` when the worker shuts down.
232
506
  *
233
- * When a `transform` is configured on the spec runner, it is applied to the
234
- * raw stdout text BEFORE `JSON.parse`. This lets consumers strip ANSI codes,
235
- * scrub machine-specific paths, etc. before structural comparison.
507
+ * Implement this interface to support new service types (e.g. MongoDB,
508
+ * Elasticsearch, S3).
236
509
  */
237
- declare class JsonAccessor {
238
- private readonly rawText;
239
- private readonly testDir;
240
- private readonly transform?;
241
- constructor(rawText: string, testDir: string, transform?: (text: string) => string);
242
- /** The parsed JSON value. Throws if the text is not valid JSON. */
243
- get value(): unknown;
510
+ interface IsolationStrategy {
244
511
  /**
245
- * Assert the parsed JSON deep-equals the JSON in the given file on disk.
512
+ * Create an isolated namespace for this worker.
513
+ * Called once when the worker starts — e.g. clone a template database,
514
+ * set a Redis key prefix.
246
515
  *
247
- * Path is resolved relative to the test file directory unless absolute.
248
- * If `JTERRAZZ_TEST_UPDATE=1` (or vitest `-u`), the file is (re)written
249
- * with the pretty-printed parsed value.
516
+ * @param workerId - Unique identifier for this vitest worker.
250
517
  */
251
- toMatchFile(path: string, options?: JsonSnapshotOptions): void;
518
+ acquire: (workerId: string) => Promise<void>;
252
519
  /**
253
- * Assert the parsed JSON matches a convention-based fixture:
254
- * `<test-file-dir>/expected/json/<name>`.
255
- *
256
- * The extension is part of the name and must be included by the caller —
257
- * e.g. `toMatch('error.json')`, not `toMatch('error')`. Explicit extensions
258
- * are clearer at the call site and remove magic from the resolution.
520
+ * Fast reset within the namespace between `spec.run()` calls.
521
+ * E.g. truncate tables (without dropping the database).
522
+ */
523
+ reset: () => Promise<void>;
524
+ /**
525
+ * Tear down the isolated namespace.
526
+ * Called once when the worker shuts down — e.g. drop the cloned database.
259
527
  */
260
- toMatch(name: string, options?: JsonSnapshotOptions): void;
261
- private parse;
528
+ release: () => Promise<void>;
262
529
  }
263
530
  //#endregion
264
- //#region src/spec/result/table.d.ts
265
- /** Assertion helper for verifying database table contents after a specification run. */
266
- declare class TableAssertion {
267
- private tableName;
268
- private db;
269
- constructor(tableName: string, db: DatabasePort);
531
+ //#region src/core/ports/service.port.d.ts
532
+ /**
533
+ * A service handle — returned by factory functions like postgres(), redis().
534
+ * Mutable: connectionString is populated after the orchestrator starts containers.
535
+ */
536
+ interface ServiceHandle {
537
+ /** Service type identifier. */
538
+ readonly type: string;
270
539
  /**
271
- * Assert that the table contains exactly the expected rows for the given columns.
272
- *
273
- * @example
274
- * await result.table("users").toMatch({
275
- * columns: ["name", "email"],
276
- * rows: [["Alice", "alice@example.com"]],
277
- * });
540
+ * Compose service name. Left `null` until the orchestrator binds it at
541
+ * start time (CONVENTIONS A6): a handle with no explicit `composeService`
542
+ * links to the compose service named exactly like its record key, else the
543
+ * kebab-case conversion of the key — so `{ analyticsDb: postgres() }` binds
544
+ * to the compose service `analytics-db` without any option. Set
545
+ * `composeService` explicitly only for names the key cannot derive.
278
546
  */
279
- toMatch(expected: {
280
- columns: string[];
281
- rows: unknown[][];
282
- }): Promise<void>;
283
- /** Assert that the table has zero rows. */
284
- toBeEmpty(): Promise<void>;
547
+ composeName: null | string;
548
+ /** Default container port for this service type. */
549
+ readonly defaultPort: number;
550
+ /** Default Docker image for this service type. */
551
+ readonly defaultImage: string;
552
+ /** Environment variables to pass to the container. */
553
+ readonly environment: Record<string, string>;
554
+ /** Connection string — populated after start. */
555
+ connectionString: string;
556
+ /** Whether this service has been started. */
557
+ started: boolean;
558
+ /** Build the connection string from host and port. */
559
+ buildConnectionString: (host: string, port: number) => string;
560
+ /** Create a DatabasePort adapter (if this is a database). Returns null otherwise. */
561
+ createDatabaseAdapter: () => DatabasePort | null;
562
+ /** Verify the service is ready and accepting connections. Throws with context if not. */
563
+ healthcheck: () => Promise<void>;
564
+ /** Run initialization scripts (e.g., init.sql). Throws with SQL error context if it fails. */
565
+ initialize: (composeDir: string) => Promise<void>;
566
+ /** Reset state between tests (truncate tables, flush cache, etc.) */
567
+ reset: () => Promise<void>;
568
+ /** Get the isolation strategy for parallel test execution. */
569
+ isolation: () => IsolationStrategy;
285
570
  }
286
571
  //#endregion
287
- //#region src/spec/result/result.d.ts
572
+ //#region src/core/specification/shared/result/result.d.ts
288
573
  /** Read-only handle to a single file produced by a spec action. */
289
574
  interface FileAccessor {
290
575
  /** The UTF-8 text content. Throws if the file does not exist. */
291
576
  readonly content: string;
292
577
  readonly exists: boolean;
578
+ /** The file text as a {@link TextAccessor}, keeping only blocks matching `pattern`. */
579
+ grep: (pattern: string) => TextAccessor;
293
580
  }
294
581
  interface BaseResultOptions {
295
582
  config: SpecificationConfig;
@@ -298,9 +585,14 @@ interface BaseResultOptions {
298
585
  }
299
586
  /**
300
587
  * Base result - common accessors available after any action type.
301
- * Extended by HttpResult, CliResult, and used directly by JobResult.
588
+ * Extended by HttpResult, CliResult, and used directly by job results.
589
+ *
590
+ * Every result carries a fresh {@link CaptureScope}: `match.ref()` captures
591
+ * and `{{type#ref}}` placeholders are scoped to one spec execution.
302
592
  */
303
593
  declare class BaseResult {
594
+ /** @internal Ref-capture scope shared by every assertion on this result. */
595
+ readonly captures: CaptureScope;
304
596
  protected config: SpecificationConfig;
305
597
  protected testDir: string;
306
598
  protected workDir?: string;
@@ -309,127 +601,164 @@ declare class BaseResult {
309
601
  directory(path?: string): DirectoryAccessor;
310
602
  /** Access a single file (relative to the working directory) for content assertions. */
311
603
  file(path: string): FileAccessor;
312
- /** Access a database table for row-level assertions. */
604
+ /** Access a database table for row-level assertions via expect() matchers. */
313
605
  table(tableName: string, options?: {
314
- service?: string;
315
- }): TableAssertion;
606
+ database?: string;
607
+ }): TableAccessor;
316
608
  private resolveDatabase;
317
609
  }
318
610
  //#endregion
319
- //#region src/spec/result/stream.d.ts
320
- interface StreamSnapshotOptions {
321
- update?: boolean;
611
+ //#region src/core/specification/api/result.d.ts
612
+ /** Result from an HTTP action (.request(), .get(), .post(), .put(), .delete()). */
613
+ declare class HttpResult extends BaseResult {
614
+ private responseData;
615
+ constructor(options: {
616
+ config: SpecificationConfig;
617
+ response: ServerResponse;
618
+ testDir: string;
619
+ });
620
+ /** Access the HTTP response (status, headers, body) for assertions. */
621
+ get response(): ResponseAccessor;
622
+ /** The HTTP response status code. */
623
+ get status(): number;
624
+ }
625
+ //#endregion
626
+ //#region src/core/specification/shared/builder.d.ts
627
+ /** A named job that can be triggered via jobs.trigger(). */
628
+ interface JobHandle {
629
+ name: string;
630
+ execute: () => Promise<void>;
322
631
  }
323
632
  /**
324
- * Accessor for a captured text stream (stdout/stderr) with file-based
325
- * assertion support.
326
- *
327
- * Backed by a primitive string, exposed via {@link text}, but also provides
328
- * `toString()` / `valueOf()` for string coercion, so common patterns like
329
- * `String(result.stdout)` and template-literal interpolation still work.
633
+ * Configuration for the docker-aware cli mode. When set on
634
+ * {@link SpecificationConfig}, the cli runner generates a test-run id, injects
635
+ * it into the child env under `envVar`, then queries Docker for every
636
+ * container carrying `testRunLabel=<id>` after the command exits.
330
637
  */
331
- declare class StreamAccessor {
332
- private readonly streamName;
333
- private readonly testDir;
334
- private readonly transform?;
335
- readonly text: string;
336
- constructor(text: string, streamName: string, testDir: string, transform?: (text: string) => string);
638
+ interface DockerSpecConfig {
639
+ envVar: string;
640
+ nameLabel: string;
641
+ testRunLabel: string;
642
+ }
643
+ /** Adapter configuration passed to the specification facets at setup time. */
644
+ interface SpecificationConfig {
645
+ command?: CliPort;
646
+ database?: DatabasePort;
337
647
  /**
338
- * Assert the captured text matches the given file on disk.
339
- *
340
- * Path is resolved relative to the test file directory unless absolute.
341
- * If `JTERRAZZ_TEST_UPDATE=1` (or vitest `-u`), the file is (re)written
342
- * with the actual text.
343
- *
344
- * When a `transform` is configured on the spec runner, it is applied to
345
- * the actual text before comparison (and before writing in update mode).
346
- * The fixture file is treated as authoritative and is NOT transformed.
648
+ * Keys of the declared services record that are databases. Drives the
649
+ * CONVENTIONS A7 rule: with 2+ databases the `database` option is
650
+ * mandatory on `.seed()` / `.table()`; with exactly one it is forbidden.
347
651
  */
348
- toMatchFile(path: string, options?: StreamSnapshotOptions): void;
652
+ databaseKeys?: string[];
653
+ databases?: Map<string, DatabasePort>;
654
+ dockerConfig?: DockerSpecConfig;
349
655
  /**
350
- * Assert the captured text matches a convention-based fixture:
351
- * `<test-file-dir>/expected/<stream>/<name>`.
352
- *
353
- * The extension is part of the name and must be included by the caller —
354
- * e.g. `toMatch('valid.txt')`, not `toMatch('valid')`. Explicit extensions
355
- * are clearer at the call site and remove magic from the resolution.
656
+ * Unique id shared by every spec from this runner instance.
657
+ * Stable for the runner's lifetime so multi-step tests (spawn in
658
+ * one run, inspect in another) see the same container scope. The
659
+ * facet factories auto-populate this when `dockerConfig` is present.
356
660
  */
357
- toMatch(name: string, options?: StreamSnapshotOptions): void;
661
+ dockerTestRunId?: string;
358
662
  /**
359
- * Assert the captured text contains the given substring. The runner's
360
- * `transform` (if any) is applied before the check so callers can rely
361
- * on the same normalised view as {@link toMatch}.
362
- *
363
- * Throws with a tight diff-style error on miss so tests read uniformly
364
- * with the other accessor assertions (no reaching through `.text` into
365
- * `expect(...).toContain(...)`).
663
+ * When set, `.intercept()` is unavailable on this runner and throws this
664
+ * reason immediately (compose mode MSW is in-process, CONVENTIONS I3).
366
665
  */
367
- toContain(substring: string): void;
368
- toString(): string;
369
- valueOf(): string;
666
+ interceptDisabledReason?: string;
667
+ jobs?: JobHandle[];
668
+ server?: ServerPort;
669
+ /**
670
+ * The declared services record. In cli mode, drives the automatic
671
+ * connection-URL injection into the child env (CONVENTIONS B6):
672
+ * `<KEY>_URL` per service, plus `DATABASE_URL` / `REDIS_URL` when
673
+ * unambiguous.
674
+ */
675
+ services?: Record<string, ServiceHandle>;
676
+ /**
677
+ * Optional normaliser applied to command stdout/stderr before every
678
+ * comparison. Does not mutate the raw `.text` accessor.
679
+ */
680
+ transform?: (text: string) => string;
370
681
  }
371
- //#endregion
372
- //#region src/spec/modes/cli/container-accessor.d.ts
373
682
  /**
374
- * Assertion accessor for a single Docker container captured by the docker()
375
- * spec mode. Mirrors the shape of {@link CliResult} so tests use the same
376
- * vocabulary (`stdout.toContain`, `file(...).content`, etc.) regardless of
377
- * where output came from.
683
+ * The `api` facet HTTP chain entry handed out by `specification.api()`.
684
+ * Setup methods chain; action methods are terminal: they execute the spec
685
+ * and resolve to the result.
378
686
  *
379
- * Sync state (`exists`, `running`, `status`) is derived from the `docker
380
- * inspect` payload captured at result-construction time. Logs are fetched
381
- * lazily on first access to `stdout`/`stderr`.
687
+ * The `DatabaseKey` parameter is the typed vocabulary of `.seed()`: the keys
688
+ * of the declared services record that are databases.
382
689
  */
383
- declare class ContainerAccessor {
384
- private cachedLogs;
385
- private readonly containerId;
386
- readonly exists: boolean;
387
- private readonly inspectData;
388
- readonly running: boolean;
389
- readonly status: string;
390
- private readonly testDir;
391
- private readonly transform?;
392
- /**
393
- * Underlying Docker container ID, or `null` if no container was captured
394
- * for this name. Useful when a follow-up CLI command needs to reference
395
- * the container by id (e.g. `spwn world inspect <id>`). Prefer the other
396
- * accessors for common reads pulling the raw id is the escape hatch.
397
- */
398
- get id(): null | string;
399
- constructor(containerId: null | string, inspectData: unknown, testDir: string, transform?: (text: string) => string);
400
- /**
401
- * Raw `docker inspect` object for this container. Throws if the container
402
- * was not captured (i.e. `.exists === false`).
403
- */
404
- get inspect(): JsonAccessor;
405
- /** Captured logs (stdout+stderr combined for v1). */
406
- get stdout(): StreamAccessor;
407
- /** Captured logs (stdout+stderr combined for v1). */
408
- get stderr(): StreamAccessor;
690
+ interface ApiSpecification<DatabaseKey extends string = string> {
691
+ /** Set HTTP headers for the request. Multiple calls merge. */
692
+ headers: (headers: Record<string, string>) => ApiSpecification<DatabaseKey>;
693
+ /** Intercept an outgoing HTTP call — a contract, an array of contracts, or a trigger + response pair. */
694
+ intercept: ((contractOrContracts: InterceptContract | InterceptContract[]) => ApiSpecification<DatabaseKey>) & ((trigger: InterceptTrigger, response: InterceptResponder | InterceptResponse | string) => ApiSpecification<DatabaseKey>);
695
+ /** Queue a SQL seed file from `seeds/` to run before the action. */
696
+ seed: (file: string, options?: {
697
+ database?: DatabaseKey;
698
+ }) => ApiSpecification<DatabaseKey>;
699
+ /** Send a DELETE request and resolve with the result. */
700
+ delete: (path: string) => Promise<HttpResult>;
701
+ /** Send a GET request and resolve with the result. */
702
+ get: (path: string) => Promise<HttpResult>;
703
+ /** Send a POST request (optional inline JSON body) and resolve with the result. */
704
+ post: (path: string, body?: unknown) => Promise<HttpResult>;
705
+ /** Send a PUT request (optional inline JSON body) and resolve with the result. */
706
+ put: (path: string, body?: unknown) => Promise<HttpResult>;
707
+ /** Send the complete request described by `requests/<file>` (.http format). */
708
+ request: (file: string) => Promise<HttpResult>;
709
+ }
710
+ /**
711
+ * The `jobs` facet — job chain entry handed out by `specification.jobs()`.
712
+ * Jobs run in-process by definition (CONVENTIONS A5/A8).
713
+ */
714
+ interface JobsSpecification<DatabaseKey extends string = string> {
715
+ /** Intercept an outgoing HTTP call — a contract, an array of contracts, or a trigger + response pair. */
716
+ intercept: ((contractOrContracts: InterceptContract | InterceptContract[]) => JobsSpecification<DatabaseKey>) & ((trigger: InterceptTrigger, response: InterceptResponder | InterceptResponse | string) => JobsSpecification<DatabaseKey>);
717
+ /** Queue a SQL seed file from `seeds/` to run before the action. */
718
+ seed: (file: string, options?: {
719
+ database?: DatabaseKey;
720
+ }) => JobsSpecification<DatabaseKey>;
721
+ /** Execute the named job registered via the `jobs` option and resolve with the result. */
722
+ trigger: (name: string) => Promise<BaseResult>;
723
+ }
724
+ /**
725
+ * The `cli` facet — command chain entry handed out by `specification.cli()`.
726
+ * Setup methods chain; `.exec()` is the single terminal action (CONVENTIONS
727
+ * B2) — `{ waitFor?, timeout? }` covers long-running processes.
728
+ */
729
+ interface CliSpecification<DatabaseKey extends string = string> {
730
+ /** Set environment variables on the child process. `$WORKDIR` expands; `null` unsets. */
731
+ env: (env: CliEnv) => CliSpecification<DatabaseKey>;
409
732
  /**
410
- * Read a file from inside the container via `docker exec cat`. The
411
- * returned object satisfies the same {@link FileAccessor} shape used by
412
- * the host filesystem accessor, so tests do not need to learn a new API.
733
+ * Copy a fixture into the working directory before execution. The path is
734
+ * feature-local (`<test-dir>/fixtures/<path>`) or, with a `$FIXTURES/`
735
+ * prefix, from the shared pool at `<specs-root>/fixtures/`. A trailing
736
+ * slash spreads a directory's contents into the cwd; without one a
737
+ * directory (or file) is copied under its own name. Chained calls layer.
413
738
  */
414
- file(path: string): FileAccessor;
739
+ fixture: (path: string) => CliSpecification<DatabaseKey>;
740
+ /** Queue a SQL seed file from `seeds/` to run against a database before the action. */
741
+ seed: (file: string, options?: {
742
+ database?: DatabaseKey;
743
+ }) => CliSpecification<DatabaseKey>;
415
744
  /**
416
- * Run a shell command inside the container and get back the same
417
- * {@link CliResult} used for host-side executions.
745
+ * Execute the command (or sequence of commands) and resolve with the
746
+ * result. Called with no arguments (`cli.exec()`), the binary runs bare —
747
+ * no CLI arguments. With `{ waitFor, timeout }`, the process is
748
+ * long-running: it resolves when the pattern appears and is killed at the
749
+ * timeout.
418
750
  */
419
- exec(cmd: string): Promise<CliResult>;
420
- private requireExists;
421
- private loadLogs;
422
- private containerExec;
751
+ exec: (args?: string | string[], options?: ExecOptions) => Promise<CliResult>;
423
752
  }
424
753
  //#endregion
425
- //#region src/spec/modes/cli/result.d.ts
754
+ //#region src/core/specification/cli/result.d.ts
426
755
  /**
427
- * Result from a CLI action (.exec(), .spawn()).
756
+ * Result from a command action (`.exec()`).
428
757
  *
429
758
  * When the runner was configured with a `docker` option, the result also
430
759
  * exposes container accessors and participates in `Symbol.asyncDispose`
431
760
  * cleanup. The Docker shell-outs are **lazy**: a test that never calls
432
- * `.container(...)` never queries the Docker daemon, so CLI-only tests
761
+ * `.container(...)` never queries the Docker daemon, so command-only tests
433
762
  * pay zero Docker cost even when the runner is Docker-aware.
434
763
  *
435
764
  * Dispose always runs one final label-filtered `docker ps` to catch
@@ -438,13 +767,13 @@ declare class ContainerAccessor {
438
767
  * a container.
439
768
  */
440
769
  declare class CliResult extends BaseResult {
441
- private readonly commandResult;
770
+ private readonly commandOutput;
442
771
  private containersCache;
443
772
  private readonly dockerConfig?;
444
773
  private readonly testRunId?;
445
774
  private readonly transform?;
446
775
  constructor(options: {
447
- commandResult: CommandResult;
776
+ commandOutput: CliOutput;
448
777
  config: SpecificationConfig;
449
778
  dockerConfig?: DockerSpecConfig;
450
779
  testDir: string;
@@ -455,16 +784,17 @@ declare class CliResult extends BaseResult {
455
784
  /** The process exit code. */
456
785
  get exitCode(): number;
457
786
  /** Accessor for the captured standard output with file-based assertions. */
458
- get stdout(): StreamAccessor;
787
+ get stdout(): TextAccessor;
459
788
  /** Accessor for the captured standard error with file-based assertions. */
460
- get stderr(): StreamAccessor;
789
+ get stderr(): TextAccessor;
461
790
  /** Accessor for parsing stdout as JSON and asserting against JSON fixtures. */
462
791
  get json(): JsonAccessor;
463
792
  /** Accessor for the temporary working directory the command ran in. */
464
793
  get filesystem(): FilesystemAccessor;
465
794
  /**
466
- * Look up a container the CLI spawned during this run by the value of
467
- * its name-label (as declared in `SpecOptions.docker.nameLabel`).
795
+ * Look up a container the command spawned during this run by the value of
796
+ * its name-label (as declared in the `docker.nameLabel` of the
797
+ * `specification.cli()` options).
468
798
  * First access triggers a one-shot `docker ps` + `docker inspect`
469
799
  * query and caches the result for the rest of the result's lifetime.
470
800
  * Tests that don't call this never touch Docker.
@@ -476,433 +806,349 @@ declare class CliResult extends BaseResult {
476
806
  /** All captured container IDs. Triggers the same lazy query as `container()`. */
477
807
  get containerIds(): string[];
478
808
  [Symbol.asyncDispose](): Promise<void>;
479
- /**
480
- * Extract text blocks from stdout that contain a pattern.
481
- *
482
- * @example
483
- * expect(result.grep('error.ts')).toContain('no-unused-vars');
484
- */
485
- grep(pattern: string): string;
486
809
  private ensureDockerAware;
487
810
  private loadContainers;
488
811
  }
489
812
  //#endregion
490
- //#region src/spec/result/response.d.ts
491
- /** Accessor for an HTTP response body with file-based assertion support. */
492
- declare class ResponseAccessor {
493
- readonly body: unknown;
494
- private testDir;
495
- constructor(body: unknown, testDir: string);
813
+ //#region src/integrations/docker/container-accessor.d.ts
814
+ /**
815
+ * Assertion accessor for a single Docker container captured by the docker()
816
+ * spec mode. Mirrors the shape of {@link CliResult} so tests use the same
817
+ * vocabulary (`stdout.toContain`, `file(...).content`, etc.) regardless
818
+ * of where output came from.
819
+ *
820
+ * Sync state (`exists`, `running`, `status`) is derived from the `docker
821
+ * inspect` payload captured at result-construction time. Logs are fetched
822
+ * lazily on first access to `stdout`/`stderr`.
823
+ */
824
+ declare class ContainerAccessor {
825
+ private cachedLogs;
826
+ private readonly containerId;
827
+ readonly exists: boolean;
828
+ private readonly inspectData;
829
+ readonly running: boolean;
830
+ readonly status: string;
831
+ private readonly testDir;
832
+ private readonly transform?;
496
833
  /**
497
- * Assert that the response body matches the JSON in `responses/{file}`.
498
- *
499
- * @example
500
- * result.response.toMatchFile("expected-items.json");
834
+ * Underlying Docker container ID, or `null` if no container was captured
835
+ * for this name. Useful when a follow-up CLI command needs to reference
836
+ * the container by id (e.g. `spwn world inspect <id>`). Prefer the other
837
+ * accessors for common reads — pulling the raw id is the escape hatch.
838
+ */
839
+ get id(): null | string;
840
+ constructor(containerId: null | string, inspectData: unknown, testDir: string, transform?: (text: string) => string);
841
+ /**
842
+ * Raw `docker inspect` object for this container. Throws if the container
843
+ * was not captured (i.e. `.exists === false`).
844
+ */
845
+ get inspect(): JsonAccessor;
846
+ /** Captured logs (stdout+stderr combined for v1). */
847
+ get stdout(): TextAccessor;
848
+ /** Captured logs (stdout+stderr combined for v1). */
849
+ get stderr(): TextAccessor;
850
+ /**
851
+ * Read a file from inside the container via `docker exec cat`. The
852
+ * returned object satisfies the same {@link FileAccessor} shape used by
853
+ * the host filesystem accessor, so tests do not need to learn a new API.
854
+ */
855
+ file(path: string): FileAccessor;
856
+ /**
857
+ * Run a shell command inside the container and get back the same
858
+ * {@link CliResult} used for host-side executions.
501
859
  */
502
- toMatchFile(file: string): void;
860
+ exec(cmd: string): Promise<CliResult>;
861
+ private requireExists;
862
+ private loadLogs;
863
+ private containerExec;
503
864
  }
504
865
  //#endregion
505
- //#region src/spec/modes/http/server.port.d.ts
506
- /**
507
- * HTTP response returned by a server port, with parsed JSON body.
508
- */
509
- interface ServerResponse {
510
- /** HTTP status code. */
511
- status: number;
512
- /** Parsed JSON response body, or null if parsing failed. */
513
- body: unknown;
514
- /** Response headers as a flat key-value map. */
515
- headers: Record<string, string>;
516
- }
866
+ //#region src/vitest/matchers.d.ts
517
867
  /**
518
- * Abstract server interface for specification runners.
519
- * Integration mode uses an in-process Hono app; E2E mode uses real HTTP via fetch.
868
+ * Per-call options for the fixture-file `toMatch` subjects. `frozen` opts a
869
+ * single fixture OUT of update-mode rewriting: a frozen fixture is NEVER
870
+ * written under `TEST_UPDATE=1` (or vitest `-u`) — in update mode a frozen
871
+ * mismatch still throws its diff, and a frozen missing fixture still throws its
872
+ * "does not exist" error. This is what makes a DELIBERATELY-WRONG fixture (the
873
+ * subject of a negative test that asserts the mismatch/error rendering)
874
+ * survivable across update runs instead of being silently overwritten with the
875
+ * actual output.
520
876
  */
521
- interface ServerPort {
522
- /** Send an HTTP request and return the parsed response. */
523
- request(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<ServerResponse>;
877
+ interface MatchFixtureOptions {
878
+ frozen?: boolean;
524
879
  }
525
880
  //#endregion
526
- //#region src/spec/modes/http/result.d.ts
527
- /** Result from an HTTP action (.get(), .post(), .put(), .delete()). */
528
- declare class HttpResult extends BaseResult {
529
- private responseData;
530
- constructor(options: {
531
- config: SpecificationConfig;
532
- response: ServerResponse;
533
- testDir: string;
534
- });
535
- /** The HTTP response status code. */
536
- get status(): number;
537
- /** Access the HTTP response body for assertions. */
538
- get response(): ResponseAccessor;
539
- }
540
- //#endregion
541
- //#region src/spec/builder.d.ts
542
- /** A named job that can be triggered via .job(). */
543
- interface JobHandle$1 {
544
- name: string;
545
- execute: () => Promise<void>;
546
- }
547
- /**
548
- * Context passed to a {@link SeedHandler} when a non-SQL seed is dispatched.
549
- */
550
- interface SeedHandlerContext {
551
- /** Absolute path of the temporary working directory for the current spec. */
552
- cwd: string;
553
- /** Absolute path of the test file directory (where `seeds/` lives). */
554
- testDir: string;
555
- }
556
- /**
557
- * Handler invoked for a non-SQL seed entry. The first path segment of the
558
- * seed file selects the handler; the handler is given the absolute path of
559
- * the source fragment (file or directory) and the working directory to mutate.
560
- */
561
- type SeedHandler = (ctx: SeedHandlerContext, fragmentPath: string) => Promise<void> | void;
562
- /**
563
- * Configuration for the docker() spec mode. When set on
564
- * {@link SpecificationConfig}, the CLI runner generates a test-run id, injects
565
- * it into the child env under `envVar`, then queries Docker for every
566
- * container carrying `testRunLabel=<id>` after the command exits.
567
- */
568
- interface DockerSpecConfig {
569
- envVar: string;
570
- nameLabel: string;
571
- testRunLabel: string;
572
- }
573
- /** Adapter configuration passed to the specification runner at setup time. */
574
- interface SpecificationConfig {
575
- command?: CommandPort;
576
- database?: DatabasePort;
577
- databases?: Map<string, DatabasePort>;
578
- dockerConfig?: DockerSpecConfig;
579
- /**
580
- * Unique id shared by every `.run()` from this runner instance.
581
- * Stable for the runner's lifetime so multi-step tests (spawn in
582
- * one run, inspect in another) see the same container scope. The
583
- * public `createSpecificationRunner` auto-populates this when
584
- * `dockerConfig` is present — callers should leave it unset.
585
- */
586
- dockerTestRunId?: string;
587
- fixturesRoot?: string;
588
- jobs?: JobHandle$1[];
589
- /**
590
- * Pluggable seed handlers for CLI-mode tests. Keys are leading path
591
- * segments (e.g. `"spwn.yaml/"`, `"agent/"`). When `.seed(relPath)` is
592
- * called with a path whose first segment matches, the handler is invoked
593
- * with the full absolute path of the seed fragment under
594
- * `<test-file-dir>/seeds/<relPath>`.
595
- */
596
- seedHandlers?: Record<string, SeedHandler>;
597
- server?: ServerPort;
881
+ //#region src/core/specification/shared/orchestrator.d.ts
882
+ interface OrchestratorOptions {
598
883
  /**
599
- * Optional normaliser applied to CLI stdout/stderr before every
600
- * .toMatch / .toMatchFile comparison. Does not mutate the raw
601
- * `.text` accessor.
884
+ * Named infrastructure record. Keys become the database vocabulary of the
885
+ * spec (`.seed()` / `.table()` `database` option) and drive the compose
886
+ * binding: a handle with no explicit `composeService` links to the compose
887
+ * service named exactly like its key, else the kebab-case conversion of the
888
+ * key (`analyticsDb` → `analytics-db`) — see {@link resolveComposeBinding}.
602
889
  */
603
- transform?: (text: string) => string;
890
+ services: Record<string, ServiceHandle>;
891
+ mode: 'e2e' | 'integration';
892
+ root?: string;
893
+ /** Compose project name — used for per-worker stack isolation. */
894
+ projectName?: string;
604
895
  }
605
896
  /**
606
- * Fluent builder for declaring a single test specification.
607
- *
608
- * Chain setup methods ({@link seed}, {@link fixture}, {@link env}), an action
609
- * ({@link get}, {@link post}, {@link exec}), then call {@link run} to execute
610
- * and receive a {@link SpecificationResult} for assertions.
897
+ * Orchestrator for test infrastructure.
898
+ * Integration: starts services via testcontainers.
899
+ * E2E: runs full docker compose up.
611
900
  */
612
- declare class SpecificationBuilder {
613
- private commandArgs;
614
- private commandEnv;
615
- private config;
616
- private fixtures;
617
- private intercepts;
618
- private jobName;
619
- private label;
620
- private mocks;
901
+ declare class Orchestrator {
902
+ private services;
903
+ private mode;
904
+ private root;
621
905
  private projectName;
622
- private request;
623
- private requestHeaders;
624
- private seeds;
625
- private spawnConfig;
626
- private testDir;
627
- constructor(config: SpecificationConfig, testDir: string, label: string);
628
- /**
629
- * Queue a SQL seed file to run before the action.
630
- *
631
- * @example
632
- * spec("creates user").seed("users.sql").exec("list-users").run();
633
- */
634
- seed(file: string, options?: {
635
- service?: string;
636
- }): this;
637
- /** Copy a file or directory from `fixtures/` into the working directory before execution. */
638
- fixture(file: string): this;
639
- /** Copy an entire project fixture from `fixturesRoot` into the working directory. */
640
- project(name: string): this;
641
- /** Register an MSW mock definition file from `mock/` to intercept HTTP calls. */
642
- mock(file: string): this;
643
- /**
644
- * Set environment variables for the CLI process. Merged on top of process.env.
645
- * Use `null` to unset a variable. Multiple calls merge.
646
- *
647
- * The token `$WORKDIR` (in any value) is replaced with the actual working
648
- * directory at run-time — useful for tests that need a fully isolated `HOME`.
649
- *
650
- * @example
651
- * spec("...").env({ HOME: "$WORKDIR", TZ: "UTC" }).exec("status").run();
906
+ private running;
907
+ private composeStack;
908
+ private composeHandles;
909
+ private started;
910
+ constructor(options: OrchestratorOptions);
911
+ /**
912
+ * Bind each declared handle to a compose service (CONVENTIONS A6): a record
913
+ * key resolves to the service named exactly like it, else the kebab-case
914
+ * conversion of the key; an explicit `composeService` wins. Throws on an
915
+ * ambiguous binding (both names present). Runs once compose config is known
916
+ * so the two-step resolution can consult the real service list.
652
917
  */
653
- env(env: CommandEnv): this;
918
+ private resolveBindings;
654
919
  /**
655
- * Set HTTP headers for the request. Multiple calls merge.
656
- *
657
- * @example
658
- * spec("french").headers({ 'Accept-Language': 'fr' }).get("/articles").run();
920
+ * Start declared services via testcontainers (integration mode).
921
+ * Phase 1: start all containers in parallel (the slow part).
922
+ * Phase 2: wire connections, healthcheck, and init sequentially (fast).
659
923
  */
660
- headers(headers: Record<string, string>): this;
924
+ start(): Promise<void>;
661
925
  /**
662
- * Intercept an outgoing HTTP request and return a controlled response.
663
- * Uses MSW under the hood. Intercepts are queued — multiple calls with the
664
- * same trigger fire sequentially (first match consumed first).
665
- *
666
- * @param trigger - What to match (use openai.agent(), http.get(), etc.)
667
- * @param response - What to return: an InterceptResponse object, or a filename
668
- * from `intercepts/` (JSON loaded and auto-wrapped by the trigger).
669
- *
670
- * @example
671
- * // With inline response
672
- * .intercept(openai.agent({...}), openai.reply({ categories: ['TECH'] }))
673
- *
674
- * // With JSON fixture file (loaded from intercepts/ directory)
675
- * .intercept(openai.agent({...}), 'ingest-tech.json')
676
- * .intercept(http.get(url), 'world-news-tech.json')
926
+ * Stop testcontainers (integration mode).
677
927
  */
678
- intercept(trigger: InterceptTrigger, response: InterceptResponse | string): this;
928
+ stop(): Promise<void>;
679
929
  /**
680
- * Send a GET request to the server adapter.
681
- *
682
- * @example
683
- * spec("list items").get("/api/items").run();
930
+ * Start full docker compose stack (e2e mode).
931
+ * Auto-detects infra services and creates handles for them.
684
932
  */
685
- get(path: string): this;
933
+ startCompose(): Promise<void>;
686
934
  /**
687
- * Send a POST request to the server adapter.
688
- *
689
- * @param bodyFile - Optional JSON file from `requests/` to use as the request body.
690
- * @example
691
- * spec("create item").post("/api/items", "new-item.json").run();
935
+ * Stop docker compose stack (e2e mode).
692
936
  */
693
- post(path: string, bodyFile?: string): this;
694
- /** Send a PUT request to the server adapter. */
695
- put(path: string, bodyFile?: string): this;
696
- /** Send a DELETE request to the server adapter. */
697
- delete(path: string): this;
937
+ stopCompose(): Promise<void>;
698
938
  /**
699
- * Execute a CLI command (or a sequence of commands) in an isolated working directory.
700
- * When an array is passed, commands run sequentially and stop on the first non-zero exit code.
701
- *
702
- * @example
703
- * spec("init project").exec("init --name demo").run();
704
- * spec("multi-step").exec(["init", "build"]).run();
939
+ * Get the default database the first declared handle that is one.
705
940
  */
706
- exec(args: string | string[]): this;
707
- /** Spawn a long-running CLI process with custom spawn options (e.g. timeout, signal). */
708
- spawn(args: string, options: SpawnOptions): this;
941
+ getDatabase(): DatabasePort | null;
709
942
  /**
710
- * Execute a named job registered via the app() factory.
711
- *
712
- * @param name - The job name to trigger (must match a registered JobHandle.name).
713
- *
714
- * @example
715
- * spec('pipeline').intercept(openai.chat(), openai.response({...})).job('report-refresh').run();
943
+ * Get all database services keyed by their record key (declared services)
944
+ * or compose service name (stack-detected services).
716
945
  */
717
- job(name: string): this;
946
+ getDatabases(): Map<string, DatabasePort>;
947
+ private allHandles;
718
948
  /**
719
- * Execute the specification: run seeds, copy fixtures, then perform the
720
- * configured action (HTTP or CLI).
721
- *
722
- * @returns The result object used for assertions.
723
- * @example
724
- * const result = await spec("test").exec("status").run();
725
- * expect(result.exitCode).toBe(0);
949
+ * Get app URL from compose (e2e mode).
726
950
  */
727
- run(): Promise<BaseResult | CliResult | HttpResult>;
728
- private resolveSeedHandler;
729
- private resolveEnv;
730
- private prepareWorkDir;
731
- private runHttpAction;
732
- private runJobAction;
733
- private runCliAction;
951
+ getAppUrl(): null | string;
734
952
  }
735
- /** Factory function returned by `cli()`, `integration()`, or `e2e()` that starts a new spec. */
736
- type SpecificationRunner = (label: string) => SpecificationBuilder;
953
+ //#endregion
954
+ //#region src/core/specification/shared/services.d.ts
737
955
  /**
738
- * Create a {@link SpecificationRunner} bound to the given adapter configuration.
739
- * The test file directory is auto-detected from the call stack.
956
+ * Infrastructure services declared as a named record. Keys become the typed
957
+ * vocabulary of the whole spec: the server factory receives the same record,
958
+ * and `.seed()` / `.table()` target databases by key.
740
959
  */
741
- declare function createSpecificationRunner(config: SpecificationConfig): SpecificationRunner;
960
+ type ServiceRecord = Record<string, ServiceHandle>;
961
+ /** Keys of a services record whose handles are databases. */
962
+ type DatabaseKeys<Services extends ServiceRecord> = { [K in keyof Services]: Services[K] extends DatabasePort ? K & string : never; }[keyof Services];
742
963
  //#endregion
743
- //#region src/spec/targets.d.ts
964
+ //#region src/core/specification/api/start-api.d.ts
744
965
  /** Any object with a request method compatible with Hono's app.request(). */
745
966
  type HonoApp = {
746
967
  request: (path: string, init?: RequestInit) => Promise<Response> | Response;
747
968
  };
748
- /** A named job that can be triggered via .job(). */
749
- interface JobHandle {
750
- name: string;
751
- execute: () => Promise<void>;
752
- }
753
- /** Services map passed to the app factory after infrastructure starts. */
754
- type AppServices = Record<string, ServiceHandle>;
755
- /**
756
- * Return value from the app() factory. Either a bare server (backward compat)
757
- * or an object with server + jobs for job-based testing.
758
- */
759
- type AppFactoryResult = HonoApp | {
760
- server: HonoApp;
761
- jobs: JobHandle[];
762
- };
763
- /** In-process Hono app target. Created by {@link app}. */
764
- interface AppTarget {
765
- readonly kind: 'app';
766
- readonly factory: (services: AppServices) => AppFactoryResult;
767
- }
768
- /** Docker compose stack target. Created by {@link stack}. */
769
- interface StackTarget {
770
- readonly kind: 'stack';
771
- readonly root: string;
772
- }
773
- /** CLI command target. Created by {@link command}. */
774
- interface CommandTarget {
775
- readonly kind: 'command';
776
- readonly bin: string;
969
+ /** Execution mode exists ONLY on `specification.api()` (CONVENTIONS A5). */
970
+ type SpecificationMode = 'compose' | 'node';
971
+ /** Options for {@link startApi | specification.api}. */
972
+ interface ApiSpecificationOptions<Services extends ServiceRecord = ServiceRecord> {
973
+ /**
974
+ * Execution mode override. Resolution: `options.mode` >
975
+ * `process.env.TEST_MODE` > `'node'`. Never hardcode this in a
976
+ * specification file when `server` is defined — set it per vitest
977
+ * project via `env: { TEST_MODE: 'compose' }` (CONVENTIONS A5).
978
+ */
979
+ mode?: SpecificationMode;
980
+ /**
981
+ * Project root override for compose detection and init scripts. When
982
+ * absent, the root is auto-discovered by walking up from the calling
983
+ * specification file to the first directory containing
984
+ * `docker/compose.test.yaml`, else the first containing `package.json`
985
+ * (CONVENTIONS A9).
986
+ */
987
+ root?: string;
988
+ /**
989
+ * The app factory receives the started services record (fully typed)
990
+ * and returns the Hono app. Required in node mode, ignored in compose
991
+ * mode (the app runs as a compose service there).
992
+ */
993
+ server?: (services: Services) => HonoApp;
994
+ /**
995
+ * Named infrastructure record. Keys become the `database` vocabulary of
996
+ * `.seed()` / `.table()` and drive the compose binding: a handle with no
997
+ * `composeService` option links to the compose service named exactly like
998
+ * its key, else the kebab-case conversion of the key (CONVENTIONS A6).
999
+ */
1000
+ services?: Services;
777
1001
  }
778
- /** Any target that produces an HTTP interface (app or stack). */
779
- type HttpTarget = AppTarget | StackTarget;
780
- /** Any valid spec target. */
781
- type SpecTarget = AppTarget | CommandTarget | StackTarget;
782
- /**
783
- * Test against an in-process Hono app. The factory receives started services
784
- * so you can wire connection strings into your app/DI container.
785
- *
786
- * @param factory - Function that receives services and returns a Hono app instance.
787
- *
788
- * @example
789
- * const db = postgres({ compose: 'db' });
790
- * await spec(
791
- * app((services) => createApp({ databaseUrl: services.db.connectionString })),
792
- * { services: [db] },
793
- * );
794
- */
795
- declare function app(factory: (services: AppServices) => HonoApp): AppTarget;
796
1002
  /**
797
- * Test against a full docker compose stack. The stack is started with
798
- * `docker compose up` and real HTTP requests are sent to the app service.
1003
+ * The record returned by {@link startApi | specification.api}. Destructure
1004
+ * with the canonical names (CONVENTIONS A3):
799
1005
  *
800
- * @param root - Project root containing `docker/compose.test.yaml`.
801
- *
802
- * @example
803
- * await spec(stack('../../'));
1006
+ * const { api, cleanup, docker } = await specification.api(…);
804
1007
  */
805
- declare function stack(root: string): StackTarget;
806
- /**
807
- * Test a CLI binary. Each spec runs in a fresh temp directory.
808
- *
809
- * Pass a `docker: { envVar, nameLabel, testRunLabel }` option on
810
- * {@link SpecOptions} to make the runner Docker-aware the runner
811
- * stamps a unique test-run id into `envVar`, and results expose
812
- * `.container(name)` accessors that lazily query Docker. Tests that
813
- * never call `.container(...)` pay zero Docker cost.
814
- *
815
- * @param bin - Path to the CLI binary or command name (resolved from node_modules/.bin or PATH).
816
- *
817
- * @example
818
- * // CLI-only
819
- * await spec(command('my-cli'), { root: '../fixtures' });
820
- *
821
- * // CLI binary that also spawns containers — same runner, just
822
- * // opt into container accessors via the docker option:
823
- * await spec(command('my-cli'), {
824
- * root: '../fixtures',
825
- * docker: {
826
- * envVar: 'MYCLI_TEST_LABEL',
827
- * nameLabel: 'com.mycli.world.name',
828
- * testRunLabel: 'com.mycli.test.run',
829
- * },
830
- * });
831
- */
832
- declare function command(bin: string): CommandTarget;
1008
+ interface ApiHandle<DatabaseKey extends string = string> {
1009
+ api: ApiSpecification<DatabaseKey>;
1010
+ /** Stop all infrastructure started by this specification. */
1011
+ cleanup: () => Promise<void>;
1012
+ /**
1013
+ * Read a running container by idreturns a {@link ContainerAccessor}
1014
+ * usable with `await expect(...).toBeRunning()` and read accessors.
1015
+ */
1016
+ docker: (containerId: string) => ContainerAccessor;
1017
+ /** The orchestrator managing the test infrastructure lifecycle. */
1018
+ orchestrator: Orchestrator;
1019
+ }
1020
+ declare function startApi<Services extends ServiceRecord>(options: ApiSpecificationOptions<Services>): Promise<ApiHandle<DatabaseKeys<Services>>>;
833
1021
  //#endregion
834
- //#region src/spec/spec.d.ts
835
- /** Shared options for all spec targets. */
836
- interface SpecOptions {
1022
+ //#region src/core/specification/cli/start-cli.d.ts
1023
+ /** Options for {@link startCli | specification.cli}. */
1024
+ interface CliSpecificationOptions<Services extends ServiceRecord = ServiceRecord> {
837
1025
  /**
838
- * Opt-in Docker awareness for CLI-mode runners. When set, every
839
- * `.run()` generates a unique test-run id, injects it into the
840
- * child process env under `envVar`, and exposes `.container(name)`
841
- * accessors on the result that lazily query Docker. A test that
842
- * never calls `.container(...)` never touches the Docker daemon —
843
- * CLI-only and container-asserting tests share one runner.
844
- *
845
- * Always wrap calls that may spawn containers with `await using`
846
- * so leaked containers get force-removed at scope exit (the
847
- * runner cleans up by label filter).
1026
+ * Opt-in Docker awareness. When set, every spec generates a unique
1027
+ * test-run id, injects it into the child process env under `envVar`,
1028
+ * and exposes `.container(name)` accessors on the result that lazily
1029
+ * query Docker. Always declare results with `await using` so leaked
1030
+ * containers get force-removed at scope exit (CONVENTIONS B5).
848
1031
  */
849
1032
  docker?: DockerSpecConfig;
850
- /** Project root for fixture lookup and compose detection (relative paths supported). */
1033
+ /**
1034
+ * Project-root override (CONVENTIONS A9) — the single meaning of `root`:
1035
+ * it anchors compose detection and local-bin resolution for the tested
1036
+ * binary. It is NOT a fixtures root; `.fixture()` resolves feature-local
1037
+ * or `$FIXTURES/` paths on its own.
1038
+ */
851
1039
  root?: string;
852
1040
  /**
853
- * Pluggable seed handlers for CLI-mode tests. Keys are leading path
854
- * segments (e.g. `"spwn.yaml/"`); values receive the seed context and the
855
- * absolute path of the source fragment under `<test-file-dir>/seeds/`.
1041
+ * Named infrastructure record started via testcontainers. Connection
1042
+ * URLs are injected automatically into the child env: `<KEY>_URL` per
1043
+ * service, plus `DATABASE_URL` / `REDIS_URL` when unambiguous
1044
+ * (CONVENTIONS B6). `.env()` overrides.
856
1045
  */
857
- seedHandlers?: Record<string, SeedHandler>;
858
- /** Infrastructure services to start via testcontainers. */
859
- services?: ServiceHandle[];
1046
+ services?: Services;
860
1047
  /**
861
- * Optional normaliser applied to result.stdout.text and
862
- * result.stderr.text before every .toMatch / .toMatchFile
863
- * comparison. Useful for stripping ANSI codes, masking
864
- * machine-specific paths, scrubbing timestamps, etc.
865
- *
866
- * The transform does NOT mutate the raw `.text` accessor —
867
- * callers can still inspect the pristine output.
1048
+ * Escape hatch: normaliser applied to result.stdout / result.stderr
1049
+ * before every comparison, AFTER the default ANSI strip (CONVENTIONS
1050
+ * D6). Does NOT mutate the raw `.text` accessor. Prefer `{{token}}`
1051
+ * placeholders in fixtures.
868
1052
  */
869
1053
  transform?: (text: string) => string;
870
1054
  }
871
- /** A specification runner with teardown support and orchestrator access. */
872
- interface SpecRunner {
873
- /** The function to create individual specs: `runner('label').get('/path').run()`. */
874
- (label: string): SpecificationBuilder;
875
- /** Stop all infrastructure started by this runner. */
1055
+ /**
1056
+ * The record returned by {@link startCli | specification.cli}. Destructure
1057
+ * with the canonical names (CONVENTIONS A3):
1058
+ *
1059
+ * const { cli, cleanup, docker } = await specification.cli(…);
1060
+ */
1061
+ interface CliHandle<DatabaseKey extends string = string> {
1062
+ /** Stop all infrastructure started by this specification. */
876
1063
  cleanup: () => Promise<void>;
1064
+ cli: CliSpecification<DatabaseKey>;
877
1065
  /**
878
- * Get a Docker assertion builder for a running container.
879
- *
880
- * @example
881
- * await runner.docker('my-container').toBeRunning();
1066
+ * Read a running container by id — returns a {@link ContainerAccessor}
1067
+ * usable with `await expect(...).toBeRunning()` and read accessors.
882
1068
  */
883
- docker: (containerId: string) => DockerAssertion;
1069
+ docker: (containerId: string) => ContainerAccessor;
884
1070
  /** The orchestrator managing the test infrastructure lifecycle. */
885
- orchestrator: Orchestrator;
1071
+ orchestrator: null | Orchestrator;
1072
+ }
1073
+ declare function startCli<Services extends ServiceRecord>(bin: string, options?: CliSpecificationOptions<Services>): Promise<CliHandle<DatabaseKeys<Services>>>;
1074
+ //#endregion
1075
+ //#region src/core/specification/jobs/start-jobs.d.ts
1076
+ /** Options for {@link startJobs | specification.jobs}. */
1077
+ interface JobsSpecificationOptions<Services extends ServiceRecord = ServiceRecord> {
1078
+ /**
1079
+ * Named jobs triggerable via `jobs.trigger(name)` — a factory receiving
1080
+ * the started services record, or a static array. Jobs run in-process by
1081
+ * definition (CONVENTIONS A5/A8) — there is no mode.
1082
+ */
1083
+ jobs: ((services: Services) => JobHandle[]) | JobHandle[];
1084
+ /** Project root override — see the `root` option of `specification.api()`. */
1085
+ root?: string;
1086
+ /** Named infrastructure record started via testcontainers. */
1087
+ services?: Services;
886
1088
  }
887
1089
  /**
888
- * Create a specification runner for the given target.
1090
+ * The record returned by {@link startJobs | specification.jobs}. Destructure
1091
+ * with the canonical names (CONVENTIONS A3):
889
1092
  *
890
- * @param target - What to test against: {@link app}, {@link stack}, or {@link command}.
891
- * @param options - Shared options: root directory, infrastructure services.
1093
+ * const { jobs, cleanup } = await specification.jobs(…);
1094
+ */
1095
+ interface JobsHandle<DatabaseKey extends string = string> {
1096
+ /** Stop all infrastructure started by this specification. */
1097
+ cleanup: () => Promise<void>;
1098
+ jobs: JobsSpecification<DatabaseKey>;
1099
+ /** The orchestrator managing the test infrastructure lifecycle. */
1100
+ orchestrator: null | Orchestrator;
1101
+ }
1102
+ declare function startJobs<Services extends ServiceRecord>(options: JobsSpecificationOptions<Services>): Promise<JobsHandle<DatabaseKeys<Services>>>;
1103
+ //#endregion
1104
+ //#region src/core/specification/shared/specification.d.ts
1105
+ /**
1106
+ * The three specification constructors (CONVENTIONS A2) — created in a
1107
+ * `*.specification.ts` file under `specs/`, destructured with canonical
1108
+ * names, and cleaned up via `afterAll(cleanup)` (A1/A3/A4).
892
1109
  *
893
1110
  * @example
894
- * // HTTP — in-process app with testcontainers
895
- * const s = await spec(app(() => createApp(db)), { services: [db] });
1111
+ * // specs/api/api.specification.ts
1112
+ * export const { api, cleanup } = await specification.api({
1113
+ * services: { db: postgres() },
1114
+ * server: ({ db }) => createApp({ databaseUrl: db.connectionString }),
1115
+ * });
1116
+ * afterAll(cleanup);
896
1117
  *
897
- * // HTTP — full docker compose stack
898
- * const s = await spec(stack('../../'));
1118
+ * // specs/jobs/jobs.specification.ts
1119
+ * export const { jobs, cleanup } = await specification.jobs({
1120
+ * services: { db: postgres() },
1121
+ * jobs: ({ db }) => [nightlyReport(db)],
1122
+ * });
1123
+ * afterAll(cleanup);
899
1124
  *
900
- * // CLI — command binary
901
- * const s = await spec(command('my-cli'), { root: '../fixtures' });
1125
+ * // specs/setup/cli.specification.ts
1126
+ * export const { cli, cleanup } = await specification.cli('my-cli');
1127
+ * afterAll(cleanup);
902
1128
  */
903
- declare function spec(target: SpecTarget, options?: SpecOptions): Promise<SpecRunner>;
1129
+ declare const specification: {
1130
+ /**
1131
+ * Test an HTTP app. Mode `'node'` (default) starts the declared services
1132
+ * via testcontainers and runs the app in-process; mode `'compose'` runs
1133
+ * `docker compose up` on `docker/compose.test.yaml` and sends real HTTP
1134
+ * requests to the app service. Resolution: `options.mode` > `TEST_MODE`
1135
+ * env var > `'node'`. Only `.api()` has a mode.
1136
+ */
1137
+ api: typeof startApi;
1138
+ /**
1139
+ * Test a command binary. Each spec runs in a fresh temp directory.
1140
+ *
1141
+ * @param bin - Path to the binary (resolved from node_modules/.bin or PATH).
1142
+ */
1143
+ cli: typeof startCli;
1144
+ /**
1145
+ * Test background jobs. Jobs run in-process by definition — no HTTP
1146
+ * server, no mode. `.trigger(name)` is the terminal action.
1147
+ */
1148
+ jobs: typeof startJobs;
1149
+ };
904
1150
  //#endregion
905
- //#region src/spec/modes/cli/docker-lookup.d.ts
1151
+ //#region src/integrations/docker/docker-lookup.d.ts
906
1152
  /** Return all container IDs (running or stopped) that carry `key=value`. */
907
1153
  declare function findContainersByLabel(key: string, value: string): string[];
908
1154
  /** Return the raw `docker inspect` payload (object, not array) for a container. */
@@ -910,71 +1156,346 @@ declare function inspectContainer(id: string): unknown;
910
1156
  /** Force-remove the given container IDs in a single call. Errors are swallowed. */
911
1157
  declare function removeContainers(ids: string[]): void;
912
1158
  //#endregion
913
- //#region src/infra/containers/container.port.d.ts
1159
+ //#region src/core/ports/container.port.d.ts
914
1160
  /**
915
1161
  * Abstract container interface.
916
1162
  * Represents a running service (database, cache, etc.)
917
1163
  */
918
1164
  interface ContainerPort {
919
1165
  /** Start the container and wait until ready. */
920
- start(): Promise<void>;
1166
+ start: () => Promise<void>;
921
1167
  /** Stop and remove the container. */
922
- stop(): Promise<void>;
1168
+ stop: () => Promise<void>;
923
1169
  /** Get the mapped host port for a container port. */
924
- getMappedPort(containerPort: number): number;
1170
+ getMappedPort: (containerPort: number) => number;
925
1171
  /** Get the host to connect to. */
926
- getHost(): string;
1172
+ getHost: () => string;
927
1173
  /** Get a full connection string for this service. */
928
- getConnectionString(): string;
1174
+ getConnectionString: () => string;
929
1175
  /** Get container logs (stdout + stderr). */
930
- getLogs(): Promise<string>;
1176
+ getLogs: () => Promise<string>;
931
1177
  }
932
1178
  //#endregion
933
- //#region src/spec/modes/cli/adapters/exec.adapter.d.ts
1179
+ //#region src/integrations/postgres/postgres.d.ts
1180
+ interface PostgresOptions {
1181
+ /**
1182
+ * Map to a service in docker/compose.test.yaml. Defaults to the handle's
1183
+ * key in the declared services record.
1184
+ */
1185
+ composeService?: string;
1186
+ /** Override image. */
1187
+ image?: string;
1188
+ /** Override environment variables. */
1189
+ env?: Record<string, string>;
1190
+ }
1191
+ declare class PostgresHandle implements DatabasePort, ServiceHandle {
1192
+ readonly type = "postgres";
1193
+ composeName: null | string;
1194
+ readonly defaultPort = 5432;
1195
+ readonly defaultImage: string;
1196
+ readonly environment: Record<string, string>;
1197
+ connectionString: string;
1198
+ started: boolean;
1199
+ private client;
1200
+ private originalConnectionString;
1201
+ private schema;
1202
+ constructor(options?: PostgresOptions);
1203
+ buildConnectionString(host: string, port: number): string;
1204
+ createDatabaseAdapter(): DatabasePort;
1205
+ healthcheck(): Promise<void>;
1206
+ initialize(composeDir: string): Promise<void>;
1207
+ private getClient;
1208
+ seed(sql: string): Promise<void>;
1209
+ reset(): Promise<void>;
1210
+ query(table: string, columns: string[]): Promise<unknown[][]>;
1211
+ isolation(): IsolationStrategy;
1212
+ }
1213
+ /**
1214
+ * Create a PostgreSQL service handle.
1215
+ *
1216
+ * @example
1217
+ * // Key derives the compose service (exact name or kebab-case):
1218
+ * // { db: postgres() } → compose service "db"
1219
+ * // { analyticsDb: postgres() } → compose service "analytics-db"
1220
+ * // Escape hatch for names the key cannot derive:
1221
+ * const events = postgres({ composeService: "legacy_event_store" });
1222
+ * // After start: events.connectionString is populated
1223
+ */
1224
+ declare function postgres(options?: PostgresOptions): PostgresHandle;
1225
+ //#endregion
1226
+ //#region src/integrations/redis/redis.d.ts
1227
+ interface RedisOptions {
1228
+ /**
1229
+ * Map to a service in docker/compose.test.yaml. Defaults to the handle's
1230
+ * key in the declared services record.
1231
+ */
1232
+ composeService?: string;
1233
+ /** Override image. */
1234
+ image?: string;
1235
+ }
1236
+ declare class RedisHandle implements ServiceHandle {
1237
+ readonly type = "redis";
1238
+ composeName: null | string;
1239
+ readonly defaultPort = 6379;
1240
+ readonly defaultImage: string;
1241
+ readonly environment: Record<string, string>;
1242
+ connectionString: string;
1243
+ started: boolean;
1244
+ private dbIndex;
1245
+ constructor(options?: RedisOptions);
1246
+ buildConnectionString(host: string, port: number): string;
1247
+ createDatabaseAdapter(): DatabasePort | null;
1248
+ healthcheck(): Promise<void>;
1249
+ initialize(): Promise<void>;
1250
+ reset(): Promise<void>;
1251
+ isolation(): IsolationStrategy;
1252
+ }
1253
+ /**
1254
+ * Create a Redis service handle.
1255
+ *
1256
+ * @example
1257
+ * // The record key derives the compose service: { cache: redis() } → "cache".
1258
+ * const cache = redis();
1259
+ * // After start: cache.connectionString is populated
1260
+ */
1261
+ declare function redis(options?: RedisOptions): RedisHandle;
1262
+ //#endregion
1263
+ //#region src/integrations/sqlite/sqlite.d.ts
1264
+ interface SqliteOptions {
1265
+ /**
1266
+ * Path to a SQL file used to initialize the database schema.
1267
+ * Mutually exclusive with `prismaSchema`.
1268
+ */
1269
+ init?: string;
1270
+ /**
1271
+ * Path to a Prisma schema directory or file.
1272
+ * The adapter runs `prisma db push` to create the template.
1273
+ * Mutually exclusive with `init`.
1274
+ */
1275
+ prismaSchema?: string;
1276
+ }
1277
+ declare class SqliteHandle implements DatabasePort, ServiceHandle {
1278
+ readonly type = "sqlite";
1279
+ composeName: null | string;
1280
+ readonly defaultPort = 0;
1281
+ readonly defaultImage = "";
1282
+ readonly environment: Record<string, string>;
1283
+ connectionString: string;
1284
+ started: boolean;
1285
+ private db;
1286
+ private templatePath;
1287
+ private workerDbPath;
1288
+ private initSql;
1289
+ private prismaSchema;
1290
+ constructor(options?: SqliteOptions);
1291
+ buildConnectionString(): string;
1292
+ createDatabaseAdapter(): DatabasePort;
1293
+ healthcheck(): Promise<void>;
1294
+ initialize(): Promise<void>;
1295
+ private getDb;
1296
+ private closeDb;
1297
+ seed(sql: string): Promise<void>;
1298
+ query(table: string, columns: string[]): Promise<unknown[][]>;
1299
+ reset(): Promise<void>;
1300
+ isolation(): IsolationStrategy;
1301
+ }
934
1302
  /**
935
- * Executes CLI commands via Node.js child_process.
936
- * Uses `spawnSync` for one-shot commands and `spawn` for long-running processes.
937
- * Used by the command() specification runner.
1303
+ * Create a SQLite service handle. Uses file-copy isolation for parallel tests.
1304
+ *
1305
+ * @example
1306
+ * // With Prisma schema
1307
+ * const db = sqlite({ prismaSchema: './prisma/schema' });
938
1308
  *
939
- * `spawnSync` is used (over the simpler `execSync`) so stdout AND stderr are
940
- * captured regardless of exit code. Many CLIs follow the Unix convention of
941
- * writing status banners to stderr on success — `execSync` would silently
942
- * discard them, leaving snapshot tests with no output to assert on.
1309
+ * // With raw SQL init
1310
+ * const db = sqlite({ init: './schema.sql' });
1311
+ *
1312
+ * // Empty database
1313
+ * const db = sqlite();
943
1314
  */
944
- declare class ExecAdapter implements CommandPort {
945
- private command;
946
- constructor(command: string);
947
- exec(args: string, cwd: string, extraEnv?: CommandEnv): Promise<CommandResult>;
948
- spawn(args: string, cwd: string, options: SpawnOptions, extraEnv?: CommandEnv): Promise<CommandResult>;
1315
+ declare function sqlite(options?: SqliteOptions): SqliteHandle;
1316
+ //#endregion
1317
+ //#region src/integrations/anthropic/anthropic.d.ts
1318
+ interface AnthropicMessagesFilter {
1319
+ model?: RegExp | string;
1320
+ system?: RegExp | string;
1321
+ user?: RegExp | string;
1322
+ tools?: string[];
949
1323
  }
1324
+ declare function buildReply(data: unknown): InterceptResponse;
1325
+ /**
1326
+ * Anthropic API intercept helpers.
1327
+ */
1328
+ declare const anthropic: {
1329
+ /**
1330
+ * Trigger: match Messages API requests, optionally routed through a
1331
+ * custom gateway URL. When used with a JSON fixture file, the data is
1332
+ * returned as-is (no wrapping) because Anthropic fixtures are typically
1333
+ * already in the Messages API response shape.
1334
+ *
1335
+ * @example
1336
+ * anthropic.messages()
1337
+ * anthropic.messages({ system: /classify/ })
1338
+ * anthropic.messages({ user: /classify/ }, GATEWAY)
1339
+ */
1340
+ messages(filter?: AnthropicMessagesFilter, url?: string): InterceptTrigger;
1341
+ /** Response: wrap data in Anthropic messages format. */
1342
+ reply: typeof buildReply;
1343
+ /** Response: return an Anthropic error. */
1344
+ error(status: number, message?: string): InterceptResponse;
1345
+ /** Response: simulate a timeout. */
1346
+ timeout(): InterceptResponse;
1347
+ };
950
1348
  //#endregion
951
- //#region src/spec/modes/http/adapters/fetch.adapter.d.ts
1349
+ //#region src/core/contracts/http.d.ts
952
1350
  /**
953
- * Server adapter that sends real HTTP requests via the Fetch API.
954
- * Used by the `e2e()` specification runner to hit a live server.
1351
+ * Request filters for the generic HTTP provider. Every field is a subset
1352
+ * constraint a request matches when all provided fields match.
955
1353
  */
956
- declare class FetchAdapter implements ServerPort {
957
- private baseUrl;
958
- constructor(url: string);
959
- request(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<ServerResponse>;
1354
+ interface HttpInterceptFilter {
1355
+ /**
1356
+ * Body constraint. An object is a deep SUBSET match (toMatchObject-style)
1357
+ * whose leaf values may be `match.*` matchers; a string is a containment
1358
+ * test and a RegExp a `test()` over the raw text body.
1359
+ */
1360
+ body?: object | RegExp | string;
1361
+ /** Header subset. Names are case-insensitive; string = exact value, RegExp = `test()`. */
1362
+ headers?: Record<string, RegExp | string>;
1363
+ /** Query-param subset. string = exact value, RegExp = `test()`. */
1364
+ query?: Record<string, RegExp | string>;
960
1365
  }
1366
+ /**
1367
+ * Generic HTTP intercept helpers for any URL. An optional {@link
1368
+ * HttpInterceptFilter} narrows matching by request body, headers, or query —
1369
+ * a request that hits the URL/method but fails the filter counts as unmatched
1370
+ * (strict intercepts, CONVENTIONS D7).
1371
+ *
1372
+ * @example
1373
+ * .intercept(http.get('https://api.example.com/data'), 'http/response.json')
1374
+ * .intercept(http.post(URL, { body: { user: 'alice' } }), http.json({ ok: true }))
1375
+ */
1376
+ declare const http: {
1377
+ get(url: RegExp | string, filter?: HttpInterceptFilter): InterceptTrigger;
1378
+ post(url: RegExp | string, filter?: HttpInterceptFilter): InterceptTrigger;
1379
+ put(url: RegExp | string, filter?: HttpInterceptFilter): InterceptTrigger;
1380
+ delete(url: RegExp | string, filter?: HttpInterceptFilter): InterceptTrigger;
1381
+ any(url: RegExp | string, filter?: HttpInterceptFilter): InterceptTrigger;
1382
+ /** Response: simple JSON success. */
1383
+ json(data: unknown, status?: number): InterceptResponse;
1384
+ /** Response: error with message. */
1385
+ error(status: number, message?: string): InterceptResponse;
1386
+ };
961
1387
  //#endregion
962
- //#region src/spec/modes/http/adapters/hono.adapter.d.ts
1388
+ //#region src/integrations/openai/openai.d.ts
1389
+ interface OpenAIChatFilter {
1390
+ model?: RegExp | string;
1391
+ system?: RegExp | string;
1392
+ user?: RegExp | string;
1393
+ tools?: string[];
1394
+ temperature?: number;
1395
+ }
1396
+ interface OpenAIResponsesFilter {
1397
+ model?: RegExp | string;
1398
+ system?: RegExp | string;
1399
+ user?: RegExp | string;
1400
+ tools?: string[];
1401
+ }
1402
+ declare function buildChatReply(data: unknown): InterceptResponse;
963
1403
  /**
964
- * Server adapter that dispatches requests in-process through a Hono app instance.
965
- * Used by the `integration()` specification runner -- no network overhead.
1404
+ * OpenAI API intercept helpers.
966
1405
  */
967
- declare class HonoAdapter implements ServerPort {
968
- private app;
969
- constructor(app: {
970
- request: (path: string, init?: RequestInit) => Promise<Response> | Response;
971
- });
972
- request(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<ServerResponse>;
1406
+ declare const openai: {
1407
+ /**
1408
+ * Trigger: match Chat Completions API requests.
1409
+ *
1410
+ * @example
1411
+ * openai.chat() // any chat call
1412
+ * openai.chat({ model: 'gpt-4o' }) // specific model
1413
+ * openai.chat({ system: /classify/ }) // system prompt match
1414
+ */
1415
+ chat(filter?: OpenAIChatFilter): InterceptTrigger;
1416
+ /**
1417
+ * Trigger: match Responses API requests (AI SDK v5+) with auto-wrapping.
1418
+ * When used with a JSON file, the data is automatically wrapped in the
1419
+ * Responses API envelope.
1420
+ *
1421
+ * @param filter - Optional body filters.
1422
+ * @param url - Custom gateway URL (default: api.openai.com).
1423
+ *
1424
+ * @example
1425
+ * openai.responses({ user: /Report Ingestion/ }, GATEWAY)
1426
+ */
1427
+ responses(filter?: OpenAIResponsesFilter, url?: string): InterceptTrigger;
1428
+ /**
1429
+ * Response: wrap data in Chat Completions format.
1430
+ *
1431
+ * @example
1432
+ * openai.reply({ categories: ['TECH'] })
1433
+ */
1434
+ reply: typeof buildChatReply;
1435
+ /** Response: return an OpenAI error. */
1436
+ error(status: number, message?: string): InterceptResponse;
1437
+ /** Response: return malformed content. */
1438
+ malformed(content: string): InterceptResponse;
1439
+ /** Response: simulate a timeout. */
1440
+ timeout(): InterceptResponse;
1441
+ };
1442
+ //#endregion
1443
+ //#region src/vitest/mock-of.d.ts
1444
+ /** Factory signature that creates a deep mock proxy for any interface. */
1445
+ type MockPort = <T>() => DeepMockProxy<T>;
1446
+ /**
1447
+ * Create a deep mock proxy for a given type.
1448
+ * Wraps `vitest-mock-extended`'s `mockDeep` for convenient port mocking.
1449
+ */
1450
+ declare const mockOf: MockPort;
1451
+ //#endregion
1452
+ //#region src/vitest/mock-of-date.d.ts
1453
+ /** Interface for freezing and resetting the global Date in tests. */
1454
+ interface MockDatePort {
1455
+ /** Restore the real Date object. */
1456
+ reset: () => void;
1457
+ /** Freeze `Date.now()` and `new Date()` to the given value. */
1458
+ set: (date: Date | number | string) => void;
973
1459
  }
1460
+ /**
1461
+ * Freeze or reset the global Date for deterministic time-dependent tests.
1462
+ * Wraps the `mockdate` package.
1463
+ */
1464
+ declare const mockOfDate: MockDatePort;
974
1465
  //#endregion
975
- //#region src/infra/docker/docker.d.ts
976
- /** Create a {@link DockerContainerPort} bound to an existing container by ID. */
977
- declare function dockerContainer(containerId: string): DockerContainerPort;
1466
+ //#region src/index.d.ts
1467
+ declare module 'vitest' {
1468
+ interface Matchers<T = any> {
1469
+ /** Assert the table has zero rows. Async — queries the database. */
1470
+ toBeEmpty: T extends TableAccessor ? () => Promise<void> : never;
1471
+ /** Assert the container is running. Async — docker-backed subject. */
1472
+ toBeRunning: T extends ContainerAccessor ? () => Promise<void> : never;
1473
+ /**
1474
+ * Assert the table contains exactly the given rows for the given
1475
+ * columns. Cells accept `match.*` dynamic-value matchers. Async —
1476
+ * queries the database.
1477
+ */
1478
+ toMatchRows: T extends TableAccessor ? (expected: {
1479
+ columns: string[];
1480
+ rows: readonly (readonly unknown[])[];
1481
+ }) => Promise<void> : never;
1482
+ }
1483
+ interface Assertion<T = any> {
1484
+ /**
1485
+ * On `@jterrazz/test` accessors: assert the subject matches a fixture
1486
+ * file under `expected/<name>` (flat — a slash creates a subfolder;
1487
+ * `.http` format for `result.response`). Async for filesystem/directory
1488
+ * subjects (tree compare on disk). Other subjects keep vitest-native
1489
+ * `toMatch` semantics (string substring / regexp).
1490
+ *
1491
+ * Pass `{ frozen: true }` to opt a single fixture OUT of update-mode
1492
+ * rewriting: a frozen fixture is never overwritten under `TEST_UPDATE=1`
1493
+ * (or vitest `-u`), and a frozen mismatch/missing fixture still throws.
1494
+ * Use it for a deliberately-wrong fixture whose diff/error rendering is
1495
+ * the behaviour under test.
1496
+ */
1497
+ toMatch: T extends DirectoryAccessor | FilesystemAccessor ? (name: RegExp | string, options?: MatchFixtureOptions) => Promise<void> : T extends JsonAccessor | ResponseAccessor | TextAccessor ? (name: RegExp | string, options?: MatchFixtureOptions) => void : (expected: RegExp | string, options?: MatchFixtureOptions) => void;
1498
+ }
1499
+ }
978
1500
  //#endregion
979
- export { type AppFactoryResult, type AppServices, type AppTarget, BaseResult, CliResult, type CommandEnv, type CommandPort, type CommandResult, type CommandTarget, ContainerAccessor, type ContainerPort, type DatabasePort, DirectoryAccessor, type DirectorySnapshotOptions, DockerAssertion, type DockerContainerPort, type DockerInspectResult, type DockerSpecConfig, ExecAdapter, FetchAdapter, type FileAccessor, FilesystemAccessor, HonoAdapter, HttpResult, type HttpTarget, type IsolationStrategy, type JobHandle, JsonAccessor, type JsonSnapshotOptions, type MockDatePort, type MockPort, Orchestrator, type PostgresOptions, type RedisOptions, ResponseAccessor, type SeedHandler, type SeedHandlerContext, type ServerPort, type ServerResponse, type ServiceHandle, type SpawnOptions, type SpecOptions, type SpecRunner, type SpecTarget, SpecificationBuilder, type SpecificationConfig, type SpecificationRunner, type SqliteOptions, type StackTarget, StreamAccessor, type StreamSnapshotOptions, TableAssertion, app, command, createSpecificationRunner, dockerContainer, findContainersByLabel, inspectContainer, mockOf, mockOfDate, postgres, redis, removeContainers, spec, sqlite, stack };
980
- //# sourceMappingURL=index.d.ts.map
1501
+ export { type ApiHandle, type ApiSpecification, type ApiSpecificationOptions, BaseResult, type CaptureScope, type CliEnv, type CliHandle, type CliOutput, type CliPort, CliResult, type CliSpecification, type CliSpecificationOptions, ContainerAccessor, type ContainerPort, type DatabaseKeys, type DatabasePort, DirectoryAccessor, type DockerSpecConfig, type ExecOptions, type FileAccessor, FilesystemAccessor, type HonoApp, HttpResult, type InterceptContract, type InterceptEntry, type InterceptResponder, type InterceptResponse, type InterceptResponseValue, type InterceptTrigger, type IsolationStrategy, type JobHandle, type JobsHandle, type JobsSpecification, type JobsSpecificationOptions, JsonAccessor, type MatchFixtureOptions, type MatchableRequest, Matcher, type MatcherKind, type MockDatePort, type MockPort, Orchestrator, type PostgresOptions, type RedisOptions, ResponseAccessor, type ServerPort, type ServerResponse, type ServiceHandle, type ServiceRecord, type SpecificationConfig, type SpecificationMode, type SqliteOptions, TableAccessor, TextAccessor, anthropic, defineContract, findContainersByLabel, http, inspectContainer, match, mockOf, mockOfDate, openai, postgres, redis, removeContainers, specification, sqlite, text };