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