@jterrazz/test 7.1.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 +1296 -529
  5. package/dist/index.js +3303 -1261
  6. package/dist/intercept.js +129 -290
  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 -1814
  15. package/dist/index.cjs.map +0 -1
  16. package/dist/index.d.cts +0 -734
  17. package/dist/index.js.map +0 -1
  18. package/dist/intercept.cjs +0 -313
  19. package/dist/intercept.cjs.map +0 -1
  20. package/dist/intercept.d.cts +0 -105
  21. package/dist/intercept.d.ts +0 -105
  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.adapter.cjs +0 -350
  42. package/dist/sqlite.adapter.cjs.map +0 -1
  43. package/dist/sqlite.adapter.d.cts +0 -209
  44. package/dist/sqlite.adapter.d.ts +0 -209
  45. package/dist/sqlite.adapter.js +0 -331
  46. package/dist/sqlite.adapter.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,45 +1,200 @@
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.adapter.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/builder/cli/command.port.d.ts
1
+ import { DeepMockProxy } from "vitest-mock-extended";
2
+ //#region src/core/matching/match.d.ts
6
3
  /**
7
- * Result of executing a CLI command, including exit code and captured output streams.
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).
8
11
  */
9
- interface CommandResult {
10
- /** Process exit code (0 = success). */
11
- exitCode: number;
12
- /** Captured standard output. */
13
- stdout: string;
14
- /** Captured standard error. */
15
- stderr: string;
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;
16
34
  }
17
35
  /**
18
- * Options for spawning a long-running process.
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
+ * });
44
+ */
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;
56
+ /**
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`).
60
+ */
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;
76
+ /**
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).
81
+ */
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.
19
108
  */
