@jterrazz/test 6.2.0 → 6.4.1

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
@@ -6,7 +6,7 @@ import { tmpdir } from "node:os";
6
6
  import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
7
7
  import { readdir } from "node:fs/promises";
8
8
  import { parse } from "yaml";
9
- //#region src/adapters/exec.adapter.ts
9
+ //#region src/builder/cli/adapters/exec.adapter.ts
10
10
  /**
11
11
  * Build a child-process env from the parent env plus user overrides.
12
12
  * `null` overrides delete keys (e.g. `INIT_CWD: null`).
@@ -103,7 +103,7 @@ var ExecAdapter = class {
103
103
  }
104
104
  };
105
105
  //#endregion
106
- //#region src/adapters/fetch.adapter.ts
106
+ //#region src/builder/http/adapters/fetch.adapter.ts
107
107
  /**
108
108
  * Server adapter that sends real HTTP requests via the Fetch API.
109
109
  * Used by the `e2e()` specification runner to hit a live server.
@@ -136,7 +136,7 @@ var FetchAdapter = class {
136
136
  }
137
137
  };
138
138
  //#endregion
139
- //#region src/adapters/hono.adapter.ts
139
+ //#region src/builder/http/adapters/hono.adapter.ts
140
140
  /**
141
141
  * Server adapter that dispatches requests in-process through a Hono app instance.
142
142
  * Used by the `integration()` specification runner -- no network overhead.
@@ -169,21 +169,7 @@ var HonoAdapter = class {
169
169
  }
170
170
  };
171
171
  //#endregion
172
- //#region src/utilities/grep.ts
173
- /**
174
- * Extract text blocks from output that contain a pattern.
175
- * Splits by blank lines (how linter/compiler output is structured),
176
- * returns only blocks matching the pattern.
177
- *
178
- * @example
179
- * expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
180
- * expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
181
- */
182
- function grep(output, pattern) {
183
- return output.replace(/\x1b\[[0-9;]*m/g, "").split(/\n\s*\n/).filter((block) => block.includes(pattern)).join("\n\n");
184
- }
185
- //#endregion
186
- //#region src/utilities/directory.ts
172
+ //#region src/builder/common/directory.ts
187
173
  /**
188
174
  * Default ignore patterns — paths that should never appear in a tracked snapshot.
189
175
  * Each entry is matched against any path segment OR a path prefix.
@@ -252,7 +238,7 @@ async function diffDirectories(expectedRoot, actualRoot, options = {}) {
252
238
  };
253
239
  }
254
240
  //#endregion
255
- //#region src/utilities/reporter.ts
241
+ //#region src/builder/common/reporter.ts
256
242
  const GREEN = "\x1B[32m";
257
243
  const RED = "\x1B[31m";
258
244
  const DIM = "\x1B[2m";
@@ -390,7 +376,7 @@ function normalizeOutput(str) {
390
376
  return stripAnsi(str).replace(/localhost:\d+/g, "localhost:PORT").replace(/\d+ms/g, "Xms").replace(/\d+\.\d+s/g, "X.Xs").trim();
391
377
  }
392
378
  //#endregion
393
- //#region src/builder/directory-accessor.ts
379
+ //#region src/builder/common/directory-accessor.ts
394
380
  /**
395
381
  * Detect whether the user wants to update snapshots — `true` for any of:
396
382
  * - vitest run with `-u` / `--update`
@@ -444,7 +430,21 @@ var DirectoryAccessor = class {
444
430
  }
445
431
  };
446
432
  //#endregion
447
- //#region src/builder/response-accessor.ts
433
+ //#region src/builder/common/grep.ts
434
+ /**
435
+ * Extract text blocks from output that contain a pattern.
436
+ * Splits by blank lines (how linter/compiler output is structured),
437
+ * returns only blocks matching the pattern.
438
+ *
439
+ * @example
440
+ * expect(grep(result.stdout, "unused-var.ts")).toContain("no-unused-vars")
441
+ * expect(grep(result.stdout, "valid/sorted.ts")).not.toContain("sort-imports")
442
+ */
443
+ function grep(output, pattern) {
444
+ return output.replace(/\x1b\[[0-9;]*m/g, "").split(/\n\s*\n/).filter((block) => block.includes(pattern)).join("\n\n");
445
+ }
446
+ //#endregion
447
+ //#region src/builder/common/response-accessor.ts
448
448
  /** Accessor for an HTTP response body with file-based assertion support. */
449
449
  var ResponseAccessor = class {
450
450
  body;
@@ -465,7 +465,7 @@ var ResponseAccessor = class {
465
465
  }
466
466
  };
467
467
  //#endregion
468
- //#region src/builder/table-assertion.ts
468
+ //#region src/builder/common/table-assertion.ts
469
469
  /** Assertion helper for verifying database table contents after a specification run. */
470
470
  var TableAssertion = class {
471
471
  tableName;
@@ -494,7 +494,7 @@ var TableAssertion = class {
494
494
  }
495
495
  };
496
496
  //#endregion
497
- //#region src/builder/specification-result.ts
497
+ //#region src/builder/common/result.ts
498
498
  /**
499
499
  * The outcome of a single specification run.
500
500
  * Provides accessors for CLI output, HTTP responses, files, directories, and database tables.
@@ -590,6 +590,8 @@ var SpecificationBuilder = class {
590
590
  commandEnv = {};
591
591
  config;
592
592
  fixtures = [];
593
+ intercepts = [];
594
+ jobName = null;
593
595
  label;
594
596
  mocks = [];
595
597
  projectName = null;
@@ -662,6 +664,28 @@ var SpecificationBuilder = class {
662
664
  return this;
663
665
  }
664
666
  /**
667
+ * Intercept an outgoing HTTP request and return a controlled response.
668
+ * Uses MSW under the hood. Intercepts are queued — multiple calls with the
669
+ * same trigger fire sequentially (first match consumed first).
670
+ *
671
+ * @param trigger - What to match (use openai.chat(), anthropic.messages(), http.get(), etc.)
672
+ * @param response - What to return (use openai.response(), http.json(), etc.)
673
+ *
674
+ * @example
675
+ * spec('pipeline')
676
+ * .intercept(openai.chat(), openai.response({ categories: ['TECH'] }))
677
+ * .intercept(openai.chat(), openai.response({ headline: 'AI News' }))
678
+ * .exec('process')
679
+ * .run();
680
+ */
681
+ intercept(trigger, response) {
682
+ this.intercepts.push({
683
+ trigger,
684
+ response
685
+ });
686
+ return this;
687
+ }
688
+ /**
665
689
  * Send a GET request to the server adapter.
666
690
  *
667
691
  * @example
@@ -727,6 +751,18 @@ var SpecificationBuilder = class {
727
751
  return this;
728
752
  }
729
753
  /**
754
+ * Execute a named job registered via the app() factory.
755
+ *
756
+ * @param name - The job name to trigger (must match a registered JobHandle.name).
757
+ *
758
+ * @example
759
+ * spec('pipeline').intercept(openai.chat(), openai.response({...})).job('report-refresh').run();
760
+ */
761
+ job(name) {
762
+ this.jobName = name;
763
+ return this;
764
+ }
765
+ /**
730
766
  * Execute the specification: run seeds, copy fixtures, then perform the
731
767
  * configured action (HTTP or CLI).
732
768
  *
@@ -738,8 +774,14 @@ var SpecificationBuilder = class {
738
774
  async run() {
739
775
  const hasHttpAction = this.request !== null;
740
776
  const hasCliAction = this.commandArgs !== null || this.spawnConfig !== null;
741
- if (!hasHttpAction && !hasCliAction) throw new Error(`Specification "${this.label}": no action defined. Call .get(), .post(), .exec(), etc. before .run()`);
742
- if (hasHttpAction && hasCliAction) throw new Error(`Specification "${this.label}": cannot mix HTTP (.get/.post) and CLI (.exec/.spawn) actions`);
777
+ const hasJobAction = this.jobName !== null;
778
+ const actionCount = [
779
+ hasHttpAction,
780
+ hasCliAction,
781
+ hasJobAction
782
+ ].filter(Boolean).length;
783
+ if (actionCount === 0) throw new Error(`Specification "${this.label}": no action defined. Call .get(), .post(), .exec(), .job(), etc. before .run()`);
784
+ if (actionCount > 1) throw new Error(`Specification "${this.label}": cannot mix action types (.get/.post, .exec/.spawn, .job)`);
743
785
  let workDir = null;
744
786
  if (hasCliAction) workDir = this.prepareWorkDir();
745
787
  if (this.config.databases) for (const db of this.config.databases.values()) await db.reset();
@@ -755,9 +797,18 @@ var SpecificationBuilder = class {
755
797
  await db.seed(sql);
756
798
  }
757
799
  if (this.fixtures.length > 0 && workDir) for (const entry of this.fixtures) cpSync(resolve(this.testDir, "fixtures", entry.file), resolve(workDir, entry.file), { recursive: true });
758
- for (const entry of this.mocks) JSON.parse(readFileSync(resolve(this.testDir, "mock", entry.file), "utf8"));
759
- if (hasHttpAction) return this.runHttpAction();
760
- return this.runCliAction(workDir);
800
+ let cleanupIntercepts = null;
801
+ if (this.intercepts.length > 0) {
802
+ const { registerIntercepts } = await import("./intercept2.js");
803
+ cleanupIntercepts = await registerIntercepts(this.intercepts);
804
+ }
805
+ try {
806
+ if (hasHttpAction) return await this.runHttpAction();
807
+ if (hasJobAction) return await this.runJobAction();
808
+ return await this.runCliAction(workDir);
809
+ } finally {
810
+ if (cleanupIntercepts) cleanupIntercepts();
811
+ }
761
812
  }
762
813
  resolveEnv(workDir) {
763
814
  const keys = Object.keys(this.commandEnv);
@@ -795,6 +846,19 @@ var SpecificationBuilder = class {
795
846
  testDir: this.testDir
796
847
  });
797
848
  }
849
+ async runJobAction() {
850
+ if (!this.config.jobs?.length) throw new Error("Job actions require jobs registered via app(() => ({ server, jobs }))");
851
+ const job = this.config.jobs.find((j) => j.name === this.jobName);
852
+ if (!job) {
853
+ const available = this.config.jobs.map((j) => j.name).join(", ");
854
+ throw new Error(`job("${this.jobName}"): not found. Available: ${available}`);
855
+ }
856
+ await job.execute();
857
+ return new SpecificationResult({
858
+ config: this.config,
859
+ testDir: this.testDir
860
+ });
861
+ }
798
862
  async runCliAction(workDir) {
799
863
  if (!this.config.command) throw new Error("CLI actions require a command adapter (use cli())");
800
864
  const env = this.resolveEnv(workDir);
@@ -828,7 +892,7 @@ function getCallerDir() {
828
892
  if (!match) continue;
829
893
  const filePath = match[1];
830
894
  if (filePath.includes("node_modules")) continue;
831
- if (filePath.includes("/src/builder/") || filePath.includes("/src/runner/")) continue;
895
+ if (filePath.includes("/src/builder/") || filePath.includes("/src/spec/")) continue;
832
896
  return resolve(filePath, "..");
833
897
  }
834
898
  throw new Error("Cannot detect caller directory from stack trace");
@@ -1020,7 +1084,7 @@ var DockerAssertion = class {
1020
1084
  }
1021
1085
  };
1022
1086
  //#endregion
1023
- //#region src/adapters/compose.adapter.ts
1087
+ //#region src/infra/adapters/compose.adapter.ts
1024
1088
  /**
1025
1089
  * Start the full compose stack and stop it all on cleanup.
1026
1090
  * Supports per-worker project names for parallel execution.
@@ -1067,7 +1131,7 @@ var ComposeStackAdapter = class {
1067
1131
  }
1068
1132
  };
1069
1133
  //#endregion
1070
- //#region src/adapters/testcontainers.adapter.ts
1134
+ //#region src/infra/adapters/testcontainers.adapter.ts
1071
1135
  /**
1072
1136
  * Container adapter using testcontainers.
1073
1137
  * Wraps a GenericContainer for programmatic container lifecycle.
@@ -1127,7 +1191,7 @@ var TestcontainersAdapter = class {
1127
1191
  }
1128
1192
  };
1129
1193
  //#endregion
1130
- //#region src/orchestrator/compose-parser.ts
1194
+ //#region src/infra/compose-parser.ts
1131
1195
  /**
1132
1196
  * Detect the service type from the image name.
1133
1197
  */
@@ -1200,7 +1264,7 @@ function parseComposeFile(filePath) {
1200
1264
  };
1201
1265
  }
1202
1266
  //#endregion
1203
- //#region src/orchestrator/orchestrator.ts
1267
+ //#region src/infra/orchestrator.ts
1204
1268
  /**
1205
1269
  * Orchestrator for test infrastructure.
1206
1270
  * Integration: starts services via testcontainers.
@@ -1409,7 +1473,7 @@ var Orchestrator = class {
1409
1473
  }
1410
1474
  };
1411
1475
  //#endregion
1412
- //#region src/runner/resolve.ts
1476
+ //#region src/spec/resolve.ts
1413
1477
  /**
1414
1478
  * Resolve root — if relative, resolves from the caller's directory.
1415
1479
  */
@@ -1423,7 +1487,7 @@ function resolveProjectRoot(root) {
1423
1487
  const match = line.match(/at\s+(?:.*?\()?(?:file:\/\/)?([^:)]+):\d+:\d+/);
1424
1488
  if (!match) continue;
1425
1489
  const filePath = match[1];
1426
- if (filePath.includes("node_modules") || filePath.includes("/src/runner/")) continue;
1490
+ if (filePath.includes("node_modules") || filePath.includes("/src/spec/")) continue;
1427
1491
  return resolve(filePath, "..", root);
1428
1492
  }
1429
1493
  }
