@jterrazz/test 5.3.2 → 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;
@@ -667,197 +813,241 @@ function getCallerDir() {
667
813
  }
668
814
  throw new Error("Cannot detect caller directory from stack trace");
669
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
+ */
670
820
  function createSpecificationRunner(config) {
671
821
  return (label) => {
672
822
  return new SpecificationBuilder(config, getCallerDir(), label);
673
823
  };
674
824
  }
675
825
  //#endregion
676
- //#region src/adapters/compose.adapter.ts
826
+ //#region src/docker/docker-adapter.ts
677
827
  /**
678
- * 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.
679
830
  */
680
- var ComposeStackAdapter = class {
681
- composeFile;
682
- started = false;
683
- constructor(composeFile) {
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"}`;
831
+ var DockerAdapter = class {
832
+ containerId;
833
+ constructor(containerId) {
834
+ this.containerId = containerId;
739
835
  }
740
- createDatabaseAdapter() {
741
- 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();
742
841
  }
743
- async healthcheck() {
744
- if (!this.connectionString) throw new Error("postgres: cannot healthcheck — no connection string");
842
+ async file(path) {
745
843
  try {
746
- const client = new pg.Client({ connectionString: this.connectionString });
747
- await client.connect();
748
- await client.query("SELECT 1");
749
- await client.end();
750
- } catch (error) {
751
- 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
+ };
752
853
  }
753
854
  }
754
- async initialize(composeDir) {
755
- if (!this.composeName) return;
756
- const initPaths = [(0, node_path.resolve)(composeDir, `${this.composeName}/init.sql`), (0, node_path.resolve)(composeDir, "postgres/init.sql")];
757
- for (const initPath of initPaths) if ((0, node_fs.existsSync)(initPath)) {
758
- const sql = (0, node_fs.readFileSync)(initPath, "utf8");
759
- try {
760
- await this.seed(sql);
761
- } catch (error) {
762
- throw new Error(`postgres init script failed (${initPath}):\n${error.message}`, { cause: error });
763
- }
764
- 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;
765
863
  }
766
864
  }
767
- async getClient() {
768
- if (this.client) return this.client;
769
- const client = new pg.Client({ connectionString: this.connectionString });
770
- client.on("error", () => {
771
- 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
772
869
  });
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
870
  }
794
- };
795
- /**
796
- * Create a PostgreSQL service handle.
797
- *
798
- * @example
799
- * const db = postgres({ compose: "db" });
800
- * // After start: db.connectionString is populated
801
- */
802
- function postgres(options = {}) {
803
- return new PostgresHandle(options);
804
- }
805
- //#endregion
806
- //#region src/adapters/redis.adapter.ts
807
- var RedisHandle = class {
808
- type = "redis";
809
- composeName;
810
- defaultPort = 6379;
811
- defaultImage;
812
- environment = {};
813
- connectionString = "";
814
- started = false;
815
- constructor(options = {}) {
816
- this.composeName = options.compose ?? null;
817
- this.defaultImage = options.image ?? "redis:7";
818
- }
819
- buildConnectionString(host, port) {
820
- return `redis://${host}:${port}`;
821
- }
822
- createDatabaseAdapter() {
823
- return null;
824
- }
825
- async healthcheck() {
826
- if (!this.connectionString) throw new Error("redis: cannot healthcheck — no connection string");
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
- }
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
+ };
836
904
  }
837
- async initialize() {}
838
- async reset() {
839
- const { createClient } = await import("redis");
840
- const client = createClient({ url: this.connectionString });
841
- await client.connect();
905
+ async exists(path) {
842
906
  try {
843
- await client.flushAll();
844
- } finally {
845
- await client.disconnect();
907
+ await this.exec([
908
+ "test",
909
+ "-e",
910
+ path
911
+ ]);
912
+ return true;
913
+ } catch {
914
+ return false;
846
915
  }
847
916
  }
848
917
  };
849
- /**
850
- * Create a Redis service handle.
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);
918
+ /** Create a {@link DockerContainerPort} bound to an existing container by ID. */
919
+ function dockerContainer(containerId) {
920
+ return new DockerAdapter(containerId);
858
921
  }
859
922
  //#endregion
860
- //#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
861
1051
  /**
862
1052
  * Container adapter using testcontainers.
863
1053
  * Wraps a GenericContainer for programmatic container lifecycle.
@@ -1000,6 +1190,7 @@ var Orchestrator = class {
1000
1190
  services;
1001
1191
  mode;
1002
1192
  root;
1193
+ projectName;
1003
1194
  running = [];
1004
1195
  composeStack = null;
1005
1196
  composeHandles = [];
@@ -1008,6 +1199,7 @@ var Orchestrator = class {
1008
1199
  this.services = options.services;
1009
1200
  this.mode = options.mode;
1010
1201
  this.root = options.root ?? process.cwd();
1202
+ this.projectName = options.projectName;
1011
1203
  }
1012
1204
  /**
1013
1205
  * Start declared services via testcontainers (integration mode).
@@ -1105,12 +1297,12 @@ var Orchestrator = class {
1105
1297
  const startTime = Date.now();
1106
1298
  const composeDir = (0, node_path.dirname)(composePath);
1107
1299
  const composeConfig = parseComposeFile(composePath);
1108
- this.composeStack = new ComposeStackAdapter(composePath);
1300
+ this.composeStack = new ComposeStackAdapter(composePath, this.projectName);
1109
1301
  await this.composeStack.start();
1110
1302
  for (const service of composeConfig.infraServices) {
1111
1303
  const type = detectServiceType(service.image);
1112
1304
  if (type === "postgres") {
1113
- const handle = postgres({
1305
+ const handle = require_redis_adapter.postgres({
1114
1306
  compose: service.name,
1115
1307
  env: service.environment
1116
1308
  });
@@ -1120,7 +1312,7 @@ var Orchestrator = class {
1120
1312
  handle.started = true;
1121
1313
  this.composeHandles.push(handle);
1122
1314
  } else if (type === "redis") {
1123
- const handle = redis({ compose: service.name });
1315
+ const handle = require_redis_adapter.redis({ compose: service.name });
1124
1316
  const port = this.composeStack.getMappedPort(service.name, 6379);
1125
1317
  handle.connectionString = handle.buildConnectionString("localhost", port);
1126
1318
  handle.started = true;
@@ -1215,6 +1407,169 @@ function resolveCommand(command, root) {
1215
1407
  return command;
1216
1408
  }
1217
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
1218
1573
  //#region src/runner/cli.ts
1219
1574
  /**
1220
1575
  * Create a CLI specification runner.
@@ -1250,36 +1605,6 @@ async function cli(options) {
1250
1605
  return runner;
1251
1606
  }
1252
1607
  //#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
1608
  //#region src/runner/e2e.ts
1284
1609
  /**
1285
1610
  * Create an E2E specification runner.
@@ -1306,36 +1631,6 @@ async function e2e(options = {}) {
1306
1631
  return runner;
1307
1632
  }
1308
1633
  //#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
1634
  //#region src/runner/integration.ts
1340
1635
  /**
1341
1636
  * Create an integration specification runner.
@@ -1361,196 +1656,6 @@ async function integration(options) {
1361
1656
  return runner;
1362
1657
  }
1363
1658
  //#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
1659
  exports.DirectoryAccessor = DirectoryAccessor;
1555
1660
  exports.DockerAssertion = DockerAssertion;
1556
1661
  exports.ExecAdapter = ExecAdapter;
@@ -1561,17 +1666,21 @@ exports.ResponseAccessor = ResponseAccessor;
1561
1666
  exports.SpecificationBuilder = SpecificationBuilder;
1562
1667
  exports.SpecificationResult = SpecificationResult;
1563
1668
  exports.TableAssertion = TableAssertion;
1669
+ exports.app = app;
1564
1670
  exports.cli = cli;
1671
+ exports.command = command;
1565
1672
  exports.createSpecificationRunner = createSpecificationRunner;
1566
1673
  exports.dockerContainer = dockerContainer;
1567
1674
  exports.e2e = e2e;
1568
1675
  exports.grep = grep;
1569
1676
  exports.integration = integration;
1570
- exports.mockOf = mockOf;
1571
- exports.mockOfDate = mockOfDate;
1677
+ exports.mockOf = require_mock_of.mockOf;
1678
+ exports.mockOfDate = require_mock_of.mockOfDate;
1572
1679
  exports.normalizeOutput = normalizeOutput;
1573
- exports.postgres = postgres;
1574
- 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;
1575
1684
  exports.stripAnsi = stripAnsi;
1576
1685
 
1577
1686
  //# sourceMappingURL=index.cjs.map