@jterrazz/test 5.0.0 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -157,11 +157,12 @@ Every test follows the same pattern: `spec("label") → setup → action → ass
157
157
 
158
158
  **CLI:**
159
159
 
160
- | Method | Description |
161
- | -------------------------------------- | ----------------------------------------------------------- |
162
- | `.exec("args")` | Run command (blocking) |
163
- | `.exec(["build", "start"])` | Run commands sequentially in same directory |
164
- | `.spawn("args", { waitFor, timeout })` | Run long-lived process, resolve on pattern match or timeout |
160
+ | Method | Description |
161
+ | -------------------------------------- | ------------------------------------------------------------------------------------- |
162
+ | `.exec("args")` | Run command (blocking) |
163
+ | `.exec(["build", "start"])` | Run commands sequentially in same directory |
164
+ | `.spawn("args", { waitFor, timeout })` | Run long-lived process, resolve on pattern match or timeout |
165
+ | `.env({ KEY: "value" })` | Set env vars on the child process (`null` unsets, `$WORKDIR` expands to the temp cwd) |
165
166
 
166
167
  ### Assertions
167
168
 
@@ -184,6 +185,16 @@ Result properties are raw values — use vitest `expect()` for assertions. Datab
184
185
  | `expect(result.file("dist/index.js").content).toContain("Hello")` | Assert file contains string |
185
186
  | `expect(result.file("dist/index.cjs").exists).toBe(false)` | Assert file does not exist |
186
187
 
188
+ **Directories (CLI scaffolding / codegen output):**
189
+
190
+ | Expression | Description |
191
+ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------ |
192
+ | `await result.directory("out").toMatchFixture("go-api")` | Snapshot the tree against `expected/go-api/`, structured diff on mismatch |
193
+ | `await result.directory().toMatchFixture("scaffold", { ignore })` | Pass extra ignore patterns; defaults already skip `.git`, `node_modules`, etc. |
194
+ | `await result.directory("out").files()` | List all files (recursive, sorted) for ad-hoc assertions |
195
+
196
+ Run with `JTERRAZZ_TEST_UPDATE=1` (or vitest `-u`) to overwrite fixtures with the current output. Fixtures live at `{test}/expected/{name}/` — same convention as `responses/` for HTTP bodies.
197
+
187
198
  **Grep (scoped text matching):**
188
199
 
189
200
  ```typescript
package/dist/index.cjs CHANGED
@@ -30,6 +30,7 @@ let node_child_process = require("node:child_process");
30
30
  let yaml = require("yaml");
31
31
  let pg = require("pg");
32
32
  let node_os = require("node:os");
33
+ let node_fs_promises = require("node:fs/promises");
33
34
  //#region src/mocking/mock-of-date.ts
34
35
  const mockOfDate = mockdate.default;
35
36
  //#endregion
@@ -292,6 +293,47 @@ function formatResponseDiff(file, expected, actual) {
292
293
  }
293
294
  return lines.join("\n");
294
295
  }