@@ -1441,7 +1505,7 @@ function resolveCommand(command, root) {
1441
1505
  return command;
1442
1506
  }
1443
1507
  //#endregion
1444
- //#region src/runner/spec.ts
1508
+ //#region src/spec/spec.ts
1445
1509
  /**
1446
1510
  * Create a specification runner for the given target.
1447
1511
  *
@@ -1489,12 +1553,15 @@ async function startApp(target, options) {
1489
1553
  const key = svc.composeName ?? svc.type;
1490
1554
  servicesMap[key] = svc;
1491
1555
  }
1492
- const honoApp = target.factory(servicesMap);
1556
+ const factoryResult = target.factory(servicesMap);
1557
+ const honoApp = "server" in factoryResult ? factoryResult.server : factoryResult;
1558
+ const jobs = "jobs" in factoryResult ? factoryResult.jobs : void 0;
1493
1559
  const database = orchestrator.getDatabase() ?? void 0;
1494
1560
  const databases = orchestrator.getDatabases();
1495
1561
  const runner = createSpecificationRunner({
1496
1562
  database,
1497
1563
  databases: databases.size > 0 ? databases : void 0,
1564
+ jobs,
1498
1565
  server: new HonoAdapter(honoApp)
1499
1566
  });
1500
1567
  runner.cleanup = async () => {
@@ -1563,7 +1630,7 @@ async function startCommand(target, options) {
1563
1630
  return runner;
1564
1631
  }
1565
1632
  //#endregion
1566
- //#region src/runner/targets.ts
1633
+ //#region src/spec/targets.ts
1567
1634
  /**
1568
1635
  * Test against an in-process Hono app. The factory receives started services
1569
1636
  * so you can wire connection strings into your app/DI container.
@@ -1613,7 +1680,7 @@ function command(bin) {
1613
1680
  };
1614
1681
  }
1615
1682
  //#endregion
1616
- //#region src/runner/cli.ts
1683
+ //#region src/spec/legacy-cli.ts
1617
1684
  /**
1618
1685
  * Create a CLI specification runner.
1619
1686
  * Runs CLI commands against fixture projects. Optionally starts infrastructure.
@@ -1648,7 +1715,7 @@ async function cli(options) {
1648
1715
  return runner;
1649
1716
  }
1650
1717
  //#endregion
1651
- //#region src/runner/e2e.ts
1718
+ //#region src/spec/legacy-e2e.ts
1652
1719
  /**
1653
1720
  * Create an E2E specification runner.
1654
1721
  * Starts full docker compose stack. App URL and database auto-detected.
@@ -1674,7 +1741,7 @@ async function e2e(options = {}) {
1674
1741
  return runner;
1675
1742
  }
1676
1743
  //#endregion
1677
- //#region src/runner/integration.ts
1744
+ //#region src/spec/legacy-integration.ts
1678
1745
  /**
1679
1746
  * Create an integration specification runner.
1680
1747
  * Starts infra containers via testcontainers, app runs in-process.