@jterrazz/test 5.3.2 → 6.1.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 +66 -56
- package/dist/chunk.cjs +28 -0
- package/dist/index.cjs +597 -463
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +343 -246
- package/dist/index.d.ts +343 -246
- package/dist/index.js +589 -438
- package/dist/index.js.map +1 -1
- package/dist/mock-of.cjs +32 -0
- package/dist/mock-of.cjs.map +1 -0
- package/dist/mock-of.d.cts +27 -0
- package/dist/mock-of.d.ts +27 -0
- package/dist/mock-of.js +19 -0
- package/dist/mock-of.js.map +1 -0
- package/dist/mock.cjs +4 -0
- package/dist/mock.d.cts +2 -0
- package/dist/mock.d.ts +2 -0
- package/dist/mock.js +2 -0
- package/dist/services.cjs +5 -0
- package/dist/services.d.cts +2 -0
- package/dist/services.d.ts +2 -0
- package/dist/services.js +2 -0
- package/dist/sqlite.adapter.cjs +331 -0
- package/dist/sqlite.adapter.cjs.map +1 -0
- package/dist/sqlite.adapter.d.cts +209 -0
- package/dist/sqlite.adapter.d.ts +209 -0
- package/dist/sqlite.adapter.js +312 -0
- package/dist/sqlite.adapter.js.map +1 -0
- package/package.json +16 -8
package/dist/index.cjs
CHANGED
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
-
var __copyProps = (to, from, except, desc) => {
|
|
10
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
-
key = keys[i];
|
|
12
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
-
get: ((k) => from[k]).bind(null, key),
|
|
14
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
return to;
|
|
18
|
-
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
-
value: mod,
|
|
21
|
-
enumerable: true
|
|
22
|
-
}) : target, mod));
|
|
23
|
-
//#endregion
|
|
2
|
+
require("./chunk.cjs");
|
|
3
|
+
const require_sqlite_adapter = require("./sqlite.adapter.cjs");
|
|
4
|
+
const require_mock_of = require("./mock-of.cjs");
|
|
24
5
|
let node_child_process = require("node:child_process");
|
|
25
6
|
let node_fs = require("node:fs");
|
|
26
7
|
let node_os = require("node:os");
|
|
27
8
|
let node_path = require("node:path");
|
|
28
9
|
let node_fs_promises = require("node:fs/promises");
|
|
29
|
-
let pg = require("pg");
|
|
30
10
|
let yaml = require("yaml");
|
|
31
|
-
let mockdate = require("mockdate");
|
|
32
|
-
mockdate = __toESM(mockdate);
|
|
33
|
-
let vitest_mock_extended = require("vitest-mock-extended");
|
|
34
11
|
//#region src/adapters/exec.adapter.ts
|
|
35
12
|
/**
|
|
36
13
|
* Build a child-process env from the parent env plus user overrides.
|
|
@@ -46,8 +23,9 @@ function buildEnv(extra) {
|
|
|
46
23
|
return env;
|
|
47
24
|
}
|
|
48
25
|
/**
|
|
49
|
-
* Executes CLI commands via
|
|
50
|
-
*
|
|
26
|
+
* Executes CLI commands via Node.js child_process.
|
|
27
|
+
* Uses `execSync` for one-shot commands and `spawn` for long-running processes.
|
|
28
|
+
* Used by the `cli()` specification runner.
|
|
51
29
|
*/
|
|
52
30
|
var ExecAdapter = class {
|
|
53
31
|
command;
|
|
@@ -127,6 +105,80 @@ var ExecAdapter = class {
|
|
|
127
105
|
}
|
|
128
106
|
};
|
|
129
107
|
//#endregion
|
|
108
|
+
//#region src/adapters/fetch.adapter.ts
|
|
109
|
+
/**
|
|
110
|
+
* Server adapter that sends real HTTP requests via the Fetch API.
|
|
111
|
+
* Used by the `e2e()` specification runner to hit a live server.
|
|
112
|
+
*/
|
|
113
|
+
var FetchAdapter = class {
|
|
114
|
+
baseUrl;
|
|
115
|
+
constructor(url) {
|
|
116
|
+
this.baseUrl = url.replace(/\/$/, "");
|
|
117
|
+
}
|
|
118
|
+
async request(method, path, body) {
|
|
119
|
+
const init = {
|
|
120
|
+
method,
|
|
121
|
+
headers: { "Content-Type": "application/json" }
|
|
122
|
+
};
|
|
123
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
124
|
+
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
125
|
+
const responseBody = await response.json().catch(() => null);
|
|
126
|
+
const headers = {};
|
|
127
|
+
response.headers.forEach((value, key) => {
|
|
128
|
+
headers[key] = value;
|
|
129
|
+
});
|
|
130
|
+
return {
|
|
131
|
+
status: response.status,
|
|
132
|
+
body: responseBody,
|
|
133
|
+
headers
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
//#endregion
|
|
138
|
+
//#region src/adapters/hono.adapter.ts
|
|
139
|
+
/**
|
|
140
|
+
* Server adapter that dispatches requests in-process through a Hono app instance.
|
|
141
|
+
* Used by the `integration()` specification runner -- no network overhead.
|
|
142
|
+
*/
|
|
143
|
+
var HonoAdapter = class {
|
|
144
|
+
app;
|
|
145
|
+
constructor(app) {
|
|
146
|
+
this.app = app;
|
|
147
|
+
}
|
|
148
|
+
async request(method, path, body) {
|
|
149
|
+
const init = {
|
|
150
|
+
method,
|
|
151
|
+
headers: { "Content-Type": "application/json" }
|
|
152
|
+
};
|
|
153
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
154
|
+
const response = await this.app.request(path, init);
|
|
155
|
+
const responseBody = await response.json().catch(() => null);
|
|
156
|
+
const headers = {};
|
|
157
|
+
response.headers.forEach((value, key) => {
|
|
158
|
+
headers[key] = value;
|
|
159
|
+
});
|
|
160
|
+
return {
|
|
161
|
+
status: response.status,
|
|
162
|
+
body: responseBody,
|
|
163
|
+
headers
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/utilities/grep.ts
|
|
169
|
+
/**
|
|
170
|
+
* Extract text blocks from output that contain a pattern.
|
|
171
|
+
* Splits by blank lines (how linter/compiler output is structured),
|
|
172
|
+
* returns only blocks matching the pattern.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
|
|
176
|
+
* expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
|
|
177
|
+
*/
|
|
178
|
+
function grep(output, pattern) {
|
|
179
|
+
return output.replace(/\x1b\[[0-9;]*m/g, "").split(/\n\s*\n/).filter((block) => block.includes(pattern)).join("\n\n");
|
|
180
|
+
}
|
|
181
|
+
//#endregion
|
|
130
182
|
//#region src/utilities/directory.ts
|
|
131
183
|
/**
|
|
132
184
|
* Default ignore patterns — paths that should never appear in a tracked snapshot.
|
|
@@ -325,9 +377,11 @@ function rowLabel(n) {
|
|
|
325
377
|
function formatRow(row) {
|
|
326
378
|
return row.map((v) => String(v ?? "null")).join(" | ");
|
|
327
379
|
}
|
|
380
|
+
/** Strip ANSI escape codes from a string. */
|
|
328
381
|
function stripAnsi(str) {
|
|
329
382
|
return str.replace(/\x1b\[[0-9;]*m/g, "");
|
|
330
383
|
}
|
|
384
|
+
/** Strip ANSI codes and replace volatile tokens (ports, durations) with placeholders. */
|
|
331
385
|
function normalizeOutput(str) {
|
|
332
386
|
return stripAnsi(str).replace(/localhost:\d+/g, "localhost:PORT").replace(/\d+ms/g, "Xms").replace(/\d+\.\d+s/g, "X.Xs").trim();
|
|
333
387
|
}
|
|
@@ -345,6 +399,10 @@ function shouldUpdateSnapshots() {
|
|
|
345
399
|
if (process.argv.includes("-u") || process.argv.includes("--update")) return true;
|
|
346
400
|
return false;
|
|
347
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* Handle to a directory produced by a specification run.
|
|
404
|
+
* Supports snapshot-based assertions via {@link toMatchFixture} and file listing via {@link files}.
|
|
405
|
+
*/
|
|
348
406
|
var DirectoryAccessor = class {
|
|
349
407
|
absPath;
|
|
350
408
|
testDir;
|
|
@@ -383,6 +441,7 @@ var DirectoryAccessor = class {
|
|
|
383
441
|
};
|
|
384
442
|
//#endregion
|
|
385
443
|
//#region src/builder/response-accessor.ts
|
|
444
|
+
/** Accessor for an HTTP response body with file-based assertion support. */
|
|
386
445
|
var ResponseAccessor = class {
|
|
387
446
|
body;
|
|
388
447
|
testDir;
|
|
@@ -390,6 +449,12 @@ var ResponseAccessor = class {
|
|
|
390
449
|
this.body = body;
|
|
391
450
|
this.testDir = testDir;
|
|
392
451
|
}
|
|
452
|
+
/**
|
|
453
|
+
* Assert that the response body matches the JSON in `responses/{file}`.
|
|
454
|
+
*
|
|
455
|
+
* @example
|
|
456
|
+
* result.response.toMatchFile("expected-items.json");
|
|
457
|
+
*/
|
|
393
458
|
toMatchFile(file) {
|
|
394
459
|
const expected = JSON.parse((0, node_fs.readFileSync)((0, node_path.resolve)(this.testDir, "responses", file), "utf8"));
|
|
395
460
|
if (JSON.stringify(this.body) !== JSON.stringify(expected)) throw new Error(formatResponseDiff(file, expected, this.body));
|
|
@@ -397,6 +462,7 @@ var ResponseAccessor = class {
|
|
|
397
462
|
};
|
|
398
463
|
//#endregion
|
|
399
464
|
//#region src/builder/table-assertion.ts
|
|
465
|
+
/** Assertion helper for verifying database table contents after a specification run. */
|
|
400
466
|
var TableAssertion = class {
|
|
401
467
|
tableName;
|
|
402
468
|
db;
|
|
@@ -404,10 +470,20 @@ var TableAssertion = class {
|
|
|
404
470
|
this.tableName = tableName;
|
|
405
471
|
this.db = db;
|
|
406
472
|
}
|
|
473
|
+
/**
|
|
474
|
+
* Assert that the table contains exactly the expected rows for the given columns.
|
|
475
|
+
*
|
|
476
|
+
* @example
|
|
477
|
+
* await result.table("users").toMatch({
|
|
478
|
+
* columns: ["name", "email"],
|
|
479
|
+
* rows: [["Alice", "alice@example.com"]],
|
|
480
|
+
* });
|
|
481
|
+
*/
|
|
407
482
|
async toMatch(expected) {
|
|
408
483
|
const actual = await this.db.query(this.tableName, expected.columns);
|
|
409
484
|
if (JSON.stringify(actual) !== JSON.stringify(expected.rows)) throw new Error(formatTableDiff(this.tableName, expected.columns, expected.rows, actual));
|
|
410
485
|
}
|
|
486
|
+
/** Assert that the table has zero rows. */
|
|
411
487
|
async toBeEmpty() {
|
|
412
488
|
const actual = await this.db.query(this.tableName, ["*"]);
|
|
413
489
|
if (actual.length !== 0) throw new Error(`Expected table "${this.tableName}" to be empty, but it has ${actual.length} rows`);
|
|
@@ -415,6 +491,10 @@ var TableAssertion = class {
|
|
|
415
491
|
};
|
|
416
492
|
//#endregion
|
|
417
493
|
//#region src/builder/specification-result.ts
|
|
494
|
+
/**
|
|
495
|
+
* The outcome of a single specification run.
|
|
496
|
+
* Provides accessors for CLI output, HTTP responses, files, directories, and database tables.
|
|
497
|
+
*/
|
|
418
498
|
var SpecificationResult = class {
|
|
419
499
|
commandResult;
|
|
420
500
|
config;
|
|
@@ -430,29 +510,46 @@ var SpecificationResult = class {
|
|
|
430
510
|
this.requestInfo = options.requestInfo;
|
|
431
511
|
this.workDir = options.workDir;
|
|
432
512
|
}
|
|
513
|
+
/** The process exit code. Only available after a CLI action. */
|
|
433
514
|
get exitCode() {
|
|
434
515
|
if (!this.commandResult) throw new Error(".exitCode requires a CLI action (.exec())");
|
|
435
516
|
return this.commandResult.exitCode;
|
|
436
517
|
}
|
|
518
|
+
/** The HTTP response status code. Only available after an HTTP action. */
|
|
437
519
|
get status() {
|
|
438
520
|
if (!this.responseData) throw new Error(".status requires an HTTP action (.get(), .post(), etc.)");
|
|
439
521
|
return this.responseData.status;
|
|
440
522
|
}
|
|
523
|
+
/** The captured standard output. Only available after a CLI action. */
|
|
441
524
|
get stdout() {
|
|
442
525
|
if (!this.commandResult) throw new Error(".stdout requires a CLI action (.exec())");
|
|
443
526
|
return this.commandResult.stdout;
|
|
444
527
|
}
|
|
528
|
+
/** The captured standard error. Only available after a CLI action. */
|
|
445
529
|
get stderr() {
|
|
446
530
|
if (!this.commandResult) throw new Error(".stderr requires a CLI action (.exec())");
|
|
447
531
|
return this.commandResult.stderr;
|
|
448
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Extract text blocks from stdout that contain a pattern.
|
|
535
|
+
* Useful for parsing structured CLI output (linters, compilers).
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* expect(result.grep('error.ts')).toContain('no-unused-vars');
|
|
539
|
+
*/
|
|
540
|
+
grep(pattern) {
|
|
541
|
+
return grep(this.stdout, pattern);
|
|
542
|
+
}
|
|
543
|
+
/** Access the HTTP response body for assertions. Only available after an HTTP action. */
|
|
449
544
|
get response() {
|
|
450
545
|
if (!this.responseData) throw new Error(".response requires an HTTP action (.get(), .post(), etc.)");
|
|
451
546
|
return new ResponseAccessor(this.responseData.body, this.testDir);
|
|
452
547
|
}
|
|
548
|
+
/** Access a directory (relative to the working directory) for snapshot assertions. */
|
|
453
549
|
directory(path = ".") {
|
|
454
550
|
return new DirectoryAccessor((0, node_path.resolve)(this.workDir ?? this.testDir, path), this.testDir);
|
|
455
551
|
}
|
|
552
|
+
/** Access a single file (relative to the working directory) for content assertions. */
|
|
456
553
|
file(path) {
|
|
457
554
|
const resolvedPath = (0, node_path.resolve)(this.workDir ?? this.testDir, path);
|
|
458
555
|
const exists = (0, node_fs.existsSync)(resolvedPath);
|
|
@@ -464,6 +561,7 @@ var SpecificationResult = class {
|
|
|
464
561
|
exists
|
|
465
562
|
};
|
|
466
563
|
}
|
|
564
|
+
/** Access a database table for row-level assertions. */
|
|
467
565
|
table(tableName, options) {
|
|
468
566
|
const db = this.resolveDatabase(options?.service);
|
|
469
567
|
if (!db) throw new Error(options?.service ? `table("${tableName}") requires database "${options.service}" but it was not found` : `table("${tableName}") requires a database adapter`);
|
|
@@ -476,6 +574,13 @@ var SpecificationResult = class {
|
|
|
476
574
|
};
|
|
477
575
|
//#endregion
|
|
478
576
|
//#region src/builder/specification-builder.ts
|
|
577
|
+
/**
|
|
578
|
+
* Fluent builder for declaring a single test specification.
|
|
579
|
+
*
|
|
580
|
+
* Chain setup methods ({@link seed}, {@link fixture}, {@link env}), an action
|
|
581
|
+
* ({@link get}, {@link post}, {@link exec}), then call {@link run} to execute
|
|
582
|
+
* and receive a {@link SpecificationResult} for assertions.
|
|
583
|
+
*/
|
|
479
584
|
var SpecificationBuilder = class {
|
|
480
585
|
commandArgs = null;
|
|
481
586
|
commandEnv = {};
|
|
@@ -493,6 +598,12 @@ var SpecificationBuilder = class {
|
|
|
493
598
|
this.testDir = testDir;
|
|
494
599
|
this.label = label;
|
|
495
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Queue a SQL seed file to run before the action.
|
|
603
|
+
*
|
|
604
|
+
* @example
|
|
605
|
+
* spec("creates user").seed("users.sql").exec("list-users").run();
|
|
606
|
+
*/
|
|
496
607
|
seed(file, options) {
|
|
497
608
|
this.seeds.push({
|
|
498
609
|
file,
|
|
@@ -500,14 +611,17 @@ var SpecificationBuilder = class {
|
|
|
500
611
|
});
|
|
501
612
|
return this;
|
|
502
613
|
}
|
|
614
|
+
/** Copy a file or directory from `fixtures/` into the working directory before execution. */
|
|
503
615
|
fixture(file) {
|
|
504
616
|
this.fixtures.push({ file });
|
|
505
617
|
return this;
|
|
506
618
|
}
|
|
619
|
+
/** Copy an entire project fixture from `fixturesRoot` into the working directory. */
|
|
507
620
|
project(name) {
|
|
508
621
|
this.projectName = name;
|
|
509
622
|
return this;
|
|
510
623
|
}
|
|
624
|
+
/** Register an MSW mock definition file from `mock/` to intercept HTTP calls. */
|
|
511
625
|
mock(file) {
|
|
512
626
|
this.mocks.push({ file });
|
|
513
627
|
return this;
|
|
@@ -529,6 +643,12 @@ var SpecificationBuilder = class {
|
|
|
529
643
|
};
|
|
530
644
|
return this;
|
|
531
645
|
}
|
|
646
|
+
/**
|
|
647
|
+
* Send a GET request to the server adapter.
|
|
648
|
+
*
|
|
649
|
+
* @example
|
|
650
|
+
* spec("list items").get("/api/items").run();
|
|
651
|
+
*/
|
|
532
652
|
get(path) {
|
|
533
653
|
this.request = {
|
|
534
654
|
method: "GET",
|
|
@@ -536,6 +656,13 @@ var SpecificationBuilder = class {
|
|
|
536
656
|
};
|
|
537
657
|
return this;
|
|
538
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* Send a POST request to the server adapter.
|
|
661
|
+
*
|
|
662
|
+
* @param bodyFile - Optional JSON file from `requests/` to use as the request body.
|
|
663
|
+
* @example
|
|
664
|
+
* spec("create item").post("/api/items", "new-item.json").run();
|
|
665
|
+
*/
|
|
539
666
|
post(path, bodyFile) {
|
|
540
667
|
this.request = {
|
|
541
668
|
bodyFile,
|
|
@@ -544,6 +671,7 @@ var SpecificationBuilder = class {
|
|
|
544
671
|
};
|
|
545
672
|
return this;
|
|
546
673
|
}
|
|
674
|
+
/** Send a PUT request to the server adapter. */
|
|
547
675
|
put(path, bodyFile) {
|
|
548
676
|
this.request = {
|
|
549
677
|
bodyFile,
|
|
@@ -552,6 +680,7 @@ var SpecificationBuilder = class {
|
|
|
552
680
|
};
|
|
553
681
|
return this;
|
|
554
682
|
}
|
|
683
|
+
/** Send a DELETE request to the server adapter. */
|
|
555
684
|
delete(path) {
|
|
556
685
|
this.request = {
|
|
557
686
|
method: "DELETE",
|
|
@@ -559,10 +688,19 @@ var SpecificationBuilder = class {
|
|
|
559
688
|
};
|
|
560
689
|
return this;
|
|
561
690
|
}
|
|
691
|
+
/**
|
|
692
|
+
* Execute a CLI command (or a sequence of commands) in an isolated working directory.
|
|
693
|
+
* When an array is passed, commands run sequentially and stop on the first non-zero exit code.
|
|
694
|
+
*
|
|
695
|
+
* @example
|
|
696
|
+
* spec("init project").exec("init --name demo").run();
|
|
697
|
+
* spec("multi-step").exec(["init", "build"]).run();
|
|
698
|
+
*/
|
|
562
699
|
exec(args) {
|
|
563
700
|
this.commandArgs = args;
|
|
564
701
|
return this;
|
|
565
702
|
}
|
|
703
|
+
/** Spawn a long-running CLI process with custom spawn options (e.g. timeout, signal). */
|
|
566
704
|
spawn(args, options) {
|
|
567
705
|
this.spawnConfig = {
|
|
568
706
|
args,
|
|
@@ -570,6 +708,15 @@ var SpecificationBuilder = class {
|
|
|
570
708
|
};
|
|
571
709
|
return this;
|
|
572
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Execute the specification: run seeds, copy fixtures, then perform the
|
|
713
|
+
* configured action (HTTP or CLI).
|
|
714
|
+
*
|
|
715
|
+
* @returns The result object used for assertions.
|
|
716
|
+
* @example
|
|
717
|
+
* const result = await spec("test").exec("status").run();
|
|
718
|
+
* expect(result.exitCode).toBe(0);
|
|
719
|
+
*/
|
|
573
720
|
async run() {
|
|
574
721
|
const hasHttpAction = this.request !== null;
|
|
575
722
|
const hasCliAction = this.commandArgs !== null || this.spawnConfig !== null;
|
|
@@ -667,197 +814,241 @@ function getCallerDir() {
|
|
|
667
814
|
}
|
|
668
815
|
throw new Error("Cannot detect caller directory from stack trace");
|
|
669
816
|
}
|
|
817
|
+
/**
|
|
818
|
+
* Create a {@link SpecificationRunner} bound to the given adapter configuration.
|
|
819
|
+
* The test file directory is auto-detected from the call stack.
|
|
820
|
+
*/
|
|
670
821
|
function createSpecificationRunner(config) {
|
|
671
822
|
return (label) => {
|
|
672
823
|
return new SpecificationBuilder(config, getCallerDir(), label);
|
|
673
824
|
};
|
|
674
825
|
}
|
|
675
826
|
//#endregion
|
|
676
|
-
//#region src/
|
|
827
|
+
//#region src/docker/docker-adapter.ts
|
|
677
828
|
/**
|
|
678
|
-
*
|
|
829
|
+
* Implements {@link DockerContainerPort} by shelling out to the Docker CLI.
|
|
830
|
+
* Each instance is bound to a single container ID.
|
|
679
831
|
*/
|
|
680
|
-
var
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
this.composeFile = composeFile;
|
|
685
|
-
}
|
|
686
|
-
run(command) {
|
|
687
|
-
try {
|
|
688
|
-
return (0, node_child_process.execSync)(command, {
|
|
689
|
-
cwd: (0, node_path.dirname)(this.composeFile),
|
|
690
|
-
encoding: "utf8",
|
|
691
|
-
timeout: 12e4
|
|
692
|
-
}).trim();
|
|
693
|
-
} catch (error) {
|
|
694
|
-
const stderr = error.stderr?.toString().trim() ?? error.message;
|
|
695
|
-
throw new Error(`docker compose failed: ${stderr}`, { cause: error });
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
async start() {
|
|
699
|
-
if (this.started) return;
|
|
700
|
-
this.run(`docker compose -f ${this.composeFile} up -d --wait`);
|
|
701
|
-
this.started = true;
|
|
702
|
-
}
|
|
703
|
-
async stop() {
|
|
704
|
-
if (!this.started) return;
|
|
705
|
-
this.run(`docker compose -f ${this.composeFile} down -v`);
|
|
706
|
-
this.started = false;
|
|
707
|
-
}
|
|
708
|
-
getMappedPort(serviceName, containerPort) {
|
|
709
|
-
const port = this.run(`docker compose -f ${this.composeFile} port ${serviceName} ${containerPort}`).split(":").pop();
|
|
710
|
-
return Number(port);
|
|
711
|
-
}
|
|
712
|
-
getHost() {
|
|
713
|
-
return "localhost";
|
|
714
|
-
}
|
|
715
|
-
};
|
|
716
|
-
//#endregion
|
|
717
|
-
//#region src/adapters/postgres.adapter.ts
|
|
718
|
-
var PostgresHandle = class {
|
|
719
|
-
type = "postgres";
|
|
720
|
-
composeName;
|
|
721
|
-
defaultPort = 5432;
|
|
722
|
-
defaultImage;
|
|
723
|
-
environment;
|
|
724
|
-
connectionString = "";
|
|
725
|
-
started = false;
|
|
726
|
-
client = null;
|
|
727
|
-
constructor(options = {}) {
|
|
728
|
-
this.composeName = options.compose ?? null;
|
|
729
|
-
this.defaultImage = options.image ?? "postgres:17";
|
|
730
|
-
this.environment = {
|
|
731
|
-
POSTGRES_DB: "test",
|
|
732
|
-
POSTGRES_PASSWORD: "test",
|
|
733
|
-
POSTGRES_USER: "test",
|
|
734
|
-
...options.env
|
|
735
|
-
};
|
|
736
|
-
}
|
|
737
|
-
buildConnectionString(host, port) {
|
|
738
|
-
return `postgresql://${this.environment.POSTGRES_USER ?? "test"}:${this.environment.POSTGRES_PASSWORD ?? "test"}@${host}:${port}/${this.environment.POSTGRES_DB ?? "test"}`;
|
|
832
|
+
var DockerAdapter = class {
|
|
833
|
+
containerId;
|
|
834
|
+
constructor(containerId) {
|
|
835
|
+
this.containerId = containerId;
|
|
739
836
|
}
|
|
740
|
-
|
|
741
|
-
return this
|
|
837
|
+
async exec(cmd) {
|
|
838
|
+
return (0, node_child_process.execSync)(`docker exec ${this.containerId} ${cmd.map((c) => `'${c}'`).join(" ")}`, {
|
|
839
|
+
encoding: "utf8",
|
|
840
|
+
timeout: 1e4
|
|
841
|
+
}).trim();
|
|
742
842
|
}
|
|
743
|
-
async
|
|
744
|
-
if (!this.connectionString) throw new Error("postgres: cannot healthcheck — no connection string");
|
|
843
|
+
async file(path) {
|
|
745
844
|
try {
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
} catch
|
|
751
|
-
|
|
845
|
+
return {
|
|
846
|
+
exists: true,
|
|
847
|
+
content: await this.exec(["cat", path])
|
|
848
|
+
};
|
|
849
|
+
} catch {
|
|
850
|
+
return {
|
|
851
|
+
exists: false,
|
|
852
|
+
content: ""
|
|
853
|
+
};
|
|
752
854
|
}
|
|
753
855
|
}
|
|
754
|
-
async
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
throw new Error(`postgres init script failed (${initPath}):\n${error.message}`, { cause: error });
|
|
763
|
-
}
|
|
764
|
-
return;
|
|
856
|
+
async isRunning() {
|
|
857
|
+
try {
|
|
858
|
+
return (0, node_child_process.execSync)(`docker inspect --format='{{.State.Running}}' ${this.containerId}`, {
|
|
859
|
+
encoding: "utf8",
|
|
860
|
+
timeout: 5e3
|
|
861
|
+
}).trim() === "true";
|
|
862
|
+
} catch {
|
|
863
|
+
return false;
|
|
765
864
|
}
|
|
766
865
|
}
|
|
767
|
-
async
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
this.client = null;
|
|
866
|
+
async logs(tail) {
|
|
867
|
+
return (0, node_child_process.execSync)(`docker logs ${tail ? `--tail ${tail}` : ""} ${this.containerId}`, {
|
|
868
|
+
encoding: "utf8",
|
|
869
|
+
timeout: 1e4
|
|
772
870
|
});
|
|
773
|
-
await client.connect();
|
|
774
|
-
this.client = client;
|
|
775
|
-
return client;
|
|
776
|
-
}
|
|
777
|
-
async seed(sql) {
|
|
778
|
-
await (await this.getClient()).query(sql);
|
|
779
|
-
}
|
|
780
|
-
async query(table, columns) {
|
|
781
|
-
const client = await this.getClient();
|
|
782
|
-
const columnList = columns.join(", ");
|
|
783
|
-
return (await client.query(`SELECT ${columnList} FROM "${table}" ORDER BY 1`)).rows.map((row) => columns.map((col) => row[col]));
|
|
784
|
-
}
|
|
785
|
-
async reset() {
|
|
786
|
-
const client = await this.getClient();
|
|
787
|
-
const result = await client.query(`
|
|
788
|
-
SELECT tablename FROM pg_tables
|
|
789
|
-
WHERE schemaname = 'public'
|
|
790
|
-
AND tablename NOT LIKE '_prisma%'
|
|
791
|
-
`);
|
|
792
|
-
for (const row of result.rows) await client.query(`TRUNCATE "${row.tablename}" CASCADE`);
|
|
793
871
|
}
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
try {
|
|
828
|
-
const { createClient } = await import("redis");
|
|
829
|
-
const client = createClient({ url: this.connectionString });
|
|
830
|
-
await client.connect();
|
|
831
|
-
await client.ping();
|
|
832
|
-
await client.disconnect();
|
|
833
|
-
} catch (error) {
|
|
834
|
-
throw new Error(`redis healthcheck failed: ${error.message || error.code || String(error)}`, { cause: error });
|
|
835
|
-
}
|
|
872
|
+
async inspect() {
|
|
873
|
+
const raw = (0, node_child_process.execSync)(`docker inspect ${this.containerId}`, {
|
|
874
|
+
encoding: "utf8",
|
|
875
|
+
timeout: 5e3
|
|
876
|
+
});
|
|
877
|
+
const data = JSON.parse(raw)[0];
|
|
878
|
+
return {
|
|
879
|
+
id: data.Id,
|
|
880
|
+
name: data.Name,
|
|
881
|
+
state: {
|
|
882
|
+
running: data.State.Running,
|
|
883
|
+
exitCode: data.State.ExitCode,
|
|
884
|
+
status: data.State.Status
|
|
885
|
+
},
|
|
886
|
+
config: {
|
|
887
|
+
image: data.Config.Image,
|
|
888
|
+
env: data.Config.Env || []
|
|
889
|
+
},
|
|
890
|
+
hostConfig: {
|
|
891
|
+
memory: data.HostConfig.Memory || 0,
|
|
892
|
+
cpuQuota: data.HostConfig.CpuQuota || 0,
|
|
893
|
+
networkMode: data.HostConfig.NetworkMode || "",
|
|
894
|
+
mounts: (data.Mounts || []).map((m) => ({
|
|
895
|
+
source: m.Source,
|
|
896
|
+
destination: m.Destination,
|
|
897
|
+
type: m.Type
|
|
898
|
+
}))
|
|
899
|
+
},
|
|
900
|
+
networkSettings: { networks: Object.fromEntries(Object.entries(data.NetworkSettings.Networks || {}).map(([name, net]) => [name, {
|
|
901
|
+
gateway: net.Gateway,
|
|
902
|
+
ipAddress: net.IPAddress
|
|
903
|
+
}])) }
|
|
904
|
+
};
|
|
836
905
|
}
|
|
837
|
-
async
|
|
838
|
-
async reset() {
|
|
839
|
-
const { createClient } = await import("redis");
|
|
840
|
-
const client = createClient({ url: this.connectionString });
|
|
841
|
-
await client.connect();
|
|
906
|
+
async exists(path) {
|
|
842
907
|
try {
|
|
843
|
-
await
|
|
844
|
-
|
|
845
|
-
|
|
908
|
+
await this.exec([
|
|
909
|
+
"test",
|
|
910
|
+
"-e",
|
|
911
|
+
path
|
|
912
|
+
]);
|
|
913
|
+
return true;
|
|
914
|
+
} catch {
|
|
915
|
+
return false;
|
|
846
916
|
}
|
|
847
917
|
}
|
|
848
918
|
};
|
|
849
|
-
/**
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
* @example
|
|
853
|
-
* const cache = redis({ compose: "cache" });
|
|
854
|
-
* // After start: cache.connectionString is populated
|
|
855
|
-
*/
|
|
856
|
-
function redis(options = {}) {
|
|
857
|
-
return new RedisHandle(options);
|
|
919
|
+
/** Create a {@link DockerContainerPort} bound to an existing container by ID. */
|
|
920
|
+
function dockerContainer(containerId) {
|
|
921
|
+
return new DockerAdapter(containerId);
|
|
858
922
|
}
|
|
859
923
|
//#endregion
|
|
860
|
-
//#region src/
|
|
924
|
+
//#region src/docker/docker-assertion.ts
|
|
925
|
+
/**
|
|
926
|
+
* Fluent assertion builder for Docker containers.
|
|
927
|
+
* Chain async assertions like `toBeRunning()`, `toHaveFile()`, `toHaveMount()`.
|
|
928
|
+
*/
|
|
929
|
+
var DockerAssertion = class {
|
|
930
|
+
container;
|
|
931
|
+
constructor(container) {
|
|
932
|
+
this.container = container;
|
|
933
|
+
}
|
|
934
|
+
/** Assert the container is running */
|
|
935
|
+
async toBeRunning() {
|
|
936
|
+
if (!await this.container.isRunning()) throw new Error("Expected container to be running");
|
|
937
|
+
return this;
|
|
938
|
+
}
|
|
939
|
+
/** Assert the container is NOT running / doesn't exist */
|
|
940
|
+
async toNotExist() {
|
|
941
|
+
if (await this.container.isRunning()) throw new Error("Expected container to not exist or not be running");
|
|
942
|
+
return this;
|
|
943
|
+
}
|
|
944
|
+
/** Assert a file exists inside the container */
|
|
945
|
+
async toHaveFile(path, opts) {
|
|
946
|
+
const file = await this.container.file(path);
|
|
947
|
+
if (!file.exists) throw new Error(`Expected file ${path} to exist in container`);
|
|
948
|
+
if (opts?.containing && !file.content.includes(opts.containing)) throw new Error(`Expected file ${path} to contain "${opts.containing}", got: ${file.content.slice(0, 200)}`);
|
|
949
|
+
return this;
|
|
950
|
+
}
|
|
951
|
+
/** Assert a file does NOT exist */
|
|
952
|
+
async toNotHaveFile(path) {
|
|
953
|
+
if (await this.container.exists(path)) throw new Error(`Expected ${path} to not exist in container`);
|
|
954
|
+
return this;
|
|
955
|
+
}
|
|
956
|
+
/** Assert a directory exists */
|
|
957
|
+
async toHaveDirectory(path) {
|
|
958
|
+
if (!await this.container.exists(path)) throw new Error(`Expected directory ${path} to exist in container`);
|
|
959
|
+
return this;
|
|
960
|
+
}
|
|
961
|
+
/** Assert a mount exists */
|
|
962
|
+
async toHaveMount(destination) {
|
|
963
|
+
const info = await this.container.inspect();
|
|
964
|
+
if (!info.hostConfig.mounts.find((m) => m.destination === destination)) {
|
|
965
|
+
const available = info.hostConfig.mounts.map((m) => m.destination).join(", ");
|
|
966
|
+
throw new Error(`Expected mount at ${destination}, found: [${available}]`);
|
|
967
|
+
}
|
|
968
|
+
return this;
|
|
969
|
+
}
|
|
970
|
+
/** Assert network mode */
|
|
971
|
+
async toHaveNetwork(mode) {
|
|
972
|
+
const info = await this.container.inspect();
|
|
973
|
+
if (!info.hostConfig.networkMode.includes(mode)) throw new Error(`Expected network mode "${mode}", got "${info.hostConfig.networkMode}"`);
|
|
974
|
+
return this;
|
|
975
|
+
}
|
|
976
|
+
/** Assert memory limit */
|
|
977
|
+
async toHaveMemoryLimit(bytes) {
|
|
978
|
+
const info = await this.container.inspect();
|
|
979
|
+
if (info.hostConfig.memory !== bytes) throw new Error(`Expected memory limit ${bytes}, got ${info.hostConfig.memory}`);
|
|
980
|
+
return this;
|
|
981
|
+
}
|
|
982
|
+
/** Assert CPU quota */
|
|
983
|
+
async toHaveCpuQuota(quota) {
|
|
984
|
+
const info = await this.container.inspect();
|
|
985
|
+
if (info.hostConfig.cpuQuota !== quota) throw new Error(`Expected CPU quota ${quota}, got ${info.hostConfig.cpuQuota}`);
|
|
986
|
+
return this;
|
|
987
|
+
}
|
|
988
|
+
/** Execute a command and return output for custom assertions */
|
|
989
|
+
async exec(cmd) {
|
|
990
|
+
return this.container.exec(cmd);
|
|
991
|
+
}
|
|
992
|
+
/** Read a file for custom assertions */
|
|
993
|
+
async readFile(path) {
|
|
994
|
+
const file = await this.container.file(path);
|
|
995
|
+
if (!file.exists) throw new Error(`File ${path} does not exist`);
|
|
996
|
+
return file.content;
|
|
997
|
+
}
|
|
998
|
+
/** Get logs for custom assertions */
|
|
999
|
+
async getLogs(tail) {
|
|
1000
|
+
return this.container.logs(tail);
|
|
1001
|
+
}
|
|
1002
|
+
};
|
|
1003
|
+
//#endregion
|
|
1004
|
+
//#region src/adapters/compose.adapter.ts
|
|
1005
|
+
/**
|
|
1006
|
+
* Start the full compose stack and stop it all on cleanup.
|
|
1007
|
+
* Supports per-worker project names for parallel execution.
|
|
1008
|
+
*/
|
|
1009
|
+
var ComposeStackAdapter = class {
|
|
1010
|
+
composeFile;
|
|
1011
|
+
projectName;
|
|
1012
|
+
started = false;
|
|
1013
|
+
constructor(composeFile, projectName) {
|
|
1014
|
+
this.composeFile = composeFile;
|
|
1015
|
+
this.projectName = projectName ?? null;
|
|
1016
|
+
}
|
|
1017
|
+
get projectFlag() {
|
|
1018
|
+
return this.projectName ? `-p ${this.projectName}` : "";
|
|
1019
|
+
}
|
|
1020
|
+
run(command) {
|
|
1021
|
+
try {
|
|
1022
|
+
return (0, node_child_process.execSync)(command, {
|
|
1023
|
+
cwd: (0, node_path.dirname)(this.composeFile),
|
|
1024
|
+
encoding: "utf8",
|
|
1025
|
+
timeout: 12e4
|
|
1026
|
+
}).trim();
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
const stderr = error.stderr?.toString().trim() ?? error.message;
|
|
1029
|
+
throw new Error(`docker compose failed: ${stderr}`, { cause: error });
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
async start() {
|
|
1033
|
+
if (this.started) return;
|
|
1034
|
+
this.run(`docker compose ${this.projectFlag} -f ${this.composeFile} up -d --wait`);
|
|
1035
|
+
this.started = true;
|
|
1036
|
+
}
|
|
1037
|
+
async stop() {
|
|
1038
|
+
if (!this.started) return;
|
|
1039
|
+
this.run(`docker compose ${this.projectFlag} -f ${this.composeFile} down -v`);
|
|
1040
|
+
this.started = false;
|
|
1041
|
+
}
|
|
1042
|
+
getMappedPort(serviceName, containerPort) {
|
|
1043
|
+
const port = this.run(`docker compose ${this.projectFlag} -f ${this.composeFile} port ${serviceName} ${containerPort}`).split(":").pop();
|
|
1044
|
+
return Number(port);
|
|
1045
|
+
}
|
|
1046
|
+
getHost() {
|
|
1047
|
+
return "localhost";
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
//#endregion
|
|
1051
|
+
//#region src/adapters/testcontainers.adapter.ts
|
|
861
1052
|
/**
|
|
862
1053
|
* Container adapter using testcontainers.
|
|
863
1054
|
* Wraps a GenericContainer for programmatic container lifecycle.
|
|
@@ -1000,6 +1191,7 @@ var Orchestrator = class {
|
|
|
1000
1191
|
services;
|
|
1001
1192
|
mode;
|
|
1002
1193
|
root;
|
|
1194
|
+
projectName;
|
|
1003
1195
|
running = [];
|
|
1004
1196
|
composeStack = null;
|
|
1005
1197
|
composeHandles = [];
|
|
@@ -1008,6 +1200,7 @@ var Orchestrator = class {
|
|
|
1008
1200
|
this.services = options.services;
|
|
1009
1201
|
this.mode = options.mode;
|
|
1010
1202
|
this.root = options.root ?? process.cwd();
|
|
1203
|
+
this.projectName = options.projectName;
|
|
1011
1204
|
}
|
|
1012
1205
|
/**
|
|
1013
1206
|
* Start declared services via testcontainers (integration mode).
|
|
@@ -1019,7 +1212,13 @@ var Orchestrator = class {
|
|
|
1019
1212
|
const composePath = findComposeFile(this.root);
|
|
1020
1213
|
const composeDir = composePath ? (0, node_path.dirname)(composePath) : this.root;
|
|
1021
1214
|
const composeConfig = composePath ? parseComposeFile(composePath) : null;
|
|
1022
|
-
const
|
|
1215
|
+
const containerServices = [];
|
|
1216
|
+
const embeddedServices = [];
|
|
1217
|
+
for (const handle of this.services) {
|
|
1218
|
+
if (handle.defaultPort === 0) {
|
|
1219
|
+
embeddedServices.push(handle);
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1023
1222
|
let image = handle.defaultImage;
|
|
1024
1223
|
let env = { ...handle.environment };
|
|
1025
1224
|
if (handle.composeName && composeConfig) {
|
|
@@ -1033,18 +1232,26 @@ var Orchestrator = class {
|
|
|
1033
1232
|
Object.assign(handle.environment, composeService.environment);
|
|
1034
1233
|
}
|
|
1035
1234
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1235
|
+
const container = new TestcontainersAdapter({
|
|
1236
|
+
image,
|
|
1237
|
+
port: handle.defaultPort,
|
|
1238
|
+
env
|
|
1239
|
+
});
|
|
1240
|
+
containerServices.push({
|
|
1241
|
+
container,
|
|
1042
1242
|
handle
|
|
1043
|
-
};
|
|
1044
|
-
}
|
|
1045
|
-
await Promise.all(
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
await Promise.all([...containerServices.map(({ container }) => container.start()), ...embeddedServices.map(async (handle) => {
|
|
1246
|
+
await handle.initialize(composeDir);
|
|
1247
|
+
handle.started = true;
|
|
1248
|
+
this.running.push({
|
|
1249
|
+
handle,
|
|
1250
|
+
container: null
|
|
1251
|
+
});
|
|
1252
|
+
})]);
|
|
1046
1253
|
const reports = [];
|
|
1047
|
-
for (const { container, handle } of
|
|
1254
|
+
for (const { container, handle } of containerServices) {
|
|
1048
1255
|
const serviceStartTime = Date.now();
|
|
1049
1256
|
try {
|
|
1050
1257
|
const host = container.getHost();
|
|
@@ -1105,12 +1312,12 @@ var Orchestrator = class {
|
|
|
1105
1312
|
const startTime = Date.now();
|
|
1106
1313
|
const composeDir = (0, node_path.dirname)(composePath);
|
|
1107
1314
|
const composeConfig = parseComposeFile(composePath);
|
|
1108
|
-
this.composeStack = new ComposeStackAdapter(composePath);
|
|
1315
|
+
this.composeStack = new ComposeStackAdapter(composePath, this.projectName);
|
|
1109
1316
|
await this.composeStack.start();
|
|
1110
1317
|
for (const service of composeConfig.infraServices) {
|
|
1111
1318
|
const type = detectServiceType(service.image);
|
|
1112
1319
|
if (type === "postgres") {
|
|
1113
|
-
const handle = postgres({
|
|
1320
|
+
const handle = require_sqlite_adapter.postgres({
|
|
1114
1321
|
compose: service.name,
|
|
1115
1322
|
env: service.environment
|
|
1116
1323
|
});
|
|
@@ -1120,7 +1327,7 @@ var Orchestrator = class {
|
|
|
1120
1327
|
handle.started = true;
|
|
1121
1328
|
this.composeHandles.push(handle);
|
|
1122
1329
|
} else if (type === "redis") {
|
|
1123
|
-
const handle = redis({ compose: service.name });
|
|
1330
|
+
const handle = require_sqlite_adapter.redis({ compose: service.name });
|
|
1124
1331
|
const port = this.composeStack.getMappedPort(service.name, 6379);
|
|
1125
1332
|
handle.connectionString = handle.buildConnectionString("localhost", port);
|
|
1126
1333
|
handle.started = true;
|
|
@@ -1215,6 +1422,178 @@ function resolveCommand(command, root) {
|
|
|
1215
1422
|
return command;
|
|
1216
1423
|
}
|
|
1217
1424
|
//#endregion
|
|
1425
|
+
//#region src/runner/spec.ts
|
|
1426
|
+
/**
|
|
1427
|
+
* Create a specification runner for the given target.
|
|
1428
|
+
*
|
|
1429
|
+
* @param target - What to test against: {@link app}, {@link stack}, or {@link command}.
|
|
1430
|
+
* @param options - Shared options: root directory, infrastructure services.
|
|
1431
|
+
*
|
|
1432
|
+
* @example
|
|
1433
|
+
* // HTTP — in-process app with testcontainers
|
|
1434
|
+
* const s = await spec(app(() => createApp(db)), { services: [db] });
|
|
1435
|
+
*
|
|
1436
|
+
* // HTTP — full docker compose stack
|
|
1437
|
+
* const s = await spec(stack('../../'));
|
|
1438
|
+
*
|
|
1439
|
+
* // CLI — command binary
|
|
1440
|
+
* const s = await spec(command('my-cli'), { root: '../fixtures' });
|
|
1441
|
+
*/
|
|
1442
|
+
async function spec(target, options = {}) {
|
|
1443
|
+
switch (target.kind) {
|
|
1444
|
+
case "app": return startApp(target, options);
|
|
1445
|
+
case "stack": return startStack(target, options);
|
|
1446
|
+
case "command": return startCommand(target, options);
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
function getWorkerId() {
|
|
1450
|
+
return process.env.VITEST_POOL_ID ?? "0";
|
|
1451
|
+
}
|
|
1452
|
+
async function acquireIsolation(services) {
|
|
1453
|
+
const workerId = getWorkerId();
|
|
1454
|
+
for (const service of services) await service.isolation().acquire(workerId);
|
|
1455
|
+
}
|
|
1456
|
+
async function releaseIsolation(services) {
|
|
1457
|
+
for (const service of services) await service.isolation().release();
|
|
1458
|
+
}
|
|
1459
|
+
async function startApp(target, options) {
|
|
1460
|
+
const services = options.services ?? [];
|
|
1461
|
+
const orchestrator = new Orchestrator({
|
|
1462
|
+
mode: "integration",
|
|
1463
|
+
root: resolveProjectRoot(options.root),
|
|
1464
|
+
services
|
|
1465
|
+
});
|
|
1466
|
+
await orchestrator.start();
|
|
1467
|
+
await acquireIsolation(services);
|
|
1468
|
+
const servicesMap = {};
|
|
1469
|
+
for (const svc of services) {
|
|
1470
|
+
const key = svc.composeName ?? svc.type;
|
|
1471
|
+
servicesMap[key] = svc;
|
|
1472
|
+
}
|
|
1473
|
+
const honoApp = target.factory(servicesMap);
|
|
1474
|
+
const database = orchestrator.getDatabase() ?? void 0;
|
|
1475
|
+
const databases = orchestrator.getDatabases();
|
|
1476
|
+
const runner = createSpecificationRunner({
|
|
1477
|
+
database,
|
|
1478
|
+
databases: databases.size > 0 ? databases : void 0,
|
|
1479
|
+
server: new HonoAdapter(honoApp)
|
|
1480
|
+
});
|
|
1481
|
+
runner.cleanup = async () => {
|
|
1482
|
+
await releaseIsolation(services);
|
|
1483
|
+
await orchestrator.stop();
|
|
1484
|
+
};
|
|
1485
|
+
runner.docker = (id) => new DockerAssertion(dockerContainer(id));
|
|
1486
|
+
runner.orchestrator = orchestrator;
|
|
1487
|
+
return runner;
|
|
1488
|
+
}
|
|
1489
|
+
async function startStack(target, options) {
|
|
1490
|
+
const root = resolveProjectRoot(target.root ?? options.root);
|
|
1491
|
+
const projectName = `test-worker-${getWorkerId()}`;
|
|
1492
|
+
const orchestrator = new Orchestrator({
|
|
1493
|
+
mode: "e2e",
|
|
1494
|
+
root,
|
|
1495
|
+
services: options.services ?? [],
|
|
1496
|
+
projectName
|
|
1497
|
+
});
|
|
1498
|
+
await orchestrator.startCompose();
|
|
1499
|
+
const appUrl = orchestrator.getAppUrl();
|
|
1500
|
+
if (!appUrl) throw new Error("stack(): could not detect app URL from compose. Ensure an app service with ports is defined.");
|
|
1501
|
+
const database = orchestrator.getDatabase() ?? void 0;
|
|
1502
|
+
const databases = orchestrator.getDatabases();
|
|
1503
|
+
const runner = createSpecificationRunner({
|
|
1504
|
+
database,
|
|
1505
|
+
databases: databases.size > 0 ? databases : void 0,
|
|
1506
|
+
server: new FetchAdapter(appUrl)
|
|
1507
|
+
});
|
|
1508
|
+
runner.cleanup = () => orchestrator.stopCompose();
|
|
1509
|
+
runner.docker = (id) => new DockerAssertion(dockerContainer(id));
|
|
1510
|
+
runner.orchestrator = orchestrator;
|
|
1511
|
+
return runner;
|
|
1512
|
+
}
|
|
1513
|
+
async function startCommand(target, options) {
|
|
1514
|
+
const root = resolveProjectRoot(options.root);
|
|
1515
|
+
const bin = resolveCommand(target.bin, root);
|
|
1516
|
+
const services = options.services ?? [];
|
|
1517
|
+
let orchestrator = null;
|
|
1518
|
+
let database;
|
|
1519
|
+
let databases;
|
|
1520
|
+
if (services.length) {
|
|
1521
|
+
orchestrator = new Orchestrator({
|
|
1522
|
+
mode: "integration",
|
|
1523
|
+
root,
|
|
1524
|
+
services
|
|
1525
|
+
});
|
|
1526
|
+
await orchestrator.start();
|
|
1527
|
+
await acquireIsolation(services);
|
|
1528
|
+
database = orchestrator.getDatabase() ?? void 0;
|
|
1529
|
+
const dbMap = orchestrator.getDatabases();
|
|
1530
|
+
databases = dbMap.size > 0 ? dbMap : void 0;
|
|
1531
|
+
}
|
|
1532
|
+
const runner = createSpecificationRunner({
|
|
1533
|
+
command: new ExecAdapter(bin),
|
|
1534
|
+
database,
|
|
1535
|
+
databases,
|
|
1536
|
+
fixturesRoot: root
|
|
1537
|
+
});
|
|
1538
|
+
runner.cleanup = async () => {
|
|
1539
|
+
await releaseIsolation(services);
|
|
1540
|
+
if (orchestrator) await orchestrator.stop();
|
|
1541
|
+
};
|
|
1542
|
+
runner.docker = (id) => new DockerAssertion(dockerContainer(id));
|
|
1543
|
+
runner.orchestrator = orchestrator;
|
|
1544
|
+
return runner;
|
|
1545
|
+
}
|
|
1546
|
+
//#endregion
|
|
1547
|
+
//#region src/runner/targets.ts
|
|
1548
|
+
/**
|
|
1549
|
+
* Test against an in-process Hono app. The factory receives started services
|
|
1550
|
+
* so you can wire connection strings into your app/DI container.
|
|
1551
|
+
*
|
|
1552
|
+
* @param factory - Function that receives services and returns a Hono app instance.
|
|
1553
|
+
*
|
|
1554
|
+
* @example
|
|
1555
|
+
* const db = postgres({ compose: 'db' });
|
|
1556
|
+
* await spec(
|
|
1557
|
+
* app((services) => createApp({ databaseUrl: services.db.connectionString })),
|
|
1558
|
+
* { services: [db] },
|
|
1559
|
+
* );
|
|
1560
|
+
*/
|
|
1561
|
+
function app(factory) {
|
|
1562
|
+
return {
|
|
1563
|
+
kind: "app",
|
|
1564
|
+
factory
|
|
1565
|
+
};
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Test against a full docker compose stack. The stack is started with
|
|
1569
|
+
* `docker compose up` and real HTTP requests are sent to the app service.
|
|
1570
|
+
*
|
|
1571
|
+
* @param root - Project root containing `docker/compose.test.yaml`.
|
|
1572
|
+
*
|
|
1573
|
+
* @example
|
|
1574
|
+
* await spec(stack('../../'));
|
|
1575
|
+
*/
|
|
1576
|
+
function stack(root) {
|
|
1577
|
+
return {
|
|
1578
|
+
kind: "stack",
|
|
1579
|
+
root
|
|
1580
|
+
};
|
|
1581
|
+
}
|
|
1582
|
+
/**
|
|
1583
|
+
* Test a CLI binary. Each spec runs in a fresh temp directory.
|
|
1584
|
+
*
|
|
1585
|
+
* @param bin - Path to the CLI binary or command name (resolved from node_modules/.bin or PATH).
|
|
1586
|
+
*
|
|
1587
|
+
* @example
|
|
1588
|
+
* await spec(command('my-cli'), { root: '../fixtures' });
|
|
1589
|
+
*/
|
|
1590
|
+
function command(bin) {
|
|
1591
|
+
return {
|
|
1592
|
+
kind: "command",
|
|
1593
|
+
bin
|
|
1594
|
+
};
|
|
1595
|
+
}
|
|
1596
|
+
//#endregion
|
|
1218
1597
|
//#region src/runner/cli.ts
|
|
1219
1598
|
/**
|
|
1220
1599
|
* Create a CLI specification runner.
|
|
@@ -1250,36 +1629,6 @@ async function cli(options) {
|
|
|
1250
1629
|
return runner;
|
|
1251
1630
|
}
|
|
1252
1631
|
//#endregion
|
|
1253
|
-
//#region src/adapters/fetch.adapter.ts
|
|
1254
|
-
/**
|
|
1255
|
-
* Server adapter for real HTTP — sends actual fetch requests.
|
|
1256
|
-
* Used by e2e() specification runner.
|
|
1257
|
-
*/
|
|
1258
|
-
var FetchAdapter = class {
|
|
1259
|
-
baseUrl;
|
|
1260
|
-
constructor(url) {
|
|
1261
|
-
this.baseUrl = url.replace(/\/$/, "");
|
|
1262
|
-
}
|
|
1263
|
-
async request(method, path, body) {
|
|
1264
|
-
const init = {
|
|
1265
|
-
method,
|
|
1266
|
-
headers: { "Content-Type": "application/json" }
|
|
1267
|
-
};
|
|
1268
|
-
if (body !== void 0) init.body = JSON.stringify(body);
|
|
1269
|
-
const response = await fetch(`${this.baseUrl}${path}`, init);
|
|
1270
|
-
const responseBody = await response.json().catch(() => null);
|
|
1271
|
-
const headers = {};
|
|
1272
|
-
response.headers.forEach((value, key) => {
|
|
1273
|
-
headers[key] = value;
|
|
1274
|
-
});
|
|
1275
|
-
return {
|
|
1276
|
-
status: response.status,
|
|
1277
|
-
body: responseBody,
|
|
1278
|
-
headers
|
|
1279
|
-
};
|
|
1280
|
-
}
|
|
1281
|
-
};
|
|
1282
|
-
//#endregion
|
|
1283
1632
|
//#region src/runner/e2e.ts
|
|
1284
1633
|
/**
|
|
1285
1634
|
* Create an E2E specification runner.
|
|
@@ -1306,36 +1655,6 @@ async function e2e(options = {}) {
|
|
|
1306
1655
|
return runner;
|
|
1307
1656
|
}
|
|
1308
1657
|
//#endregion
|
|
1309
|
-
//#region src/adapters/hono.adapter.ts
|
|
1310
|
-
/**
|
|
1311
|
-
* Server adapter for Hono — in-process requests, no real HTTP.
|
|
1312
|
-
* Used by integration() specification runner.
|
|
1313
|
-
*/
|
|
1314
|
-
var HonoAdapter = class {
|
|
1315
|
-
app;
|
|
1316
|
-
constructor(app) {
|
|
1317
|
-
this.app = app;
|
|
1318
|
-
}
|
|
1319
|
-
async request(method, path, body) {
|
|
1320
|
-
const init = {
|
|
1321
|
-
method,
|
|
1322
|
-
headers: { "Content-Type": "application/json" }
|
|
1323
|
-
};
|
|
1324
|
-
if (body !== void 0) init.body = JSON.stringify(body);
|
|
1325
|
-
const response = await this.app.request(path, init);
|
|
1326
|
-
const responseBody = await response.json().catch(() => null);
|
|
1327
|
-
const headers = {};
|
|
1328
|
-
response.headers.forEach((value, key) => {
|
|
1329
|
-
headers[key] = value;
|
|
1330
|
-
});
|
|
1331
|
-
return {
|
|
1332
|
-
status: response.status,
|
|
1333
|
-
body: responseBody,
|
|
1334
|
-
headers
|
|
1335
|
-
};
|
|
1336
|
-
}
|
|
1337
|
-
};
|
|
1338
|
-
//#endregion
|
|
1339
1658
|
//#region src/runner/integration.ts
|
|
1340
1659
|
/**
|
|
1341
1660
|
* Create an integration specification runner.
|
|
@@ -1361,196 +1680,6 @@ async function integration(options) {
|
|
|
1361
1680
|
return runner;
|
|
1362
1681
|
}
|
|
1363
1682
|
//#endregion
|
|
1364
|
-
//#region src/docker/docker-adapter.ts
|
|
1365
|
-
var DockerAdapter = class {
|
|
1366
|
-
containerId;
|
|
1367
|
-
constructor(containerId) {
|
|
1368
|
-
this.containerId = containerId;
|
|
1369
|
-
}
|
|
1370
|
-
async exec(cmd) {
|
|
1371
|
-
return (0, node_child_process.execSync)(`docker exec ${this.containerId} ${cmd.map((c) => `'${c}'`).join(" ")}`, {
|
|
1372
|
-
encoding: "utf8",
|
|
1373
|
-
timeout: 1e4
|
|
1374
|
-
}).trim();
|
|
1375
|
-
}
|
|
1376
|
-
async file(path) {
|
|
1377
|
-
try {
|
|
1378
|
-
return {
|
|
1379
|
-
exists: true,
|
|
1380
|
-
content: await this.exec(["cat", path])
|
|
1381
|
-
};
|
|
1382
|
-
} catch {
|
|
1383
|
-
return {
|
|
1384
|
-
exists: false,
|
|
1385
|
-
content: ""
|
|
1386
|
-
};
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
async isRunning() {
|
|
1390
|
-
try {
|
|
1391
|
-
return (0, node_child_process.execSync)(`docker inspect --format='{{.State.Running}}' ${this.containerId}`, {
|
|
1392
|
-
encoding: "utf8",
|
|
1393
|
-
timeout: 5e3
|
|
1394
|
-
}).trim() === "true";
|
|
1395
|
-
} catch {
|
|
1396
|
-
return false;
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
async logs(tail) {
|
|
1400
|
-
return (0, node_child_process.execSync)(`docker logs ${tail ? `--tail ${tail}` : ""} ${this.containerId}`, {
|
|
1401
|
-
encoding: "utf8",
|
|
1402
|
-
timeout: 1e4
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
async inspect() {
|
|
1406
|
-
const raw = (0, node_child_process.execSync)(`docker inspect ${this.containerId}`, {
|
|
1407
|
-
encoding: "utf8",
|
|
1408
|
-
timeout: 5e3
|
|
1409
|
-
});
|
|
1410
|
-
const data = JSON.parse(raw)[0];
|
|
1411
|
-
return {
|
|
1412
|
-
id: data.Id,
|
|
1413
|
-
name: data.Name,
|
|
1414
|
-
state: {
|
|
1415
|
-
running: data.State.Running,
|
|
1416
|
-
exitCode: data.State.ExitCode,
|
|
1417
|
-
status: data.State.Status
|
|
1418
|
-
},
|
|
1419
|
-
config: {
|
|
1420
|
-
image: data.Config.Image,
|
|
1421
|
-
env: data.Config.Env || []
|
|
1422
|
-
},
|
|
1423
|
-
hostConfig: {
|
|
1424
|
-
memory: data.HostConfig.Memory || 0,
|
|
1425
|
-
cpuQuota: data.HostConfig.CpuQuota || 0,
|
|
1426
|
-
networkMode: data.HostConfig.NetworkMode || "",
|
|
1427
|
-
mounts: (data.Mounts || []).map((m) => ({
|
|
1428
|
-
source: m.Source,
|
|
1429
|
-
destination: m.Destination,
|
|
1430
|
-
type: m.Type
|
|
1431
|
-
}))
|
|
1432
|
-
},
|
|
1433
|
-
networkSettings: { networks: Object.fromEntries(Object.entries(data.NetworkSettings.Networks || {}).map(([name, net]) => [name, {
|
|
1434
|
-
gateway: net.Gateway,
|
|
1435
|
-
ipAddress: net.IPAddress
|
|
1436
|
-
}])) }
|
|
1437
|
-
};
|
|
1438
|
-
}
|
|
1439
|
-
async exists(path) {
|
|
1440
|
-
try {
|
|
1441
|
-
await this.exec([
|
|
1442
|
-
"test",
|
|
1443
|
-
"-e",
|
|
1444
|
-
path
|
|
1445
|
-
]);
|
|
1446
|
-
return true;
|
|
1447
|
-
} catch {
|
|
1448
|
-
return false;
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
};
|
|
1452
|
-
/** Create a Docker container port for an existing container */
|
|
1453
|
-
function dockerContainer(containerId) {
|
|
1454
|
-
return new DockerAdapter(containerId);
|
|
1455
|
-
}
|
|
1456
|
-
//#endregion
|
|
1457
|
-
//#region src/docker/docker-assertion.ts
|
|
1458
|
-
/** Fluent assertion builder for Docker containers */
|
|
1459
|
-
var DockerAssertion = class {
|
|
1460
|
-
container;
|
|
1461
|
-
constructor(container) {
|
|
1462
|
-
this.container = container;
|
|
1463
|
-
}
|
|
1464
|
-
/** Assert the container is running */
|
|
1465
|
-
async toBeRunning() {
|
|
1466
|
-
if (!await this.container.isRunning()) throw new Error("Expected container to be running");
|
|
1467
|
-
return this;
|
|
1468
|
-
}
|
|
1469
|
-
/** Assert the container is NOT running / doesn't exist */
|
|
1470
|
-
async toNotExist() {
|
|
1471
|
-
if (await this.container.isRunning()) throw new Error("Expected container to not exist or not be running");
|
|
1472
|
-
return this;
|
|
1473
|
-
}
|
|
1474
|
-
/** Assert a file exists inside the container */
|
|
1475
|
-
async toHaveFile(path, opts) {
|
|
1476
|
-
const file = await this.container.file(path);
|
|
1477
|
-
if (!file.exists) throw new Error(`Expected file ${path} to exist in container`);
|
|
1478
|
-
if (opts?.containing && !file.content.includes(opts.containing)) throw new Error(`Expected file ${path} to contain "${opts.containing}", got: ${file.content.slice(0, 200)}`);
|
|
1479
|
-
return this;
|
|
1480
|
-
}
|
|
1481
|
-
/** Assert a file does NOT exist */
|
|
1482
|
-
async toNotHaveFile(path) {
|
|
1483
|
-
if (await this.container.exists(path)) throw new Error(`Expected ${path} to not exist in container`);
|
|
1484
|
-
return this;
|
|
1485
|
-
}
|
|
1486
|
-
/** Assert a directory exists */
|
|
1487
|
-
async toHaveDirectory(path) {
|
|
1488
|
-
if (!await this.container.exists(path)) throw new Error(`Expected directory ${path} to exist in container`);
|
|
1489
|
-
return this;
|
|
1490
|
-
}
|
|
1491
|
-
/** Assert a mount exists */
|
|
1492
|
-
async toHaveMount(destination) {
|
|
1493
|
-
const info = await this.container.inspect();
|
|
1494
|
-
if (!info.hostConfig.mounts.find((m) => m.destination === destination)) {
|
|
1495
|
-
const available = info.hostConfig.mounts.map((m) => m.destination).join(", ");
|
|
1496
|
-
throw new Error(`Expected mount at ${destination}, found: [${available}]`);
|
|
1497
|
-
}
|
|
1498
|
-
return this;
|
|
1499
|
-
}
|
|
1500
|
-
/** Assert network mode */
|
|
1501
|
-
async toHaveNetwork(mode) {
|
|
1502
|
-
const info = await this.container.inspect();
|
|
1503
|
-
if (!info.hostConfig.networkMode.includes(mode)) throw new Error(`Expected network mode "${mode}", got "${info.hostConfig.networkMode}"`);
|
|
1504
|
-
return this;
|
|
1505
|
-
}
|
|
1506
|
-
/** Assert memory limit */
|
|
1507
|
-
async toHaveMemoryLimit(bytes) {
|
|
1508
|
-
const info = await this.container.inspect();
|
|
1509
|
-
if (info.hostConfig.memory !== bytes) throw new Error(`Expected memory limit ${bytes}, got ${info.hostConfig.memory}`);
|
|
1510
|
-
return this;
|
|
1511
|
-
}
|
|
1512
|
-
/** Assert CPU quota */
|
|
1513
|
-
async toHaveCpuQuota(quota) {
|
|
1514
|
-
const info = await this.container.inspect();
|
|
1515
|
-
if (info.hostConfig.cpuQuota !== quota) throw new Error(`Expected CPU quota ${quota}, got ${info.hostConfig.cpuQuota}`);
|
|
1516
|
-
return this;
|
|
1517
|
-
}
|
|
1518
|
-
/** Execute a command and return output for custom assertions */
|
|
1519
|
-
async exec(cmd) {
|
|
1520
|
-
return this.container.exec(cmd);
|
|
1521
|
-
}
|
|
1522
|
-
/** Read a file for custom assertions */
|
|
1523
|
-
async readFile(path) {
|
|
1524
|
-
const file = await this.container.file(path);
|
|
1525
|
-
if (!file.exists) throw new Error(`File ${path} does not exist`);
|
|
1526
|
-
return file.content;
|
|
1527
|
-
}
|
|
1528
|
-
/** Get logs for custom assertions */
|
|
1529
|
-
async getLogs(tail) {
|
|
1530
|
-
return this.container.logs(tail);
|
|
1531
|
-
}
|
|
1532
|
-
};
|
|
1533
|
-
//#endregion
|
|
1534
|
-
//#region src/utilities/grep.ts
|
|
1535
|
-
/**
|
|
1536
|
-
* Extract text blocks from output that contain a pattern.
|
|
1537
|
-
* Splits by blank lines (how linter/compiler output is structured),
|
|
1538
|
-
* returns only blocks matching the pattern.
|
|
1539
|
-
*
|
|
1540
|
-
* @example
|
|
1541
|
-
* expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
|
|
1542
|
-
* expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
|
|
1543
|
-
*/
|
|
1544
|
-
function grep(output, pattern) {
|
|
1545
|
-
return output.replace(/\x1b\[[0-9;]*m/g, "").split(/\n\s*\n/).filter((block) => block.includes(pattern)).join("\n\n");
|
|
1546
|
-
}
|
|
1547
|
-
//#endregion
|
|
1548
|
-
//#region src/mocking/mock-of-date.ts
|
|
1549
|
-
const mockOfDate = mockdate.default;
|
|
1550
|
-
//#endregion
|
|
1551
|
-
//#region src/mocking/mock-of.ts
|
|
1552
|
-
const mockOf = vitest_mock_extended.mockDeep;
|
|
1553
|
-
//#endregion
|
|
1554
1683
|
exports.DirectoryAccessor = DirectoryAccessor;
|
|
1555
1684
|
exports.DockerAssertion = DockerAssertion;
|
|
1556
1685
|
exports.ExecAdapter = ExecAdapter;
|
|
@@ -1561,17 +1690,22 @@ exports.ResponseAccessor = ResponseAccessor;
|
|
|
1561
1690
|
exports.SpecificationBuilder = SpecificationBuilder;
|
|
1562
1691
|
exports.SpecificationResult = SpecificationResult;
|
|
1563
1692
|
exports.TableAssertion = TableAssertion;
|
|
1693
|
+
exports.app = app;
|
|
1564
1694
|
exports.cli = cli;
|
|
1695
|
+
exports.command = command;
|
|
1565
1696
|
exports.createSpecificationRunner = createSpecificationRunner;
|
|
1566
1697
|
exports.dockerContainer = dockerContainer;
|
|
1567
1698
|
exports.e2e = e2e;
|
|
1568
1699
|
exports.grep = grep;
|
|
1569
1700
|
exports.integration = integration;
|
|
1570
|
-
exports.mockOf = mockOf;
|
|
1571
|
-
exports.mockOfDate = mockOfDate;
|
|
1701
|
+
exports.mockOf = require_mock_of.mockOf;
|
|
1702
|
+
exports.mockOfDate = require_mock_of.mockOfDate;
|
|
1572
1703
|
exports.normalizeOutput = normalizeOutput;
|
|
1573
|
-
exports.postgres = postgres;
|
|
1574
|
-
exports.redis = redis;
|
|
1704
|
+
exports.postgres = require_sqlite_adapter.postgres;
|
|
1705
|
+
exports.redis = require_sqlite_adapter.redis;
|
|
1706
|
+
exports.spec = spec;
|
|
1707
|
+
exports.sqlite = require_sqlite_adapter.sqlite;
|
|
1708
|
+
exports.stack = stack;
|
|
1575
1709
|
exports.stripAnsi = stripAnsi;
|
|
1576
1710
|
|
|
1577
1711
|
//# sourceMappingURL=index.cjs.map
|