@jterrazz/test 6.3.0 → 6.4.2

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.
@@ -591,6 +591,7 @@ var SpecificationBuilder = class {
591
591
  config;
592
592
  fixtures = [];
593
593
  intercepts = [];
594
+ jobName = null;
594
595
  label;
595
596
  mocks = [];
596
597
  projectName = null;
@@ -750,6 +751,18 @@ var SpecificationBuilder = class {
750
751
  return this;
751
752
  }
752
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
+ /**
753
766
  * Execute the specification: run seeds, copy fixtures, then perform the
754
767
  * configured action (HTTP or CLI).
755
768
  *
@@ -761,8 +774,14 @@ var SpecificationBuilder = class {
761
774
  async run() {
762
775
  const hasHttpAction = this.request !== null;
763
776
  const hasCliAction = this.commandArgs !== null || this.spawnConfig !== null;
764
- if (!hasHttpAction && !hasCliAction) throw new Error(`Specification "${this.label}": no action defined. Call .get(), .post(), .exec(), etc. before .run()`);
765
- 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)`);
766
785
  let workDir = null;
767
786
  if (hasCliAction) workDir = this.prepareWorkDir();
768
787
  if (this.config.databases) for (const db of this.config.databases.values()) await db.reset();
@@ -780,11 +799,12 @@ var SpecificationBuilder = class {
780
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 });
781
800
  let cleanupIntercepts = null;
782
801
  if (this.intercepts.length > 0) {
783
- const { registerIntercepts } = await import("./server.js");
802
+ const { registerIntercepts } = await import("./intercept2.js");
784
803
  cleanupIntercepts = await registerIntercepts(this.intercepts);
785
804
  }
786
805
  try {
787
806
  if (hasHttpAction) return await this.runHttpAction();
807
+ if (hasJobAction) return await this.runJobAction();
788
808
  return await this.runCliAction(workDir);
789
809
  } finally {
790
810
  if (cleanupIntercepts) cleanupIntercepts();
@@ -826,6 +846,19 @@ var SpecificationBuilder = class {
826
846
  testDir: this.testDir
827
847
  });
828
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
+ }
829
862
  async runCliAction(workDir) {
830
863
  if (!this.config.command) throw new Error("CLI actions require a command adapter (use cli())");
831
864
  const env = this.resolveEnv(workDir);
@@ -859,7 +892,7 @@ function getCallerDir() {
859
892
  if (!match) continue;
860
893
  const filePath = match[1];
861
894
  if (filePath.includes("node_modules")) continue;
862
- if (filePath.includes("/src/builder/") || filePath.includes("/src/runner/")) continue;
895
+ if (filePath.includes("/src/builder/") || filePath.includes("/src/spec/")) continue;
863
896
  return resolve(filePath, "..");
864
897
  }
865
898
  throw new Error("Cannot detect caller directory from stack trace");
@@ -1051,7 +1084,7 @@ var DockerAssertion = class {
1051
1084
  }
1052
1085
  };
1053
1086
  //#endregion
1054
- //#region src/adapters/compose.adapter.ts
1087
+ //#region src/infra/adapters/compose.adapter.ts
1055
1088
  /**
1056
1089
  * Start the full compose stack and stop it all on cleanup.
1057
1090
  * Supports per-worker project names for parallel execution.
@@ -1098,7 +1131,7 @@ var ComposeStackAdapter = class {
1098
1131
  }
1099
1132
  };
1100
1133
  //#endregion
1101
- //#region src/adapters/testcontainers.adapter.ts
1134
+ //#region src/infra/adapters/testcontainers.adapter.ts
1102
1135
  /**
1103
1136
  * Container adapter using testcontainers.
1104
1137
  * Wraps a GenericContainer for programmatic container lifecycle.
@@ -1158,7 +1191,7 @@ var TestcontainersAdapter = class {
1158
1191
  }
1159
1192
  };
1160
1193
  //#endregion
1161
- //#region src/orchestrator/compose-parser.ts
1194
+ //#region src/infra/compose-parser.ts
1162
1195
  /**
1163
1196
  * Detect the service type from the image name.
1164
1197
  */
@@ -1231,7 +1264,7 @@ function parseComposeFile(filePath) {
1231
1264
  };
1232
1265
  }