296
+ function formatDirectoryDiff(fixtureName, diff, hint) {
297
+ const lines = [];
298
+ const total = diff.added.length + diff.removed.length + diff.changed.length;
299
+ lines.push(`Directory mismatch: ${BOLD}${fixtureName}${RESET}`);
300
+ lines.push(`${DIM} ${total} difference${total === 1 ? "" : "s"}: ${diff.added.length} added, ${diff.removed.length} removed, ${diff.changed.length} changed${RESET}`);
301
+ lines.push("");
302
+ lines.push(`${GREEN}- Expected (fixture)${RESET}`);
303
+ lines.push(`${RED}+ Received (generated)${RESET}`);
304
+ lines.push("");
305
+ for (const path of diff.added) lines.push(`${RED}+ added ${path}${RESET} ${DIM}(not in fixture)${RESET}`);
306
+ for (const path of diff.removed) lines.push(`${GREEN}- removed ${path}${RESET} ${DIM}(in fixture, not generated)${RESET}`);
307
+ for (const { path, expected, actual } of diff.changed) {
308
+ const expectedLines = expected.split("\n");
309
+ const actualLines = actual.split("\n");
310
+ const changedCount = countLineDifferences(expectedLines, actualLines);
311
+ lines.push(`${BOLD}~ changed ${path}${RESET} ${DIM}(${changedCount} line${changedCount === 1 ? "" : "s"} differ)${RESET}`);
312
+ let shown = 0;
313
+ const maxShown = 5;
314
+ const maxLines = Math.max(expectedLines.length, actualLines.length);
315
+ for (let i = 0; i < maxLines && shown < maxShown; i++) {
316
+ const exp = expectedLines[i];
317
+ const act = actualLines[i];
318
+ if (exp !== act) {
319
+ lines.push(`${DIM} line ${i + 1}:${RESET}`);
320
+ if (exp !== void 0) lines.push(` ${GREEN}- ${exp}${RESET}`);
321
+ if (act !== void 0) lines.push(` ${RED}+ ${act}${RESET}`);
322
+ shown++;
323
+ }
324
+ }
325
+ if (changedCount > maxShown) lines.push(` ${DIM}... ${changedCount - maxShown} more line(s)${RESET}`);
326
+ }
327
+ lines.push("");
328
+ lines.push(`${DIM}${hint}${RESET}`);
329
+ return lines.join("\n");
330
+ }
331
+ function countLineDifferences(expected, actual) {
332
+ let count = 0;
333
+ const max = Math.max(expected.length, actual.length);
334
+ for (let i = 0; i < max; i++) if (expected[i] !== actual[i]) count++;
335
+ return count;
336
+ }
295
337
  function rowLabel(n) {
296
338
  return n === 1 ? "1 row" : `${n} rows`;
297
339
  }
