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