1233
1266
  //#endregion
1234
- //#region src/orchestrator/orchestrator.ts
1267
+ //#region src/infra/orchestrator.ts
1235
1268
  /**
1236
1269
  * Orchestrator for test infrastructure.
1237
1270
  * Integration: starts services via testcontainers.
@@ -1440,7 +1473,7 @@ var Orchestrator = class {
1440
1473
  }
1441
1474
  };
1442
1475
  //#endregion
1443
- //#region src/runner/resolve.ts
1476
+ //#region src/spec/resolve.ts
1444
1477
  /**
1445
1478
  * Resolve root — if relative, resolves from the caller's directory.
1446
1479
  */
@@ -1454,7 +1487,7 @@ function resolveProjectRoot(root) {
1454
1487
  const match = line.match(/at\s+(?:.*?\()?(?:file:\/\/)?([^:)]+):\d+:\d+/);
1455
1488
  if (!match) continue;
1456
1489
  const filePath = match[1];
1457
- if (filePath.includes("node_modules") || filePath.includes("/src/runner/")) continue;
1490
+ if (filePath.includes("node_modules") || filePath.includes("/src/spec/")) continue;
1458
1491
  return resolve(filePath, "..", root);
1459
1492
  }
1460
1493
  }
@@ -1472,7 +1505,7 @@ function resolveCommand(command, root) {
1472
1505
  return command;
1473
1506
  }
1474
1507
  //#endregion
1475
- //#region src/runner/spec.ts
1508
+ //#region src/spec/spec.ts
1476
1509
  /**
1477
1510
  * Create a specification runner for the given target.
1478
1511
  *
@@ -1520,12 +1553,16 @@ async function startApp(target, options) {
1520
1553
  const key = svc.composeName ?? svc.type;
1521
1554
  servicesMap[key] = svc;
1522
1555
  }
1523
- const honoApp = target.factory(servicesMap);
1556
+ const factoryResult = target.factory(servicesMap);
1557
+ const isComposite = Array.isArray(factoryResult.jobs);
1558
+ const honoApp = isComposite ? factoryResult.server : factoryResult;
1559
+ const jobs = isComposite ? factoryResult.jobs : void 0;
1524
1560
  const database = orchestrator.getDatabase() ?? void 0;
1525
1561
  const databases = orchestrator.getDatabases();
1526
1562
  const runner = createSpecificationRunner({
1527
1563
  database,
1528
1564
  databases: databases.size > 0 ? databases : void 0,
1565
+ jobs,
1529
1566
  server: new HonoAdapter(honoApp)
1530
1567
  });
1531
1568
  runner.cleanup = async () => {
@@ -1594,7 +1631,7 @@ async function startCommand(target, options) {
1594
1631
  return runner;
1595
1632
  }
1596
1633
  //#endregion
1597
- //#region src/runner/targets.ts
1634
+ //#region src/spec/targets.ts
1598
1635
  /**
1599
1636
  * Test against an in-process Hono app. The factory receives started services
1600
1637
  * so you can wire connection strings into your app/DI container.
@@ -1644,7 +1681,7 @@ function command(bin) {
1644
1681
  };
1645
1682
  }
1646
1683
  //#endregion
1647
- //#region src/runner/cli.ts
1684
+ //#region src/spec/legacy-cli.ts
1648
1685
  /**
1649
1686
  * Create a CLI specification runner.
1650
1687
  * Runs CLI commands against fixture projects. Optionally starts infrastructure.
@@ -1679,7 +1716,7 @@ async function cli(options) {
1679
1716
  return runner;
1680
1717
  }
1681
1718
  //#endregion
1682
- //#region src/runner/e2e.ts
1719
+ //#region src/spec/legacy-e2e.ts
1683
1720
  /**
1684
1721
  * Create an E2E specification runner.
1685
1722
  * Starts full docker compose stack. App URL and database auto-detected.
@@ -1705,7 +1742,7 @@ async function e2e(options = {}) {
1705
1742
  return runner;
1706
1743
  }
1707
1744
  //#endregion
1708
- //#region src/runner/integration.ts
1745
+ //#region src/spec/legacy-integration.ts
1709
1746
  /**
1710
1747
  * Create an integration specification runner.
1711
1748
  * Starts infra containers via testcontainers, app runs in-process.