20
- interface SpawnOptions {
21
- /** Resolve when stdout/stderr contains this string. */
22
- waitFor: string;
23
- /** Kill the process after this many milliseconds. */
24
- timeout: number;
109
+ declare class CaptureScope {
110
+ private readonly values;
111
+ /**
112
+ * The exact working directory of the current spec, when the framework
113
+ * knows it (cli mode). Drives the `{{workdir}}` token / `match.workdir()`.
114
+ */
115
+ workdir?: string;
116
+ constructor(workdir?: string);
117
+ get(name: string): unknown;
118
+ has(name: string): boolean;
119
+ set(name: string, value: unknown): void;
25
120
  }
121
+ //#endregion
122
+ //#region src/core/specification/shared/result/directory.d.ts
26
123
  /**
27
- * Extra environment variables to set for the child process.
28
- * Values are merged on top of process.env. A `null` value unsets the variable.
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>/`.
29
129
  */
30
- type CommandEnv = Record<string, null | string>;
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
31
145
  /**
32
- * Abstract CLI interface for specification runners.
33
- * Implement this to plug in your command execution strategy.
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).
34
182
  */
35
- interface CommandPort {
36
- /** Execute a CLI command with the given arguments in the given working directory. */
37
- exec(args: string, cwd: string, env?: CommandEnv): Promise<CommandResult>;
38
- /** Spawn a long-running process and wait for a pattern or timeout. */
39
- spawn(args: string, cwd: string, options: SpawnOptions, env?: CommandEnv): Promise<CommandResult>;
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;
40
195
  }
41
196
  //#endregion
42
- //#region src/builder/http/server.port.d.ts
197
+ //#region src/core/ports/server.port.d.ts
43
198
  /**
44
199
  * HTTP response returned by a server port, with parsed JSON body.
45
200
  */
@@ -57,380 +212,682 @@ interface ServerResponse {
57
212
  */
58
213
  interface ServerPort {
59
214
  /** Send an HTTP request and return the parsed response. */
60
- request(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<ServerResponse>;
215
+ request: (method: string, path: string, body?: unknown, headers?: Record<string, string>) => Promise<ServerResponse>;
61
216
  }
62
217
  //#endregion
63
- //#region src/builder/common/directory-accessor.d.ts
64
- interface DirectorySnapshotOptions {
65
- /** Extra path segments to ignore (in addition to default: .git, node_modules, etc.). */
66
- ignore?: string[];
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;
67
320
  /**
68
- * Force update mode regardless of vitest flags / env vars.
69
- * `true` writes the fixture, `false` always asserts. Defaults to auto-detect.
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.
70
326
  */
71
- update?: boolean;
327
+ grep(pattern: string): TextAccessor;
328
+ toString(): string;
329
+ valueOf(): string;
72
330
  }
73
331
  /**
74
- * Handle to a directory produced by a specification run.
75
- * Supports snapshot-based assertions via {@link toMatchFixture} and file listing via {@link files}.
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.
76
347
  */
77
- declare class DirectoryAccessor {
78
- private absPath;
79
- private testDir;
80
- constructor(absPath: string, testDir: string);
348
+ declare function text(value: string): TextAccessor;
349
+ //#endregion
350
+ //#region src/core/ports/cli.port.d.ts
351
+ /**
352
+ * Raw output from a command execution, including exit code and captured output streams.
353
+ */
354
+ interface CliOutput {
355
+ /** Process exit code (0 = success). */
356
+ exitCode: number;
357
+ /** Captured standard output. */
358
+ stdout: string;
359
+ /** Captured standard error. */
360
+ stderr: string;
361
+ }
362
+ /**
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).
367
+ */
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;
373
+ }
374
+ /**
375
+ * Extra environment variables to set for the child process.
376
+ * Values are merged on top of process.env. A `null` value unsets the variable.
377
+ */
378
+ type CliEnv = Record<string, null | string>;
379
+ /**
380
+ * Abstract command interface for specification runners.
381
+ * Implement this to plug in your command execution strategy.
382
+ */
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>;
388
+ }
389
+ //#endregion
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;
403
+ }
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;
81
416
  /**
82
- * Compare the directory tree against `expected/{name}/` (relative to the test file).
83
- * On mismatch, throws with a structured diff. With update mode enabled, the
84
- * fixture is overwritten with the current contents instead.
417
+ * Transform raw JSON data into a provider-specific response envelope.
418
+ * Called when .intercept(trigger, 'adapter/file.json') loads a file.
85
419
  */
86
- toMatchFixture(name: string, options?: DirectorySnapshotOptions): Promise<void>;
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;
453
+ }
454
+ //#endregion
455
+ //#region src/core/contracts/contract.d.ts
456
+ /**
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).
463
+ */
464
+ interface InterceptContract {
465
+ trigger: InterceptTrigger;
87
466
  /**
88
- * List all files in the directory (recursive, sorted, ignoring defaults).
89
- * Useful for ad-hoc assertions when you don't want a full snapshot.
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.
90
470
  */
91
- files(options?: {
92
- ignore?: string[];
93
- }): Promise<string[]>;
471
+ response: InterceptResponseValue;
94
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;
95
498
  //#endregion
96
- //#region src/builder/common/response-accessor.d.ts
97
- /** Accessor for an HTTP response body with file-based assertion support. */
98
- declare class ResponseAccessor {
99
- readonly body: unknown;
100
- private testDir;
101
- constructor(body: unknown, testDir: string);
499
+ //#region src/core/ports/isolation.port.d.ts
500
+ /**
501
+ * Strategy for isolating service state across parallel test workers.
502
+ *
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.
506
+ *
507
+ * Implement this interface to support new service types (e.g. MongoDB,
508
+ * Elasticsearch, S3).
509
+ */
510
+ interface IsolationStrategy {
102
511
  /**
103
- * Assert that the response body matches the JSON in `responses/{file}`.
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.
104
515
  *
105
- * @example
106
- * result.response.toMatchFile("expected-items.json");
516
+ * @param workerId - Unique identifier for this vitest worker.
517
+ */
518
+ acquire: (workerId: string) => Promise<void>;
519
+ /**
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.
107
527
  */
108
- toMatchFile(file: string): void;
528
+ release: () => Promise<void>;
109
529
  }
110
530
  //#endregion
111
- //#region src/builder/common/table-assertion.d.ts
112
- /** Assertion helper for verifying database table contents after a specification run. */
113
- declare class TableAssertion {
114
- private tableName;
115
- private db;
116
- 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;
117
539
  /**
118
- * Assert that the table contains exactly the expected rows for the given columns.
119
- *
120
- * @example
121
- * await result.table("users").toMatch({
122
- * columns: ["name", "email"],
123
- * rows: [["Alice", "alice@example.com"]],
124
- * });
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.
125
546
  */
126
- toMatch(expected: {
127
- columns: string[];
128
- rows: unknown[][];
129
- }): Promise<void>;
130
- /** Assert that the table has zero rows. */
131
- 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;
132
570
  }
133
571
  //#endregion
134
- //#region src/builder/common/result.d.ts
135
- /** Read-only handle to a single file produced by a CLI action. */
572
+ //#region src/core/specification/shared/result/result.d.ts
573
+ /** Read-only handle to a single file produced by a spec action. */
136
574
  interface FileAccessor {
137
575
  /** The UTF-8 text content. Throws if the file does not exist. */
138
576
  readonly content: string;
139
577
  readonly exists: boolean;
578
+ /** The file text as a {@link TextAccessor}, keeping only blocks matching `pattern`. */
579
+ grep: (pattern: string) => TextAccessor;
580
+ }
581
+ interface BaseResultOptions {
582
+ config: SpecificationConfig;
583
+ testDir: string;
584
+ workDir?: string;
140
585
  }
141
586
  /**
142
- * The outcome of a single specification run.
143
- * Provides accessors for CLI output, HTTP responses, files, directories, and database tables.
587
+ * Base result - common accessors available after any action type.
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.
144
592
  */
145
- declare class SpecificationResult {
146
- private commandResult?;
147
- private config;
148
- private requestInfo?;
149
- private responseData?;
150
- private testDir;
151
- private workDir?;
152
- constructor(options: {
153
- commandResult?: CommandResult;
154
- config: SpecificationConfig;
155
- requestInfo?: {
156
- body?: unknown;
157
- method: string;
158
- path: string;
159
- };
160
- response?: ServerResponse;
161
- testDir: string;
162
- workDir?: string;
163
- });
164
- /** The process exit code. Only available after a CLI action. */
165
- get exitCode(): number;
166
- /** The HTTP response status code. Only available after an HTTP action. */
167
- get status(): number;
168
- /** The captured standard output. Only available after a CLI action. */
169
- get stdout(): string;
170
- /** The captured standard error. Only available after a CLI action. */
171
- get stderr(): string;
172
- /**
173
- * Extract text blocks from stdout that contain a pattern.
174
- * Useful for parsing structured CLI output (linters, compilers).
175
- *
176
- * @example
177
- * expect(result.grep('error.ts')).toContain('no-unused-vars');
178
- */
179
- grep(pattern: string): string;
180
- /** Access the HTTP response body for assertions. Only available after an HTTP action. */
181
- get response(): ResponseAccessor;
593
+ declare class BaseResult {
594
+ /** @internal Ref-capture scope shared by every assertion on this result. */
595
+ readonly captures: CaptureScope;
596
+ protected config: SpecificationConfig;
597
+ protected testDir: string;
598
+ protected workDir?: string;
599
+ constructor(options: BaseResultOptions);
182
600
  /** Access a directory (relative to the working directory) for snapshot assertions. */
183
601
  directory(path?: string): DirectoryAccessor;
184
602
  /** Access a single file (relative to the working directory) for content assertions. */
185
603
  file(path: string): FileAccessor;
186
- /** Access a database table for row-level assertions. */
604
+ /** Access a database table for row-level assertions via expect() matchers. */
187
605
  table(tableName: string, options?: {
188
- service?: string;
189
- }): TableAssertion;
606
+ database?: string;
607
+ }): TableAccessor;
190
608
  private resolveDatabase;
191
609
  }
192
610
  //#endregion
193
- //#region src/builder/specification-builder.d.ts
194
- /** A named job that can be triggered via .job(). */
195
- interface JobHandle$1 {
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 {
196
629
  name: string;
197
630
  execute: () => Promise<void>;
198
631
  }
199
- /** Adapter configuration passed to the specification runner at setup time. */
632
+ /**
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.
637
+ */
638
+ interface DockerSpecConfig {
639
+ envVar: string;
640
+ nameLabel: string;
641
+ testRunLabel: string;
642
+ }
643
+ /** Adapter configuration passed to the specification facets at setup time. */
200
644
  interface SpecificationConfig {
201
- command?: CommandPort;
645
+ command?: CliPort;
202
646
  database?: DatabasePort;
647
+ /**
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.
651
+ */
652
+ databaseKeys?: string[];
203
653
  databases?: Map<string, DatabasePort>;
204
- fixturesRoot?: string;
205
- jobs?: JobHandle$1[];
654
+ dockerConfig?: DockerSpecConfig;
655
+ /**
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.
660
+ */
661
+ dockerTestRunId?: string;
662
+ /**
663
+ * When set, `.intercept()` is unavailable on this runner and throws this
664
+ * reason immediately (compose mode — MSW is in-process, CONVENTIONS I3).
665
+ */
666
+ interceptDisabledReason?: string;
667
+ jobs?: JobHandle[];
206
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;
207
681
  }
208
682
  /**
209
- * Fluent builder for declaring a single test specification.
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.
210
686
  *
211
- * Chain setup methods ({@link seed}, {@link fixture}, {@link env}), an action
212
- * ({@link get}, {@link post}, {@link exec}), then call {@link run} to execute
213
- * and receive a {@link SpecificationResult} for assertions.
687
+ * The `DatabaseKey` parameter is the typed vocabulary of `.seed()`: the keys
688
+ * of the declared services record that are databases.
214
689
  */
215
- declare class SpecificationBuilder {
216
- private commandArgs;
217
- private commandEnv;
218
- private config;
219
- private fixtures;
220
- private intercepts;
221
- private jobName;
222
- private label;
223
- private mocks;
224
- private projectName;
225
- private request;
226
- private requestHeaders;
227
- private seeds;
228
- private spawnConfig;
229
- private testDir;
230
- constructor(config: SpecificationConfig, testDir: string, label: string);
231
- /**
232
- * Queue a SQL seed file to run before the action.
233
- *
234
- * @example
235
- * spec("creates user").seed("users.sql").exec("list-users").run();
236
- */
237
- seed(file: string, options?: {
238
- service?: string;
239
- }): this;
240
- /** Copy a file or directory from `fixtures/` into the working directory before execution. */
241
- fixture(file: string): this;
242
- /** Copy an entire project fixture from `fixturesRoot` into the working directory. */
243
- project(name: string): this;
244
- /** Register an MSW mock definition file from `mock/` to intercept HTTP calls. */
245
- mock(file: string): this;
246
- /**
247
- * Set environment variables for the CLI process. Merged on top of process.env.
248
- * Use `null` to unset a variable. Multiple calls merge.
249
- *
250
- * The token `$WORKDIR` (in any value) is replaced with the actual working
251
- * directory at run-time useful for tests that need a fully isolated `HOME`.
252
- *
253
- * @example
254
- * spec("...").env({ HOME: "$WORKDIR", TZ: "UTC" }).exec("status").run();
255
- */
256
- env(env: CommandEnv): this;
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>;
257
732
  /**
258
- * Set HTTP headers for the request. Multiple calls merge.
259
- *
260
- * @example
261
- * spec("french").headers({ 'Accept-Language': 'fr' }).get("/articles").run();
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.
262
738
  */
263
- headers(headers: Record<string, string>): this;
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>;
264
744
  /**
265
- * Intercept an outgoing HTTP request and return a controlled response.
266
- * Uses MSW under the hood. Intercepts are queued multiple calls with the
267
- * same trigger fire sequentially (first match consumed first).
268
- *
269
- * @param trigger - What to match (use openai.agent(), http.get(), etc.)
270
- * @param response - What to return: an InterceptResponse object, or a filename
271
- * from `intercepts/` (JSON loaded and auto-wrapped by the trigger).
272
- *
273
- * @example
274
- * // With inline response
275
- * .intercept(openai.agent({...}), openai.reply({ categories: ['TECH'] }))
276
- *
277
- * // With JSON fixture file (loaded from intercepts/ directory)
278
- * .intercept(openai.agent({...}), 'ingest-tech.json')
279
- * .intercept(http.get(url), 'world-news-tech.json')
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.
280
750
  */
281
- intercept(trigger: InterceptTrigger, response: InterceptResponse | string): this;
751
+ exec: (args?: string | string[], options?: ExecOptions) => Promise<CliResult>;
752
+ }
753
+ //#endregion
754
+ //#region src/core/specification/cli/result.d.ts
755
+ /**
756
+ * Result from a command action (`.exec()`).
757
+ *
758
+ * When the runner was configured with a `docker` option, the result also
759
+ * exposes container accessors and participates in `Symbol.asyncDispose`
760
+ * cleanup. The Docker shell-outs are **lazy**: a test that never calls
761
+ * `.container(...)` never queries the Docker daemon, so command-only tests
762
+ * pay zero Docker cost even when the runner is Docker-aware.
763
+ *
764
+ * Dispose always runs one final label-filtered `docker ps` to catch
765
+ * containers that were spawned during `.exec` but never asserted on —
766
+ * tests still get leak-free cleanup even if they forget to reach into
767
+ * a container.
768
+ */
769
+ declare class CliResult extends BaseResult {
770
+ private readonly commandOutput;
771
+ private containersCache;
772
+ private readonly dockerConfig?;
773
+ private readonly testRunId?;
774
+ private readonly transform?;
775
+ constructor(options: {
776
+ commandOutput: CliOutput;
777
+ config: SpecificationConfig;
778
+ dockerConfig?: DockerSpecConfig;
779
+ testDir: string;
780
+ testRunId?: string;
781
+ transform?: (text: string) => string;
782
+ workDir: string;
783
+ });
784
+ /** The process exit code. */
785
+ get exitCode(): number;
786
+ /** Accessor for the captured standard output with file-based assertions. */
787
+ get stdout(): TextAccessor;
788
+ /** Accessor for the captured standard error with file-based assertions. */
789
+ get stderr(): TextAccessor;
790
+ /** Accessor for parsing stdout as JSON and asserting against JSON fixtures. */
791
+ get json(): JsonAccessor;
792
+ /** Accessor for the temporary working directory the command ran in. */
793
+ get filesystem(): FilesystemAccessor;
282
794
  /**
283
- * Send a GET request to the server adapter.
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).
798
+ * First access triggers a one-shot `docker ps` + `docker inspect`
799
+ * query and caches the result for the rest of the result's lifetime.
800
+ * Tests that don't call this never touch Docker.
284
801
  *
285
- * @example
286
- * spec("list items").get("/api/items").run();
802
+ * Returns an accessor with `.exists === false` when no container
803
+ * carries that name — callers can still assert absence without a try.
287
804
  */
288
- get(path: string): this;
805
+ container(name: string): ContainerAccessor;
806
+ /** All captured container IDs. Triggers the same lazy query as `container()`. */
807
+ get containerIds(): string[];
808
+ [Symbol.asyncDispose](): Promise<void>;
809
+ private ensureDockerAware;
810
+ private loadContainers;
811
+ }
812
+ //#endregion
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?;
289
833
  /**
290
- * Send a POST request to the server adapter.
291
- *
292
- * @param bodyFile - Optional JSON file from `requests/` to use as the request body.
293
- * @example
294
- * spec("create item").post("/api/items", "new-item.json").run();
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.
295
838
  */
296
- post(path: string, bodyFile?: string): this;
297
- /** Send a PUT request to the server adapter. */
298
- put(path: string, bodyFile?: string): this;
299
- /** Send a DELETE request to the server adapter. */
300
- delete(path: string): this;
839
+ get id(): null | string;
840
+ constructor(containerId: null | string, inspectData: unknown, testDir: string, transform?: (text: string) => string);
301
841
  /**
302
- * Execute a CLI command (or a sequence of commands) in an isolated working directory.
303
- * When an array is passed, commands run sequentially and stop on the first non-zero exit code.
304
- *
305
- * @example
306
- * spec("init project").exec("init --name demo").run();
307
- * spec("multi-step").exec(["init", "build"]).run();
842
+ * Raw `docker inspect` object for this container. Throws if the container
843
+ * was not captured (i.e. `.exists === false`).
308
844
  */
309
- exec(args: string | string[]): this;
310
- /** Spawn a long-running CLI process with custom spawn options (e.g. timeout, signal). */
311
- spawn(args: string, options: SpawnOptions): this;
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;
312
850
  /**
313
- * Execute a named job registered via the app() factory.
314
- *
315
- * @param name - The job name to trigger (must match a registered JobHandle.name).
316
- *
317
- * @example
318
- * spec('pipeline').intercept(openai.chat(), openai.response({...})).job('report-refresh').run();
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.
319
854
  */
320
- job(name: string): this;
855
+ file(path: string): FileAccessor;
321
856
  /**
322
- * Execute the specification: run seeds, copy fixtures, then perform the
323
- * configured action (HTTP or CLI).
324
- *
325
- * @returns The result object used for assertions.
326
- * @example
327
- * const result = await spec("test").exec("status").run();
328
- * expect(result.exitCode).toBe(0);
329
- */
330
- run(): Promise<SpecificationResult>;
331
- private resolveEnv;
332
- private prepareWorkDir;
333
- private runHttpAction;
334
- private runJobAction;
335
- private runCliAction;
336
- }
337
- /** Factory function returned by `cli()`, `integration()`, or `e2e()` that starts a new spec. */
338
- type SpecificationRunner = (label: string) => SpecificationBuilder;
339
- /**
340
- * Create a {@link SpecificationRunner} bound to the given adapter configuration.
341
- * The test file directory is auto-detected from the call stack.
342
- */
343
- declare function createSpecificationRunner(config: SpecificationConfig): SpecificationRunner;
344
- //#endregion
345
- //#region src/docker/docker-port.d.ts
346
- /** Abstract interface for interacting with a Docker container (exec, inspect, file I/O). */
347
- interface DockerContainerPort {
348
- /** Execute a command inside the container, return stdout */
349
- exec(cmd: string[]): Promise<string>;
350
- /** Read a file from inside the container */
351
- file(path: string): Promise<{
352
- exists: boolean;
353
- content: string;
354
- }>;
355
- /** Check if container is running */
356
- isRunning(): Promise<boolean>;
357
- /** Get container logs */
358
- logs(tail?: number): Promise<string>;
359
- /** Get full docker inspect JSON */
360
- inspect(): Promise<DockerInspectResult>;
361
- /** Check if a file/directory exists */
362
- exists(path: string): Promise<boolean>;
363
- }
364
- /** Normalized subset of `docker inspect` output for assertion use. */
365
- interface DockerInspectResult {
366
- id: string;
367
- name: string;
368
- state: {
369
- running: boolean;
370
- exitCode: number;
371
- status: string;
372
- };
373
- config: {
374
- image: string;
375
- env: string[];
376
- };
377
- hostConfig: {
378
- memory: number;
379
- cpuQuota: number;
380
- networkMode: string;
381
- mounts: Array<{
382
- source: string;
383
- destination: string;
384
- type: string;
385
- }>;
386
- };
387
- networkSettings: {
388
- networks: Record<string, {
389
- gateway: string;
390
- ipAddress: string;
391
- }>;
392
- };
393
- }
394
- //#endregion
395
- //#region src/docker/docker-assertion.d.ts
396
- /**
397
- * Fluent assertion builder for Docker containers.
398
- * Chain async assertions like `toBeRunning()`, `toHaveFile()`, `toHaveMount()`.
399
- */
400
- declare class DockerAssertion {
401
- private container;
402
- constructor(container: DockerContainerPort);
403
- /** Assert the container is running */
404
- toBeRunning(): Promise<this>;
405
- /** Assert the container is NOT running / doesn't exist */
406
- toNotExist(): Promise<this>;
407
- /** Assert a file exists inside the container */
408
- toHaveFile(path: string, opts?: {
409
- containing?: string;
410
- }): Promise<this>;
411
- /** Assert a file does NOT exist */
412
- toNotHaveFile(path: string): Promise<this>;
413
- /** Assert a directory exists */
414
- toHaveDirectory(path: string): Promise<this>;
415
- /** Assert a mount exists */
416
- toHaveMount(destination: string): Promise<this>;
417
- /** Assert network mode */
418
- toHaveNetwork(mode: string): Promise<this>;
419
- /** Assert memory limit */
420
- toHaveMemoryLimit(bytes: number): Promise<this>;
421
- /** Assert CPU quota */
422
- toHaveCpuQuota(quota: number): Promise<this>;
423
- /** Execute a command and return output for custom assertions */
424
- exec(cmd: string[]): Promise<string>;
425
- /** Read a file for custom assertions */
426
- readFile(path: string): Promise<string>;
427
- /** Get logs for custom assertions */
428
- getLogs(tail?: number): Promise<string>;
429
- }
430
- //#endregion
431
- //#region src/infra/orchestrator.d.ts
857
+ * Run a shell command inside the container and get back the same
858
+ * {@link CliResult} used for host-side executions.
859
+ */
860
+ exec(cmd: string): Promise<CliResult>;
861
+ private requireExists;
862
+ private loadLogs;
863
+ private containerExec;
864
+ }
865
+ //#endregion
866
+ //#region src/vitest/matchers.d.ts
867
+ /**
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.
876
+ */
877
+ interface MatchFixtureOptions {
878
+ frozen?: boolean;
879
+ }
880
+ //#endregion
881
+ //#region src/core/specification/shared/orchestrator.d.ts
432
882
  interface OrchestratorOptions {
433
- services: ServiceHandle[];
883
+ /**
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}.
889
+ */
890
+ services: Record<string, ServiceHandle>;
434
891
  mode: 'e2e' | 'integration';
435
892
  root?: string;
436
893
  /** Compose project name — used for per-worker stack isolation. */
@@ -451,6 +908,14 @@ declare class Orchestrator {
451
908
  private composeHandles;
452
909
  private started;
453
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.
917
+ */
918
+ private resolveBindings;
454
919
  /**
455
920
  * Start declared services via testcontainers (integration mode).
456
921
  * Phase 1: start all containers in parallel (the slow part).
@@ -471,264 +936,566 @@ declare class Orchestrator {
471
936
  */
472
937
  stopCompose(): Promise<void>;
473
938
  /**
474
- * Get a database service by compose name, or the first one if no name given.
939
+ * Get the default database the first declared handle that is one.
475
940
  */
476
- getDatabase(serviceName?: string): DatabasePort | null;
941
+ getDatabase(): DatabasePort | null;
477
942
  /**
478
- * Get all database services keyed by compose name.
943
+ * Get all database services keyed by their record key (declared services)
944
+ * or compose service name (stack-detected services).
479
945
  */
480
946
  getDatabases(): Map<string, DatabasePort>;
947
+ private allHandles;
481
948
  /**
482
949
  * Get app URL from compose (e2e mode).
483
950
  */
484
951
  getAppUrl(): null | string;
485
952
  }
486
953
  //#endregion
487
- //#region src/spec/targets.d.ts
954
+ //#region src/core/specification/shared/services.d.ts
955
+ /**
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.
959
+ */
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];
963
+ //#endregion
964
+ //#region src/core/specification/api/start-api.d.ts
488
965
  /** Any object with a request method compatible with Hono's app.request(). */