@@ -643,6 +685,19 @@ var Orchestrator = class {
643
685
  //#endregion
644
686
  //#region src/specification/adapters/exec.adapter.ts
645
687
  /**
688
+ * Build a child-process env from the parent env plus user overrides.
689
+ * `null` overrides delete keys (e.g. `INIT_CWD: null`).
690
+ */
691
+ function buildEnv(extra) {
692
+ const env = {
693
+ ...process.env,
694
+ INIT_CWD: void 0
695
+ };
696
+ if (extra) for (const [key, value] of Object.entries(extra)) if (value === null) delete env[key];
697
+ else env[key] = value;
698
+ return env;
699
+ }
700
+ /**
646
701
  * Executes CLI commands via execSync (blocking) or spawn (long-running).
647
702
  * Used by cli() for local command execution.
648
703
  */
@@ -651,11 +706,8 @@ var ExecAdapter = class {
651
706
  constructor(command) {
652
707
  this.command = command;
653
708
  }
654
- async exec(args, cwd) {
655
- const env = {
656
- ...process.env,
657
- INIT_CWD: void 0
658
- };
709
+ async exec(args, cwd, extraEnv) {
710
+ const env = buildEnv(extraEnv);
659
711
  try {
660
712
  return {
661
713
  exitCode: 0,
@@ -679,11 +731,8 @@ var ExecAdapter = class {
679
731
  };
680
732
  }
681
733
  }
682
- async spawn(args, cwd, options) {
683
- const env = {
684
- ...process.env,
685
- INIT_CWD: void 0
686
- };
734
+ async spawn(args, cwd, options, extraEnv) {
735
+ const env = buildEnv(extraEnv);
687
736
  return new Promise((resolve) => {
688
737
  let stdout = "";
689
738
  let stderr = "";
@@ -790,6 +839,75 @@ var HonoAdapter = class {
790
839
  }
791
840
  };
792
841
  //#endregion
842
+ //#region src/specification/directory.ts
843
+ /**
844
+ * Default ignore patterns — paths that should never appear in a tracked snapshot.
845
+ * Each entry is matched against any path segment OR a path prefix.
846
+ */
847
+ const DEFAULT_IGNORES = [
848
+ ".git",
849
+ ".DS_Store",
850
+ "node_modules",
851
+ ".next",
852
+ "dist",
853
+ ".turbo",
854
+ ".cache"
855
+ ];
856
+ /**
857
+ * Recursively walk a directory, returning sorted relative paths of files only.
858
+ * Ignored entries (default + caller-supplied) are skipped.
859
+ */
860
+ async function walkDirectory(root, options = {}) {
861
+ const ignores = new Set([...DEFAULT_IGNORES, ...options.ignore ?? []]);
862
+ const out = [];
863
+ async function walk(current) {
864
+ let entries;
865
+ try {
866
+ entries = await (0, node_fs_promises.readdir)(current);
867
+ } catch {
868
+ return;
869
+ }
870
+ for (const entry of entries) {
871
+ if (ignores.has(entry)) continue;
872
+ const abs = (0, node_path.resolve)(current, entry);
873
+ const stat = (0, node_fs.statSync)(abs);
874
+ if (stat.isDirectory()) await walk(abs);
875
+ else if (stat.isFile()) out.push((0, node_path.relative)(root, abs).split(node_path.sep).join("/"));
876
+ }
877
+ }
878
+ await walk(root);
879
+ out.sort();
880
+ return out;
881
+ }
882
+ /**
883
+ * Compare two directory trees file-by-file.
884
+ * Binary files are compared by byte equality but reported without inline diff.
885
+ */
886
+ async function diffDirectories(expectedRoot, actualRoot, options = {}) {
887
+ const expectedFiles = await walkDirectory(expectedRoot, options);
888
+ const actualFiles = await walkDirectory(actualRoot, options);
889
+ const expectedSet = new Set(expectedFiles);
890
+ const actualSet = new Set(actualFiles);
891
+ const added = actualFiles.filter((f) => !expectedSet.has(f));
892
+ const removed = expectedFiles.filter((f) => !actualSet.has(f));
893
+ const changed = [];
894
+ for (const file of expectedFiles) {
895
+ if (!actualSet.has(file)) continue;
896
+ const expected = (0, node_fs.readFileSync)((0, node_path.resolve)(expectedRoot, file), "utf8");
897
+ const actual = (0, node_fs.readFileSync)((0, node_path.resolve)(actualRoot, file), "utf8");
898
+ if (expected !== actual) changed.push({
899
+ actual,
900
+ expected,
901
+ path: file
902
+ });
903
+ }
904
+ return {
905
+ added,
906
+ changed,
907
+ removed
908
+ };
909
+ }
910
+ //#endregion
793
911
  //#region src/specification/specification.ts
794
912
  var TableAssertion = class {
795
913
  tableName;
@@ -807,6 +925,54 @@ var TableAssertion = class {
807
925
  if (actual.length !== 0) throw new Error(`Expected table "${this.tableName}" to be empty, but it has ${actual.length} rows`);
808
926
  }
809
927
  };
928
+ /**
929
+ * Detect whether the user wants to update snapshots — `true` for any of:
930
+ * - vitest run with `-u` / `--update`
931
+ * - JTERRAZZ_TEST_UPDATE=1
932
+ * - UPDATE_SNAPSHOTS=1
933
+ */
934
+ function shouldUpdateSnapshots() {
935
+ if (process.env.JTERRAZZ_TEST_UPDATE === "1") return true;
936
+ if (process.env.UPDATE_SNAPSHOTS === "1") return true;
937
+ if (process.argv.includes("-u") || process.argv.includes("--update")) return true;
938
+ return false;
939
+ }
940
+ var DirectoryAccessor = class {
941
+ absPath;
942
+ testDir;
943
+ constructor(absPath, testDir) {
944
+ this.absPath = absPath;
945
+ this.testDir = testDir;
946
+ }
947
+ /**
948
+ * Compare the directory tree against `expected/{name}/` (relative to the test file).
949
+ * On mismatch, throws with a structured diff. With update mode enabled, the
950
+ * fixture is overwritten with the current contents instead.
951
+ */
952
+ async toMatchFixture(name, options = {}) {
953
+ const fixtureDir = (0, node_path.resolve)(this.testDir, "expected", name);
954
+ if (options.update ?? shouldUpdateSnapshots()) {
955
+ (0, node_fs.rmSync)(fixtureDir, {
956
+ force: true,
957
+ recursive: true
958
+ });
959
+ (0, node_fs.mkdirSync)(fixtureDir, { recursive: true });
960
+ (0, node_fs.cpSync)(this.absPath, fixtureDir, { recursive: true });
961
+ return;
962
+ }
963
+ if (!(0, node_fs.existsSync)(fixtureDir)) throw new Error(`Directory fixture "${name}" does not exist at ${fixtureDir}.\nRun with JTERRAZZ_TEST_UPDATE=1 (or vitest -u) to create it.`);
964
+ const diff = await diffDirectories(fixtureDir, this.absPath, { ignore: options.ignore });
965
+ if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0) return;
966
+ throw new Error(formatDirectoryDiff(name, diff, "Run with JTERRAZZ_TEST_UPDATE=1 to update the fixture."));
967
+ }
968
+ /**
969
+ * List all files in the directory (recursive, sorted, ignoring defaults).
970
+ * Useful for ad-hoc assertions when you don't want a full snapshot.
971
+ */
972
+ async files(options = {}) {
973
+ return walkDirectory(this.absPath, options);
974
+ }
975
+ };
810
976
  var ResponseAccessor = class {
811
977
  body;
812
978
  testDir;
@@ -854,6 +1020,9 @@ var SpecificationResult = class {
854
1020
  if (!this.responseData) throw new Error(".response requires an HTTP action (.get(), .post(), etc.)");
855
1021
  return new ResponseAccessor(this.responseData.body, this.testDir);
856
1022
  }
1023
+ directory(path = ".") {
1024
+ return new DirectoryAccessor((0, node_path.resolve)(this.workDir ?? this.testDir, path), this.testDir);
1025
+ }
857
1026
  file(path) {
858
1027
  const resolvedPath = (0, node_path.resolve)(this.workDir ?? this.testDir, path);
859
1028
  const exists = (0, node_fs.existsSync)(resolvedPath);
@@ -877,6 +1046,7 @@ var SpecificationResult = class {
877
1046
  };
878
1047
  var SpecificationBuilder = class {
879
1048
  commandArgs = null;
1049
+ commandEnv = {};
880
1050
  config;
881
1051
  fixtures = [];
882
1052
  label;
@@ -910,6 +1080,23 @@ var SpecificationBuilder = class {
910
1080
  this.mocks.push({ file });
911
1081
  return this;
912
1082
  }
1083
+ /**
1084
+ * Set environment variables for the CLI process. Merged on top of process.env.
1085
+ * Use `null` to unset a variable. Multiple calls merge.
1086
+ *
1087
+ * The token `$WORKDIR` (in any value) is replaced with the actual working
1088
+ * directory at run-time — useful for tests that need a fully isolated `HOME`.
1089
+ *
1090
+ * @example
1091
+ * spec("...").env({ HOME: "$WORKDIR", TZ: "UTC" }).exec("status").run();
1092
+ */
1093
+ env(env) {
1094
+ this.commandEnv = {
1095
+ ...this.commandEnv,
1096
+ ...env
1097
+ };
1098
+ return this;
1099
+ }
913
1100
  get(path) {
914
1101
  this.request = {
915
1102
  method: "GET",
@@ -975,6 +1162,16 @@ var SpecificationBuilder = class {
975
1162
  if (hasHttpAction) return this.runHttpAction();
976
1163
  return this.runCliAction(workDir);
977
1164
  }
1165
+ resolveEnv(workDir) {
1166
+ const keys = Object.keys(this.commandEnv);
1167
+ if (keys.length === 0) return;
1168
+ const resolved = {};
1169
+ for (const key of keys) {
1170
+ const value = this.commandEnv[key];
1171
+ resolved[key] = typeof value === "string" ? value.replace(/\$WORKDIR/g, workDir) : value;
1172
+ }
1173
+ return resolved;
1174
+ }
978
1175
  prepareWorkDir() {
979
1176
  if (!this.projectName && this.fixtures.length === 0) return this.config.fixturesRoot ?? process.cwd();
980
1177
  const tempDir = (0, node_fs.mkdtempSync)((0, node_path.resolve)((0, node_os.tmpdir)(), "spec-cli-"));
@@ -1003,8 +1200,9 @@ var SpecificationBuilder = class {
1003
1200
  }
1004
1201
  async runCliAction(workDir) {
1005
1202
  if (!this.config.command) throw new Error("CLI actions require a command adapter (use cli())");
1203
+ const env = this.resolveEnv(workDir);
1006
1204
  let commandResult;
1007
- if (this.spawnConfig) commandResult = await this.config.command.spawn(this.spawnConfig.args, workDir, this.spawnConfig.options);
1205
+ if (this.spawnConfig) commandResult = await this.config.command.spawn(this.spawnConfig.args, workDir, this.spawnConfig.options, env);
1008
1206
  else if (Array.isArray(this.commandArgs)) {
1009
1207
  commandResult = {
1010
1208
  exitCode: 0,
@@ -1012,10 +1210,10 @@ var SpecificationBuilder = class {
1012
1210
  stdout: ""
1013
1211
  };
1014
1212
  for (const args of this.commandArgs) {
1015
- commandResult = await this.config.command.exec(args, workDir);
1213
+ commandResult = await this.config.command.exec(args, workDir, env);
1016
1214
  if (commandResult.exitCode !== 0) break;
1017
1215
  }
1018
- } else commandResult = await this.config.command.exec(this.commandArgs, workDir);
1216
+ } else commandResult = await this.config.command.exec(this.commandArgs, workDir, env);
1019
1217
  return new SpecificationResult({
1020
1218
  commandResult,
1021
1219
  config: this.config,
@@ -1176,11 +1374,183 @@ async function cli(options) {
1176
1374
  return runner;
1177
1375
  }
1178
1376
  //#endregion
1377
+ //#region src/infrastructure/docker/docker-adapter.ts
1378
+ var DockerAdapter = class {
1379
+ containerId;
1380
+ constructor(containerId) {
1381
+ this.containerId = containerId;
1382
+ }
1383
+ async exec(cmd) {
1384
+ return (0, node_child_process.execSync)(`docker exec ${this.containerId} ${cmd.map((c) => `'${c}'`).join(" ")}`, {
1385
+ encoding: "utf8",
1386
+ timeout: 1e4
1387
+ }).trim();
1388
+ }
1389
+ async file(path) {
1390
+ try {
1391
+ return {
1392
+ exists: true,
1393
+ content: await this.exec(["cat", path])
1394
+ };
1395
+ } catch {
1396
+ return {
1397
+ exists: false,
1398
+ content: ""
1399
+ };
1400
+ }
1401
+ }
1402
+ async isRunning() {
1403
+ try {
1404
+ return (0, node_child_process.execSync)(`docker inspect --format='{{.State.Running}}' ${this.containerId}`, {
1405
+ encoding: "utf8",
1406
+ timeout: 5e3
1407
+ }).trim() === "true";
1408
+ } catch {
1409
+ return false;
1410
+ }
1411
+ }
1412
+ async logs(tail) {
1413
+ return (0, node_child_process.execSync)(`docker logs ${tail ? `--tail ${tail}` : ""} ${this.containerId}`, {
1414
+ encoding: "utf8",
1415
+ timeout: 1e4
1416
+ });
1417
+ }
1418
+ async inspect() {
1419
+ const raw = (0, node_child_process.execSync)(`docker inspect ${this.containerId}`, {
1420
+ encoding: "utf8",
1421
+ timeout: 5e3
1422
+ });
1423
+ const data = JSON.parse(raw)[0];
1424
+ return {
1425
+ id: data.Id,
1426
+ name: data.Name,
1427
+ state: {
1428
+ running: data.State.Running,
1429
+ exitCode: data.State.ExitCode,
1430
+ status: data.State.Status
1431
+ },
1432
+ config: {
1433
+ image: data.Config.Image,
1434
+ env: data.Config.Env || []
1435
+ },
1436
+ hostConfig: {
1437
+ memory: data.HostConfig.Memory || 0,
1438
+ cpuQuota: data.HostConfig.CpuQuota || 0,
1439
+ networkMode: data.HostConfig.NetworkMode || "",
1440
+ mounts: (data.Mounts || []).map((m) => ({
1441
+ source: m.Source,
1442
+ destination: m.Destination,
1443
+ type: m.Type
1444
+ }))
1445
+ },
1446
+ networkSettings: { networks: Object.fromEntries(Object.entries(data.NetworkSettings.Networks || {}).map(([name, net]) => [name, {
1447
+ gateway: net.Gateway,
1448
+ ipAddress: net.IPAddress
1449
+ }])) }
1450
+ };
1451
+ }
1452
+ async exists(path) {
1453
+ try {
1454
+ await this.exec([
1455
+ "test",
1456
+ "-e",
1457
+ path
1458
+ ]);
1459
+ return true;
1460
+ } catch {
1461
+ return false;
1462
+ }
1463
+ }
1464
+ };
1465
+ /** Create a Docker container port for an existing container */
1466
+ function dockerContainer(containerId) {
1467
+ return new DockerAdapter(containerId);
1468
+ }
1469
+ //#endregion
1470
+ //#region src/infrastructure/docker/docker-assertion.ts
1471
+ /** Fluent assertion builder for Docker containers */
1472
+ var DockerAssertion = class {
1473
+ container;
1474
+ constructor(container) {
1475
+ this.container = container;
1476
+ }
1477
+ /** Assert the container is running */
1478
+ async toBeRunning() {
1479
+ if (!await this.container.isRunning()) throw new Error("Expected container to be running");
1480
+ return this;
1481
+ }
1482
+ /** Assert the container is NOT running / doesn't exist */
1483
+ async toNotExist() {
1484
+ if (await this.container.isRunning()) throw new Error("Expected container to not exist or not be running");
1485
+ return this;
1486
+ }
1487
+ /** Assert a file exists inside the container */
1488
+ async toHaveFile(path, opts) {
1489
+ const file = await this.container.file(path);
1490
+ if (!file.exists) throw new Error(`Expected file ${path} to exist in container`);
1491
+ if (opts?.containing && !file.content.includes(opts.containing)) throw new Error(`Expected file ${path} to contain "${opts.containing}", got: ${file.content.slice(0, 200)}`);
1492
+ return this;
1493
+ }
1494
+ /** Assert a file does NOT exist */
1495
+ async toNotHaveFile(path) {
1496
+ if (await this.container.exists(path)) throw new Error(`Expected ${path} to not exist in container`);
1497
+ return this;
1498
+ }
1499
+ /** Assert a directory exists */
1500
+ async toHaveDirectory(path) {
1501
+ if (!await this.container.exists(path)) throw new Error(`Expected directory ${path} to exist in container`);
1502
+ return this;
1503
+ }
1504
+ /** Assert a mount exists */
1505
+ async toHaveMount(destination) {
1506
+ const info = await this.container.inspect();
1507
+ if (!info.hostConfig.mounts.find((m) => m.destination === destination)) {
1508
+ const available = info.hostConfig.mounts.map((m) => m.destination).join(", ");
1509
+ throw new Error(`Expected mount at ${destination}, found: [${available}]`);
1510
+ }
1511
+ return this;
1512
+ }
1513
+ /** Assert network mode */
1514
+ async toHaveNetwork(mode) {
1515
+ const info = await this.container.inspect();
1516
+ if (!info.hostConfig.networkMode.includes(mode)) throw new Error(`Expected network mode "${mode}", got "${info.hostConfig.networkMode}"`);
1517
+ return this;
1518
+ }
1519
+ /** Assert memory limit */
1520
+ async toHaveMemoryLimit(bytes) {
1521
+ const info = await this.container.inspect();
1522
+ if (info.hostConfig.memory !== bytes) throw new Error(`Expected memory limit ${bytes}, got ${info.hostConfig.memory}`);
1523
+ return this;
1524
+ }
1525
+ /** Assert CPU quota */
1526
+ async toHaveCpuQuota(quota) {
1527
+ const info = await this.container.inspect();
1528
+ if (info.hostConfig.cpuQuota !== quota) throw new Error(`Expected CPU quota ${quota}, got ${info.hostConfig.cpuQuota}`);
1529
+ return this;
1530
+ }
1531
+ /** Execute a command and return output for custom assertions */
1532
+ async exec(cmd) {
1533
+ return this.container.exec(cmd);
1534
+ }
1535
+ /** Read a file for custom assertions */
1536
+ async readFile(path) {
1537
+ const file = await this.container.file(path);
1538
+ if (!file.exists) throw new Error(`File ${path} does not exist`);
1539
+ return file.content;
1540
+ }
1541
+ /** Get logs for custom assertions */
1542
+ async getLogs(tail) {
1543
+ return this.container.logs(tail);
1544
+ }
1545
+ };
1546
+ //#endregion
1547
+ exports.DockerAssertion = DockerAssertion;
1179
1548
  exports.ExecAdapter = ExecAdapter;
1180
1549
  exports.FetchAdapter = FetchAdapter;
1181
1550
  exports.HonoAdapter = HonoAdapter;
1182
1551
  exports.Orchestrator = Orchestrator;
1183
1552
  exports.cli = cli;
1553
+ exports.dockerContainer = dockerContainer;
1184
1554
  exports.e2e = e2e;
1185
1555
  exports.grep = grep;
1186
1556
  exports.integration = integration;