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