489
- type HonoApp$1 = {
966
+ type HonoApp = {
490
967
  request: (path: string, init?: RequestInit) => Promise<Response> | Response;
491
968
  };
492
- /** A named job that can be triggered via .job(). */
493
- interface JobHandle {
494
- name: string;
495
- execute: () => Promise<void>;
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;
496
1001
  }
497
- /** Services map passed to the app factory after infrastructure starts. */
498
- type AppServices = Record<string, ServiceHandle>;
499
1002
  /**
500
- * Return value from the app() factory. Either a bare server (backward compat)
501
- * or an object with server + jobs for job-based testing.
1003
+ * The record returned by {@link startApi | specification.api}. Destructure
1004
+ * with the canonical names (CONVENTIONS A3):
1005
+ *
1006
+ * const { api, cleanup, docker } = await specification.api(…);
502
1007
  */
503
- type AppFactoryResult = HonoApp$1 | {
504
- server: HonoApp$1;
505
- jobs: JobHandle[];
506
- };
507
- /** In-process Hono app target. Created by {@link app}. */
508
- interface AppTarget {
509
- readonly kind: 'app';
510
- readonly factory: (services: AppServices) => AppFactoryResult;
511
- }
512
- /** Docker compose stack target. Created by {@link stack}. */
513
- interface StackTarget {
514
- readonly kind: 'stack';
515
- readonly root: string;
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 id — returns 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;
516
1019
  }
517
- /** CLI command target. Created by {@link command}. */
518
- interface CommandTarget {
519
- readonly kind: 'command';
520
- readonly bin: string;
1020
+ declare function startApi<Services extends ServiceRecord>(options: ApiSpecificationOptions<Services>): Promise<ApiHandle<DatabaseKeys<Services>>>;
1021
+ //#endregion
1022
+ //#region src/core/specification/cli/start-cli.d.ts
1023
+ /** Options for {@link startCli | specification.cli}. */
1024
+ interface CliSpecificationOptions<Services extends ServiceRecord = ServiceRecord> {
1025
+ /**
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).
1031
+ */
1032
+ docker?: DockerSpecConfig;
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
+ */
1039
+ root?: string;
1040
+ /**
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.
1045
+ */
1046
+ services?: Services;
1047
+ /**
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.
1052
+ */
1053
+ transform?: (text: string) => string;
521
1054
  }
522
- /** Any target that produces an HTTP interface (app or stack). */
523
- type HttpTarget = AppTarget | StackTarget;
524
- /** Any valid spec target. */
525
- type SpecTarget = AppTarget | CommandTarget | StackTarget;
526
1055
  /**
527
- * Test against an in-process Hono app. The factory receives started services
528
- * so you can wire connection strings into your app/DI container.
1056
+ * The record returned by {@link startCli | specification.cli}. Destructure
1057
+ * with the canonical names (CONVENTIONS A3):
529
1058
  *
530
- * @param factory - Function that receives services and returns a Hono app instance.
531
- *
532
- * @example
533
- * const db = postgres({ compose: 'db' });
534
- * await spec(
535
- * app((services) => createApp({ databaseUrl: services.db.connectionString })),
536
- * { services: [db] },
537
- * );
1059
+ * const { cli, cleanup, docker } = await specification.cli(…);
538
1060
  */
539
- declare function app(factory: (services: AppServices) => HonoApp$1): AppTarget;
540
- /**
541
- * Test against a full docker compose stack. The stack is started with
542
- * `docker compose up` and real HTTP requests are sent to the app service.
543
- *
544
- * @param root - Project root containing `docker/compose.test.yaml`.
545
- *
546
- * @example
547
- * await spec(stack('../../'));
548
- */
549
- declare function stack(root: string): StackTarget;
550
- /**
551
- * Test a CLI binary. Each spec runs in a fresh temp directory.
552
- *
553
- * @param bin - Path to the CLI binary or command name (resolved from node_modules/.bin or PATH).
554
- *
555
- * @example
556
- * await spec(command('my-cli'), { root: '../fixtures' });
557
- */
558
- declare function command(bin: string): CommandTarget;
559
- //#endregion
560
- //#region src/spec/spec.d.ts
561
- /** Shared options for all spec targets. */
562
- interface SpecOptions {
563
- /** Project root for fixture lookup and compose detection (relative paths supported). */
564
- root?: string;
565
- /** Infrastructure services to start via testcontainers. */
566
- services?: ServiceHandle[];
567
- }
568
- /** A specification runner with teardown support and orchestrator access. */
569
- interface SpecRunner {
570
- /** The function to create individual specs: `runner('label').get('/path').run()`. */
571
- (label: string): SpecificationBuilder;
572
- /** Stop all infrastructure started by this runner. */
1061
+ interface CliHandle<DatabaseKey extends string = string> {
1062
+ /** Stop all infrastructure started by this specification. */
573
1063
  cleanup: () => Promise<void>;
1064
+ cli: CliSpecification<DatabaseKey>;
574
1065
  /**
575
- * Get a Docker assertion builder for a running container.
576
- *
577
- * @example
578
- * 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.
579
1068
  */
580
- docker: (containerId: string) => DockerAssertion;
1069
+ docker: (containerId: string) => ContainerAccessor;
581
1070
  /** The orchestrator managing the test infrastructure lifecycle. */
582
- 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;
583
1088
  }
584
1089
  /**
585
- * 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):
586
1092
  *
587
- * @param target - What to test against: {@link app}, {@link stack}, or {@link command}.
588
- * @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).
589
1109
  *
590
1110
  * @example
591
- * // HTTP — in-process app with testcontainers
592
- * 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);
593
1117
  *
594
- * // HTTP — full docker compose stack
595
- * 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);
596
1124
  *
597
- * // CLI — command binary
598
- * 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);
599
1128
  */
600
- 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
+ };
1150
+ //#endregion
1151
+ //#region src/integrations/docker/docker-lookup.d.ts
1152
+ /** Return all container IDs (running or stopped) that carry `key=value`. */
1153
+ declare function findContainersByLabel(key: string, value: string): string[];
1154
+ /** Return the raw `docker inspect` payload (object, not array) for a container. */
1155
+ declare function inspectContainer(id: string): unknown;
1156
+ /** Force-remove the given container IDs in a single call. Errors are swallowed. */
1157
+ declare function removeContainers(ids: string[]): void;
601
1158
  //#endregion
602
- //#region src/infra/ports/container.port.d.ts
1159
+ //#region src/core/ports/container.port.d.ts
603
1160
  /**
604
1161
  * Abstract container interface.
605
1162
  * Represents a running service (database, cache, etc.)
606
1163
  */
607
1164
  interface ContainerPort {
608
1165
  /** Start the container and wait until ready. */
609
- start(): Promise<void>;
1166
+ start: () => Promise<void>;
610
1167
  /** Stop and remove the container. */
611
- stop(): Promise<void>;
1168
+ stop: () => Promise<void>;
612
1169
  /** Get the mapped host port for a container port. */
613
- getMappedPort(containerPort: number): number;
1170
+ getMappedPort: (containerPort: number) => number;
614
1171
  /** Get the host to connect to. */
615
- getHost(): string;
1172
+ getHost: () => string;
616
1173
  /** Get a full connection string for this service. */
617
- getConnectionString(): string;
1174
+ getConnectionString: () => string;
618
1175
  /** Get container logs (stdout + stderr). */
619
- getLogs(): Promise<string>;
1176
+ getLogs: () => Promise<string>;
620
1177
  }
621
1178
  //#endregion
622
- //#region src/builder/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
+ }
623
1213
  /**
624
- * Executes CLI commands via Node.js child_process.
625
- * Uses `execSync` for one-shot commands and `spawn` for long-running processes.
626
- * Used by the `cli()` specification runner.
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
627
1223
  */
628
- declare class ExecAdapter implements CommandPort {
629
- private command;
630
- constructor(command: string);
631
- exec(args: string, cwd: string, extraEnv?: CommandEnv): Promise<CommandResult>;
632
- spawn(args: string, cwd: string, options: SpawnOptions, extraEnv?: CommandEnv): Promise<CommandResult>;
633
- }
1224
+ declare function postgres(options?: PostgresOptions): PostgresHandle;
634
1225
  //#endregion
635
- //#region src/builder/http/adapters/fetch.adapter.d.ts
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
+ }
636
1253
  /**
637
- * Server adapter that sends real HTTP requests via the Fetch API.
638
- * Used by the `e2e()` specification runner to hit a live server.
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
639
1260
  */
