@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.
- package/README.md +209 -230
- package/dist/checker.d.ts +1 -0
- package/dist/checker.js +741 -0
- package/dist/index.d.ts +1296 -529
- package/dist/index.js +3303 -1261
- package/dist/intercept.js +129 -290
- package/dist/match.js +153 -0
- package/dist/oxlint.cjs +2931 -0
- package/dist/oxlint.d.cts +150 -0
- package/dist/oxlint.d.ts +150 -0
- package/dist/oxlint.js +2925 -0
- package/package.json +47 -41
- package/dist/chunk.cjs +0 -28
- package/dist/index.cjs +0 -1814
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -734
- package/dist/index.js.map +0 -1
- package/dist/intercept.cjs +0 -313
- package/dist/intercept.cjs.map +0 -1
- package/dist/intercept.d.cts +0 -105
- package/dist/intercept.d.ts +0 -105
- package/dist/intercept.js.map +0 -1
- package/dist/intercept2.cjs +0 -81
- package/dist/intercept2.cjs.map +0 -1
- package/dist/intercept2.js +0 -81
- package/dist/intercept2.js.map +0 -1
- package/dist/mock-of.cjs +0 -32
- package/dist/mock-of.cjs.map +0 -1
- package/dist/mock-of.d.cts +0 -27
- package/dist/mock-of.d.ts +0 -27
- package/dist/mock-of.js +0 -19
- package/dist/mock-of.js.map +0 -1
- package/dist/mock.cjs +0 -4
- package/dist/mock.d.cts +0 -2
- package/dist/mock.d.ts +0 -2
- package/dist/mock.js +0 -2
- package/dist/services.cjs +0 -5
- package/dist/services.d.cts +0 -2
- package/dist/services.d.ts +0 -2
- package/dist/services.js +0 -2
- package/dist/sqlite.adapter.cjs +0 -350
- package/dist/sqlite.adapter.cjs.map +0 -1
- package/dist/sqlite.adapter.d.cts +0 -209
- package/dist/sqlite.adapter.d.ts +0 -209
- package/dist/sqlite.adapter.js +0 -331
- package/dist/sqlite.adapter.js.map +0 -1
- package/dist/types.d.cts +0 -42
- package/dist/types.d.ts +0 -42
package/dist/index.d.ts
CHANGED
|
@@ -1,45 +1,200 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
*
|
|
28
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
33
|
-
*
|
|
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
|
-
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
|
|
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/
|
|
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>)
|
|
215
|
+
request: (method: string, path: string, body?: unknown, headers?: Record<string, string>) => Promise<ServerResponse>;
|
|
61
216
|
}
|
|
62
217
|
//#endregion
|
|
63
|
-
//#region src/
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
*
|
|
69
|
-
* `
|
|
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
|
-
|
|
327
|
+
grep(pattern: string): TextAccessor;
|
|
328
|
+
toString(): string;
|
|
329
|
+
valueOf(): string;
|
|
72
330
|
}
|
|
73
331
|
/**
|
|
74
|
-
*
|
|
75
|
-
*
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
*
|
|
83
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
89
|
-
*
|
|
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
|
-
|
|
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/
|
|
97
|
-
/**
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
106
|
-
|
|
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
|
-
|
|
528
|
+
release: () => Promise<void>;
|
|
109
529
|
}
|
|
110
530
|
//#endregion
|
|
111
|
-
//#region src/
|
|
112
|
-
/**
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
*
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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/
|
|
135
|
-
/** Read-only handle to a single file produced by a
|
|
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
|
-
*
|
|
143
|
-
*
|
|
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
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
|
|
189
|
-
}):
|
|
606
|
+
database?: string;
|
|
607
|
+
}): TableAccessor;
|
|
190
608
|
private resolveDatabase;
|
|
191
609
|
}
|
|
192
610
|
//#endregion
|
|
193
|
-
//#region src/
|
|
194
|
-
/**
|
|
195
|
-
|
|
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
|
-
/**
|
|
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?:
|
|
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
|
-
|
|
205
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
212
|
-
*
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
/**
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
env(env:
|
|
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
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
*
|
|
286
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
291
|
-
*
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
-
|
|
297
|
-
|
|
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
|
-
*
|
|
303
|
-
*
|
|
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
|
-
|
|
310
|
-
/**
|
|
311
|
-
|
|
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
|
-
*
|
|
314
|
-
*
|
|
315
|
-
*
|
|
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
|
-
|
|
855
|
+
file(path: string): FileAccessor;
|
|
321
856
|
/**
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
*
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
//#
|
|
346
|
-
|
|
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
|
-
|
|
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
|
|
939
|
+
* Get the default database — the first declared handle that is one.
|
|
475
940
|
*/
|
|
476
|
-
getDatabase(
|
|
941
|
+
getDatabase(): DatabasePort | null;
|
|
477
942
|
/**
|
|
478
|
-
* Get all database services keyed by
|
|
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/
|
|
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
|
|
966
|
+
type HonoApp = {
|
|
490
967
|
request: (path: string, init?: RequestInit) => Promise<Response> | Response;
|
|
491
968
|
};
|
|
492
|
-
/**
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
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
|
-
*
|
|
501
|
-
*
|
|
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
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
|
|
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
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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
|
-
*
|
|
528
|
-
*
|
|
1056
|
+
* The record returned by {@link startCli | specification.cli}. Destructure
|
|
1057
|
+
* with the canonical names (CONVENTIONS A3):
|
|
529
1058
|
*
|
|
530
|
-
*
|
|
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
|
-
|
|
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
|
-
*
|
|
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) =>
|
|
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
|
-
*
|
|
1090
|
+
* The record returned by {@link startJobs | specification.jobs}. Destructure
|
|
1091
|
+
* with the canonical names (CONVENTIONS A3):
|
|
586
1092
|
*
|
|
587
|
-
*
|
|
588
|
-
|
|
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
|
-
* //
|
|
592
|
-
* const
|
|
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
|
-
* //
|
|
595
|
-
* const
|
|
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
|
-
* //
|
|
598
|
-
* const
|
|
1125
|
+
* // specs/setup/cli.specification.ts
|
|
1126
|
+
* export const { cli, cleanup } = await specification.cli('my-cli');
|
|
1127
|
+
* afterAll(cleanup);
|
|
599
1128
|
*/
|
|
600
|
-
declare
|
|
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/
|
|
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()
|
|
1166
|
+
start: () => Promise<void>;
|
|
610
1167
|
/** Stop and remove the container. */
|
|
611
|
-
stop()
|
|
1168
|
+
stop: () => Promise<void>;
|
|
612
1169
|
/** Get the mapped host port for a container port. */
|
|
613
|
-
getMappedPort(containerPort: number)
|
|
1170
|
+
getMappedPort: (containerPort: number) => number;
|
|
614
1171
|
/** Get the host to connect to. */
|
|
615
|
-
getHost()
|
|
1172
|
+
getHost: () => string;
|
|
616
1173
|
/** Get a full connection string for this service. */
|
|
617
|
-
getConnectionString()
|
|
1174
|
+
getConnectionString: () => string;
|
|
618
1175
|
/** Get container logs (stdout + stderr). */
|
|
619
|
-
getLogs()
|
|
1176
|
+
getLogs: () => Promise<string>;
|
|
620
1177
|
}
|
|
621
1178
|
//#endregion
|
|
622
|
-
//#region src/
|
|
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
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
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
|
|
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/
|
|
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
|
-
*
|
|
638
|
-
*
|
|
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
|
|
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/
|
|
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
|
-
*
|
|
649
|
-
*
|
|
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
|
|
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/
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
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
|
-
*
|
|
681
|
-
* Starts infra containers via testcontainers, app runs in-process.
|
|
1326
|
+
* Anthropic API intercept helpers.
|
|
682
1327
|
*/
|
|
683
|
-
declare
|
|
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/
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
/**
|
|
692
|
-
|
|
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
|
-
*
|
|
696
|
-
*
|
|
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
|
|
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/
|
|
701
|
-
interface
|
|
702
|
-
|
|
703
|
-
|
|
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
|
-
*
|
|
707
|
-
* Starts full docker compose stack. App URL and database auto-detected.
|
|
1404
|
+
* OpenAI API intercept helpers.
|
|
708
1405
|
*/
|
|
709
|
-
declare
|
|
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/
|
|
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
|
-
*
|
|
714
|
-
*
|
|
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
|
|
1450
|
+
declare const mockOf: MockPort;
|
|
722
1451
|
//#endregion
|
|
723
|
-
//#region src/
|
|
724
|
-
/**
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
|
|
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/
|
|
730
|
-
|
|
731
|
-
|
|
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
|
|
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 };
|