@jterrazz/test 5.3.2 → 6.1.0

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