640
- declare class FetchAdapter implements ServerPort {
641
- private baseUrl;
642
- constructor(url: string);
643
- request(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<ServerResponse>;
644
- }
1261
+ declare function redis(options?: RedisOptions): RedisHandle;
645
1262
  //#endregion
646
- //#region src/builder/http/adapters/hono.adapter.d.ts
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
+ }
647
1302
  /**
648
- * Server adapter that dispatches requests in-process through a Hono app instance.
649
- * Used by the `integration()` specification runner -- no network overhead.
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' });
1308
+ *
1309
+ * // With raw SQL init
1310
+ * const db = sqlite({ init: './schema.sql' });
1311
+ *
1312
+ * // Empty database
1313
+ * const db = sqlite();
650
1314
  */
651
- declare class HonoAdapter implements ServerPort {
652
- private app;
653
- constructor(app: {
654
- request: (path: string, init?: RequestInit) => Promise<Response> | Response;
655
- });
656
- request(method: string, path: string, body?: unknown, headers?: Record<string, string>): Promise<ServerResponse>;
657
- }
1315
+ declare function sqlite(options?: SqliteOptions): SqliteHandle;
658
1316
  //#endregion
659
- //#region src/spec/legacy-integration.d.ts
660
- type HonoApp = {
661
- fetch: (...args: any[]) => any;
662
- request: (path: string, init?: RequestInit) => Promise<Response> | Response;
663
- };
664
- interface IntegrationOptions {
665
- /** Factory that returns a Hono app — called after services start. */
666
- app: () => HonoApp;
667
- /** Project root for compose detection (relative paths supported). */
668
- root?: string;
669
- /** Declared services — started via testcontainers. */
670
- services: ServiceHandle[];
671
- }
672
- /** A specification runner that also exposes teardown and the underlying orchestrator. */
673
- interface SpecificationRunnerWithCleanup extends SpecificationRunner {
674
- /** Stop all infrastructure containers started by this runner. */
675
- cleanup: () => Promise<void>;
676
- /** The orchestrator managing the test infrastructure lifecycle. */
677
- orchestrator: Orchestrator;
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[];
678
1323
  }
1324
+ declare function buildReply(data: unknown): InterceptResponse;
679
1325
  /**
680
- * Create an integration specification runner.
681
- * Starts infra containers via testcontainers, app runs in-process.
1326
+ * Anthropic API intercept helpers.
682
1327
  */
683
- declare function integration(options: IntegrationOptions): Promise<SpecificationRunnerWithCleanup>;
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
+ };
684
1348
  //#endregion
685
- //#region src/spec/legacy-cli.d.ts
686
- interface CliOptions {
687
- /** CLI command to run (resolved from node_modules/.bin or PATH). */
688
- command: string;
689
- /** Project root — base dir for .project() fixture lookup (relative paths supported). */
690
- root?: string;
691
- /** Optional infrastructure services (started via testcontainers). */
692
- services?: ServiceHandle[];
1349
+ //#region src/core/contracts/http.d.ts
1350
+ /**
1351
+ * Request filters for the generic HTTP provider. Every field is a subset
1352
+ * constraint — a request matches when all provided fields match.
1353
+ */
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>;
693
1365
  }
694
1366
  /**
695
- * Create a CLI specification runner.
696
- * Runs CLI commands against fixture projects. Optionally starts infrastructure.
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 }))
697
1375
  */
698
- declare function cli(options: CliOptions): Promise<SpecificationRunnerWithCleanup>;
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
+ };
699
1387
  //#endregion
700
- //#region src/spec/legacy-e2e.d.ts
701
- interface E2eOptions {
702
- /** Project root — must contain docker/compose.test.yaml. */
703
- root?: string;
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[];
704
1401
  }
1402
+ declare function buildChatReply(data: unknown): InterceptResponse;
705
1403
  /**
706
- * Create an E2E specification runner.
707
- * Starts full docker compose stack. App URL and database auto-detected.
1404
+ * OpenAI API intercept helpers.
708
1405
  */
709
- declare function e2e(options?: E2eOptions): Promise<SpecificationRunnerWithCleanup>;
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
+ };
710
1442
  //#endregion
711
- //#region src/builder/common/grep.d.ts
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>;
712
1446
  /**
713
- * Extract text blocks from output that contain a pattern.
714
- * Splits by blank lines (how linter/compiler output is structured),
715
- * returns only blocks matching the pattern.
716
- *
717
- * @example
718
- * expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
719
- * expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
1447
+ * Create a deep mock proxy for a given type.
1448
+ * Wraps `vitest-mock-extended`'s `mockDeep` for convenient port mocking.
720
1449
  */
721
- declare function grep(output: string, pattern: string): string;
1450
+ declare const mockOf: MockPort;
722
1451
  //#endregion
723
- //#region src/builder/common/reporter.d.ts
724
- /** Strip ANSI escape codes from a string. */
725
- declare function stripAnsi(str: string): string;
726
- /** Strip ANSI codes and replace volatile tokens (ports, durations) with placeholders. */
727
- declare function normalizeOutput(str: string): string;
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;
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;
728
1465
  //#endregion
729
- //#region src/docker/docker-adapter.d.ts
730
- /** Create a {@link DockerContainerPort} bound to an existing container by ID. */
731
- 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
+ }
732
1500
  //#endregion
733
- export { type AppFactoryResult, type AppServices, type AppTarget, type CliOptions, type CommandEnv, type CommandPort, type CommandResult, type CommandTarget, type ContainerPort, type DatabasePort, DirectoryAccessor, type DirectorySnapshotOptions, DockerAssertion, type DockerContainerPort, type DockerInspectResult, type E2eOptions, ExecAdapter, FetchAdapter, type FileAccessor, HonoAdapter, type HttpTarget, type IntegrationOptions, type IsolationStrategy, type JobHandle, type MockDatePort, type MockPort, Orchestrator, type PostgresOptions, type RedisOptions, ResponseAccessor, type ServerPort, type ServerResponse, type ServiceHandle, type SpawnOptions, type SpecOptions, type SpecRunner, type SpecTarget, SpecificationBuilder, type SpecificationConfig, SpecificationResult, type SpecificationRunner, type SpecificationRunnerWithCleanup, type SqliteOptions, type StackTarget, TableAssertion, app, cli, command, createSpecificationRunner, dockerContainer, e2e, grep, integration, mockOf, mockOfDate, normalizeOutput, postgres, redis, spec, sqlite, stack, stripAnsi };
734
- //# 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 };