@hublo/sentinel 1.4.0-alpha.3 → 1.4.0-alpha.4

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.
@@ -16,7 +16,7 @@ import {
16
16
  registerAdapters,
17
17
  resolve,
18
18
  resolveBin
19
- } from "../chunk-MUQWAZQN.js";
19
+ } from "../chunk-ASBZQAXV.js";
20
20
  import {
21
21
  WORKSPACE_ROOT_MARKER,
22
22
  ensureWorkspacePrep,
@@ -220,6 +220,14 @@ function moduleScripts(cwd) {
220
220
  return {};
221
221
  }
222
222
  }
223
+ function manifestTargets(cwd) {
224
+ try {
225
+ const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
226
+ return pkg.nx?.targets ?? {};
227
+ } catch {
228
+ return {};
229
+ }
230
+ }
223
231
  function moduleName2(cwd) {
224
232
  try {
225
233
  const pkg = JSON.parse(readFileSync2(join2(cwd, "package.json"), "utf8"));
@@ -6584,12 +6592,47 @@ function applyNamedRules(source, program, fileName = "") {
6584
6592
  const applied = /* @__PURE__ */ new Set();
6585
6593
  const asyncify = /* @__PURE__ */ new Set();
6586
6594
  const mocked = mockedSpecifiers(program);
6587
- const wholeStatement = /* @__PURE__ */ new Set();
6595
+ const wholeStatement = /* @__PURE__ */ new Map();
6588
6596
  walk(program, (node) => {
6589
6597
  if (node.type !== "ExpressionStatement") return;
6590
6598
  const expression = node.expression;
6591
- if (expression !== void 0) wholeStatement.add(expression);
6599
+ if (expression !== void 0) wholeStatement.set(expression, node);
6592
6600
  });
6601
+ const standsAlone = /* @__PURE__ */ new Set();
6602
+ const alone = (node) => {
6603
+ if (node !== void 0) standsAlone.add(node);
6604
+ };
6605
+ walk(program, (node) => {
6606
+ switch (node.type) {
6607
+ case "VariableDeclarator":
6608
+ return alone(node.init);
6609
+ case "AssignmentExpression":
6610
+ return alone(node.right);
6611
+ case "ReturnStatement":
6612
+ case "ExportDefaultDeclaration":
6613
+ return alone(node.argument ?? node.declaration);
6614
+ case "ArrowFunctionExpression":
6615
+ return node.expression === true ? alone(node.body) : void 0;
6616
+ case "ConditionalExpression":
6617
+ alone(node.consequent);
6618
+ return alone(node.alternate);
6619
+ case "ArrayExpression":
6620
+ return (node.elements ?? []).forEach((e) => alone(e ?? void 0));
6621
+ case "Property":
6622
+ case "ObjectProperty":
6623
+ return node.shorthand === true ? void 0 : alone(node.value);
6624
+ case "CallExpression":
6625
+ case "NewExpression":
6626
+ return (node.arguments ?? []).forEach((a) => alone(a));
6627
+ default:
6628
+ return;
6629
+ }
6630
+ });
6631
+ const placed = (node, expression, trailing = "") => {
6632
+ if (trailing !== "") return `(${expression})${trailing}`;
6633
+ if (wholeStatement.has(node)) return expression;
6634
+ return standsAlone.has(node) ? expression : `(${expression})`;
6635
+ };
6593
6636
  const handedToTheRunnerAsAModule = /* @__PURE__ */ new Set();
6594
6637
  walk(program, (node) => {
6595
6638
  const returned = mockFactoryReturn(node);
@@ -6619,7 +6662,7 @@ function applyNamedRules(source, program, fileName = "") {
6619
6662
  edits.push({
6620
6663
  start: node.start,
6621
6664
  end: node.end,
6622
- text: `(await import(${argumentText(source, node)}))`
6665
+ text: placed(node, `await import(${argumentText(source, node)})`)
6623
6666
  });
6624
6667
  applied.add("jest.requireMock");
6625
6668
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6631,7 +6674,7 @@ function applyNamedRules(source, program, fileName = "") {
6631
6674
  edits.push({
6632
6675
  start: node.start,
6633
6676
  end: node.end,
6634
- text: `(await vi.${asyncReplacement}(${argumentText(source, node)}))`
6677
+ text: placed(node, `await vi.${asyncReplacement}(${argumentText(source, node)})`)
6635
6678
  });
6636
6679
  applied.add(`jest.${member}`);
6637
6680
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -6644,7 +6687,7 @@ function applyNamedRules(source, program, fileName = "") {
6644
6687
  edits.push({
6645
6688
  start: node.start,
6646
6689
  end: node.end,
6647
- text: wholeStatement.has(node) ? `await import(${argumentText(source, node)});` : `(await import(${argumentText(source, node)}))${interop}`
6690
+ text: placed(node, `await import(${argumentText(source, node)})`, interop)
6648
6691
  });
6649
6692
  applied.add("require");
6650
6693
  if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
@@ -7164,11 +7207,6 @@ ${text}` });
7164
7207
  import { parseSync as parseSync6 } from "oxc-parser";
7165
7208
  var SENTINEL_MSW_SERVER = "@hublo/sentinel/test/msw";
7166
7209
  var MSW_SERVER_MODULE = /(^|\/)msw\/server$/;
7167
- var MSW_PACKAGE = /^msw$/;
7168
- function importsMswPackage(declaration) {
7169
- const source = declaration.source?.value;
7170
- return source !== void 0 && MSW_PACKAGE.test(source);
7171
- }
7172
7210
  function importsServer(declaration) {
7173
7211
  return (declaration.specifiers ?? []).some((specifier) => {
7174
7212
  const imported = specifier.imported;
@@ -7184,8 +7222,7 @@ function repointMswServer(fileName, source) {
7184
7222
  if (node.type !== "ImportDeclaration") return;
7185
7223
  const specifier = node.source;
7186
7224
  if (specifier?.value === void 0) return;
7187
- const isServerModule = MSW_SERVER_MODULE.test(specifier.value) && importsServer(node);
7188
- if (!isServerModule && !importsMswPackage(node)) return;
7225
+ if (!MSW_SERVER_MODULE.test(specifier.value) || !importsServer(node)) return;
7189
7226
  edits.push({ start: specifier.start, end: specifier.end });
7190
7227
  });
7191
7228
  if (edits.length === 0) return { source, repointed: false };
@@ -7580,10 +7617,9 @@ var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
7580
7617
  function flavourOf(reading) {
7581
7618
  return reading.testEnvironment === "jsdom" ? "react" : "nest";
7582
7619
  }
7583
- var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/nest";
7620
+ var SENTINEL_TEST_SETUP = "@hublo/sentinel/test/setup/jest-parity";
7584
7621
  var SENTINEL_MSW_SETUP = "@hublo/sentinel/test/setup/msw-lifecycle";
7585
7622
  var SENTINEL_WORKSPACE_SETUP = "@hublo/sentinel/test/setup/workspace";
7586
- var NEST_TEST_CONFIG_FILE = "vitest.config.mts";
7587
7623
  function hopsToRoot(cwd, workspaceRoot) {
7588
7624
  const up = relative6(cwd, workspaceRoot).replaceAll("\\", "/");
7589
7625
  return up === "" ? "." : up;
@@ -7719,7 +7755,8 @@ function aliasBlock(reading) {
7719
7755
  if (reading.aliases === void 0 || reading.aliases.length === 0) return "";
7720
7756
  const entries = reading.aliases.map((alias) => {
7721
7757
  const pattern = alias.find.replaceAll(String.raw`\\`, "\\").replaceAll("/", String.raw`\/`);
7722
- const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
7758
+ const deadToken = /<rootdir>/i.test(alias.replacement) && !alias.replacement.includes("<rootDir>");
7759
+ const target = isPackage(alias.replacement) ? `'${alias.replacement}'` : deadToken ? `'${alias.replacement}'` : `path.resolve(import.meta.dirname, '${withoutRootDir(alias.replacement)}')`;
7723
7760
  return ` { find: /${pattern}/, replacement: ${target} },`;
7724
7761
  });
7725
7762
  return [" resolve: {", " alias: [", ...entries, " ],", " },"].join("\n");
@@ -8195,8 +8232,9 @@ function planJestMigration(context, adoption) {
8195
8232
  `${migration.blockers.length} thing(s) in this module's test files need a human before it can move. Nothing was written, because a config without its files is a suite calling jest.* under Vitest.${listed}`
8196
8233
  );
8197
8234
  }
8198
- const targets = projectTargets(context.cwd) ?? {};
8199
- const existingFor = (name) => targets[name];
8235
+ const fromProject = projectTargets(context.cwd) ?? {};
8236
+ const fromManifest = manifestTargets(context.cwd);
8237
+ const existingFor = (name) => fromProject[name] ?? fromManifest[name];
8200
8238
  const siblings = siblingScriptRewrites(context.cwd);
8201
8239
  const specTypes = specTypesOperation(context.cwd);
8202
8240
  const scripts = Object.fromEntries(
@@ -8230,9 +8268,10 @@ function planJestMigration(context, adoption) {
8230
8268
  (name) => ({ kind: "delete", path: name })
8231
8269
  )
8232
8270
  ];
8271
+ const escaping = (prepared[0]?.reading.aliases ?? []).map((alias) => alias.replacement).filter((replacement) => replacement.includes(".."));
8233
8272
  const notes = [
8234
8273
  `${migration.files} test file(s) migrated from jest to Vitest, ${migration.renamed} site(s) rewritten${migration.hoisted > 0 ? `, ${migration.hoisted} of them moving a mock factory's captures into vi.hoisted` : ""}.`,
8235
- `${NEST_TEST_CONFIG_FILE} written from what ${adoption.jestConfigs.join(", ")} declared, and that config deleted. Every specifier in the new one is a package name: nothing in it reaches outside this module.`
8274
+ `${prepared.map(({ mode }) => mode.vitestConfig).join(", ")} written from what ${prepared.map(({ mode }) => mode.jestConfig).join(", ")} declared, and that config deleted.` + (escaping.length === 0 ? ` Every specifier in the new one is a package name: nothing in it reaches outside this module.` : ` \u26A0\uFE0F ${escaping.length} alias(es) carried from that config reach OUTSIDE this module: ${escaping.slice(0, 3).join(", ")}. They came from its \`moduleNameMapper\` and are kept as they were, but a module that reaches out cannot be moved on its own.`)
8236
8275
  ];
8237
8276
  if (specTypes !== void 0) {
8238
8277
  notes.push(
@@ -8240,7 +8279,7 @@ function planJestMigration(context, adoption) {
8240
8279
  );
8241
8280
  }
8242
8281
  notes.push(
8243
- `run this module's own lint --fix (or its formatter) after installing: the Vitest type imports are written beside the existing ones, and ordering belongs to the tool that enforces it.`
8282
+ `run this module's own formatter (or lint --fix) on what this wrote, after installing. Two things need it: the Vitest type imports are written beside the existing ones, and a rewritten call is often SHORTER than the one it replaced, so the line breaks around it are no longer the ones your formatter would pick. Neither is a defect, and both fail a format check. Layout is a repository setting, so sentinel leaves it to the tool that owns it.`
8244
8283
  );
8245
8284
  notes.push(
8246
8285
  `then run this module's TYPECHECK, not only its tests. The migration rewrites types as well as calls, and a wrong type is invisible to a green suite: measured on a real module, \`--run --test\` passed while \`--run --typescript\` reported ten errors it had not had before.`
@@ -8330,7 +8369,7 @@ function plan3(context) {
8330
8369
  `${configFile} now imports ${rewrite.moved.join(", ")} from sentinel. Only the import lines changed: every alias, setup file, timeout and coverage path is untouched, so this suite runs what it ran before.`
8331
8370
  );
8332
8371
  }
8333
- const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME];
8372
+ const existing = projectTargets(context.cwd)?.[TEST_SCRIPT_NAME] ?? manifestTargets(context.cwd)[TEST_SCRIPT_NAME];
8334
8373
  const outputs = existing?.outputs ?? [];
8335
8374
  const dependsOn = existing?.dependsOn ?? [];
8336
8375
  const configurations = existing?.configurations ?? {};
@@ -8387,9 +8426,10 @@ function plan3(context) {
8387
8426
 
8388
8427
  // src/roles/test/reference.ts
8389
8428
  import { execFileSync } from "child_process";
8390
- import { existsSync as existsSync30, mkdtempSync, readFileSync as readFileSync29, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
8429
+ import { createHash } from "crypto";
8430
+ import { existsSync as existsSync30, mkdirSync, mkdtempSync, readFileSync as readFileSync29, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
8391
8431
  import { tmpdir } from "os";
8392
- import { join as join36 } from "path";
8432
+ import { join as join36, resolve as resolve5 } from "path";
8393
8433
 
8394
8434
  // src/roles/test/baseline.ts
8395
8435
  function readSnapshot(report) {
@@ -8590,8 +8630,19 @@ function unusableAsBaseline(snapshot) {
8590
8630
  }
8591
8631
  return void 0;
8592
8632
  }
8633
+ function renamedExample(before, after) {
8634
+ let shared = 0;
8635
+ while (shared < before.length && shared < after.length && before[shared] === after[shared]) {
8636
+ shared += 1;
8637
+ }
8638
+ const boundary = before.lastIndexOf(" ", shared);
8639
+ const from = boundary > 0 ? boundary + 1 : 0;
8640
+ const ellipsis = from > 0 ? "\u2026" : "";
8641
+ return `"${ellipsis}${before.slice(from)}" -> "${ellipsis}${after.slice(from)}"`;
8642
+ }
8593
8643
  function describeComparison(comparison) {
8594
- const renamedNote = comparison.renamed.length > 0 ? ` ${comparison.renamed.length} test(s) kept their identity and changed name, because the runners render an each title's interpolated value differently (e.g. "${comparison.renamed[0]?.before.slice(-40)}" -> "${comparison.renamed[0]?.after.slice(-40)}").` : "";
8644
+ const firstRename = comparison.renamed[0];
8645
+ const renamedNote = firstRename === void 0 ? "" : ` ${comparison.renamed.length} test(s) kept their identity and changed name, because the runners render an each title's interpolated value differently (e.g. ${renamedExample(firstRename.before, firstRename.after)}).`;
8595
8646
  const alreadyRedNote = comparison.alreadyFailing > 0 ? ` ${comparison.alreadyFailing} test(s) were already failing before the migration and are outside this comparison, which covers only what passed.` : "";
8596
8647
  const recoveredNote = comparison.recovered.length > 0 ? ` ${comparison.recovered.length} test(s) were failing before and pass now.` : "";
8597
8648
  if (comparison.identical) {
@@ -8616,11 +8667,14 @@ function describeComparison(comparison) {
8616
8667
  `${comparison.regressed.length} test(s) passed before and do not now: ${comparison.regressed.slice(0, 3).join(", ")}`
8617
8668
  );
8618
8669
  }
8619
- return `${parts.join("; ")}${renamedNote}${alreadyRedNote}`;
8670
+ return `${parts.join("; ")}.${renamedNote}${alreadyRedNote}`;
8620
8671
  }
8621
8672
 
8622
8673
  // src/roles/test/reference.ts
8623
- var REFERENCE_FILE = ".sentinel-test-reference.json";
8674
+ function referencePath(cwd) {
8675
+ const key = createHash("sha256").update(resolve5(cwd)).digest("hex").slice(0, 16);
8676
+ return join36(tmpdir(), "sentinel-test-reference", `${key}.json`);
8677
+ }
8624
8678
  function runJest(cwd, config) {
8625
8679
  const bin = resolveBin(cwd, "jest");
8626
8680
  if (bin === void 0) return void 0;
@@ -8663,7 +8717,9 @@ function captureReference(cwd, config) {
8663
8717
  takenAt: (/* @__PURE__ */ new Date()).toISOString(),
8664
8718
  snapshot
8665
8719
  };
8666
- writeFileSync3(join36(cwd, REFERENCE_FILE), `${JSON.stringify(stored, void 0, 2)}
8720
+ const path = referencePath(cwd);
8721
+ mkdirSync(join36(path, ".."), { recursive: true });
8722
+ writeFileSync3(path, `${JSON.stringify(stored, void 0, 2)}
8667
8723
  `);
8668
8724
  return {
8669
8725
  captured: true,
@@ -8671,10 +8727,10 @@ function captureReference(cwd, config) {
8671
8727
  };
8672
8728
  }
8673
8729
  function hasReference(cwd) {
8674
- return existsSync30(join36(cwd, REFERENCE_FILE));
8730
+ return existsSync30(referencePath(cwd));
8675
8731
  }
8676
8732
  function compareAgainstReference(cwd, report) {
8677
- const path = join36(cwd, REFERENCE_FILE);
8733
+ const path = referencePath(cwd);
8678
8734
  let stored;
8679
8735
  try {
8680
8736
  stored = JSON.parse(readFileSync29(path, "utf8"));
@@ -8683,10 +8739,11 @@ function compareAgainstReference(cwd, report) {
8683
8739
  return { ok: true, message: `the recorded reference could not be read, so nothing was proved.` };
8684
8740
  }
8685
8741
  const comparison = compareSnapshots(stored.snapshot, readSnapshot(report));
8686
- rmSync3(path, { force: true });
8742
+ if (comparison.identical) rmSync3(path, { force: true });
8743
+ const kept = comparison.identical ? "" : ` The reference is KEPT until a run passes it, so this does not go green by running again. Fix the suite and re-run, or delete ${path} to abandon the proof on purpose.`;
8687
8744
  return {
8688
8745
  ok: comparison.identical,
8689
- message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})`
8746
+ message: `${describeComparison(comparison)} (reference: ${stored.config}, ${stored.takenAt})${kept}`
8690
8747
  };
8691
8748
  }
8692
8749
 
@@ -9552,7 +9609,7 @@ function replaceLines(current, replacements) {
9552
9609
 
9553
9610
  // src/core/apply-plan.ts
9554
9611
  import { existsSync as existsSync32, readFileSync as readFileSync33, renameSync, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "fs";
9555
- import { resolve as resolve5, sep } from "path";
9612
+ import { resolve as resolve6, sep } from "path";
9556
9613
  import { applyEdits as applyEdits3, findNodeAtLocation, modify, parseTree } from "jsonc-parser";
9557
9614
 
9558
9615
  // src/shared/deep-merge.ts
@@ -9562,8 +9619,8 @@ function isPlainObject(value) {
9562
9619
 
9563
9620
  // src/core/apply-plan.ts
9564
9621
  function resolveWithinRoot(cwd, relativePath) {
9565
- const root = resolve5(cwd);
9566
- const absolutePath = resolve5(root, relativePath);
9622
+ const root = resolve6(cwd);
9623
+ const absolutePath = resolve6(root, relativePath);
9567
9624
  if (absolutePath !== root && !absolutePath.startsWith(root + sep)) {
9568
9625
  throw new Error(`Refusing to write outside the module root: "${relativePath}".`);
9569
9626
  }
@@ -9961,4 +10018,4 @@ export {
9961
10018
  detectFramework,
9962
10019
  dispatch
9963
10020
  };
9964
- //# sourceMappingURL=chunk-MUQWAZQN.js.map
10021
+ //# sourceMappingURL=chunk-ASBZQAXV.js.map
@@ -3,6 +3,12 @@ import {
3
3
  tsconfigAliases
4
4
  } from "./chunk-3TDUIKVQ.js";
5
5
 
6
+ // src/roles/test/shared-test-config.ts
7
+ import { existsSync } from "fs";
8
+ import { createRequire } from "module";
9
+ import { dirname, join } from "path";
10
+ import { fileURLToPath } from "url";
11
+
6
12
  // src/roles/test/react/jest-export-conditions.ts
7
13
  var ABSENT_UNDER_JEST = /* @__PURE__ */ new Set(["development", "development|production"]);
8
14
  function stripInPlace(conditions) {
@@ -25,17 +31,21 @@ function jestExportConditions() {
25
31
  }
26
32
 
27
33
  // src/roles/test/shared-test-config.ts
28
- import { existsSync } from "fs";
29
- import { createRequire } from "module";
30
- import { dirname, join } from "path";
31
- import { fileURLToPath } from "url";
32
34
  function siblingFile(relative) {
33
- try {
34
- const sibling = fileURLToPath(new URL(relative, import.meta.url));
35
- return existsSync(sibling) ? sibling : void 0;
36
- } catch {
37
- return void 0;
35
+ const candidates = [
36
+ relative,
37
+ `roles/test/${relative.replace(/^\.\//, "")}`,
38
+ `../${relative.replace(/^\.\//, "")}`
39
+ ];
40
+ for (const candidate of candidates) {
41
+ try {
42
+ const sibling = fileURLToPath(new URL(candidate, import.meta.url));
43
+ if (existsSync(sibling)) return sibling;
44
+ } catch {
45
+ continue;
46
+ }
38
47
  }
48
+ return void 0;
39
49
  }
40
50
  function resolveFrom(specifier) {
41
51
  try {
@@ -49,7 +59,7 @@ function resolveFrom(specifier) {
49
59
  }
50
60
  }
51
61
  function mockExtendedAdapterPath() {
52
- return siblingFile("../setup/mock-extended.js");
62
+ return siblingFile("./setup/mock-extended.js");
53
63
  }
54
64
  function vitestMockExtendedPath() {
55
65
  try {
@@ -63,7 +73,7 @@ function vitestMockExtendedPath() {
63
73
  }
64
74
  }
65
75
  function mswAliases() {
66
- const server = siblingFile("../setup/msw-server.js");
76
+ const server = siblingFile("./setup/msw-server.js");
67
77
  const own = resolveFrom("msw");
68
78
  return [
69
79
  ...own === void 0 ? [] : [{ find: /^msw$/, replacement: own }],
@@ -293,7 +303,23 @@ function sharedTestConfig(options) {
293
303
  resolve: {
294
304
  ...base.resolve,
295
305
  ...options.overrides.resolve,
296
- // The module's own aliases come AFTER sentinel's, so a module that needs to win can.
306
+ /*
307
+ * The module's own aliases come after sentinel's, and in a Vite alias ARRAY the FIRST match
308
+ * wins. So sentinel's win, which is the opposite of what this comment used to claim.
309
+ *
310
+ * ⚠️ The order is corrected here rather than in the code, because changing it would flip a
311
+ * behaviour that nothing exercises. Every alias sentinel adds is ANCHORED to one exact
312
+ * specifier: `^axios$`, `^luxon$`, `^msw$`, `^jest-mock-extended$` and
313
+ * `^@prisma/<x>/runtime/library$`. A module's own entry collides only by naming the identical
314
+ * string, and measured across every `jest.config*` in the repo, none does: the closest are
315
+ * `@front/type/axios` and `@front/api/msw-handlers` in `front-legacy`, different specifiers
316
+ * in the one module this role refuses anyway.
317
+ *
318
+ * So today the order decides nothing, and all 97 migrations were measured with it this way
319
+ * round. Reversing it on a hypothesis would be a silent behaviour change bought with nothing.
320
+ * What was actually wrong was a comment promising a module it could win, which a reader would
321
+ * have relied on.
322
+ */
297
323
  alias: [
298
324
  ...asAliasArray(base.resolve?.alias),
299
325
  ...asAliasArray(options.overrides.resolve?.alias)
@@ -307,4 +333,4 @@ export {
307
333
  jestExportConditions,
308
334
  sharedTestConfig
309
335
  };
310
- //# sourceMappingURL=chunk-SX5CBE3Z.js.map
336
+ //# sourceMappingURL=chunk-MT7VQXRA.js.map
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import {
7
7
  registerAdapters,
8
8
  resolve,
9
9
  setDefaultRunner
10
- } from "./chunk-MUQWAZQN.js";
10
+ } from "./chunk-ASBZQAXV.js";
11
11
  import "./chunk-WLFE5RUU.js";
12
12
  export {
13
13
  BaseAdapter,
@@ -1,7 +1,7 @@
1
1
  import { ViteUserConfig } from 'vitest/config';
2
2
  export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
3
3
  export { loadEnv } from 'vite';
4
- import { S as SharedTestOptions } from '../../../shared-test-config-CGxvMlmk.js';
4
+ import { SharedTestOptions } from '../shared-test-config.js';
5
5
  export { A as Alias, D as DecoratorMetadataOptions, d as decoratorMetadata, t as tsconfigAliases } from '../../../tsconfig-aliases-Ce6axdJ4.js';
6
6
 
7
7
  declare function nestTestConfig(options: SharedTestOptions): ViteUserConfig;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  sharedTestConfig
3
- } from "../../../chunk-SX5CBE3Z.js";
3
+ } from "../../../chunk-MT7VQXRA.js";
4
4
  import {
5
5
  decoratorMetadata,
6
6
  tsconfigAliases
@@ -2,7 +2,7 @@ import { ViteUserConfig } from 'vitest/config';
2
2
  export { ConfigEnv, TestUserConfig, ViteUserConfig, defineConfig, mergeConfig } from 'vitest/config';
3
3
  import { Plugin } from 'vite';
4
4
  export { loadEnv } from 'vite';
5
- import { S as SharedTestOptions } from '../../../shared-test-config-CGxvMlmk.js';
5
+ import { SharedTestOptions } from '../shared-test-config.js';
6
6
 
7
7
  /**
8
8
  * Resolve packages the way jest did, for a suite that was written against jest's resolution.
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  jestExportConditions,
3
3
  sharedTestConfig
4
- } from "../../../chunk-SX5CBE3Z.js";
4
+ } from "../../../chunk-MT7VQXRA.js";
5
5
  import "../../../chunk-3TDUIKVQ.js";
6
6
 
7
7
  // src/roles/test/react/toolchain.ts
@@ -7,7 +7,7 @@ import {
7
7
  } from "../../../chunk-2XLX6PFR.js";
8
8
  import "../../../chunk-WLFE5RUU.js";
9
9
 
10
- // src/roles/test/setup/nest.ts
10
+ // src/roles/test/setup/jest-parity.ts
11
11
  import { expect, vi } from "vitest";
12
12
 
13
13
  // src/roles/test/setup/constructor-semantics.ts
@@ -147,7 +147,7 @@ function installJestRejectedFunction() {
147
147
  }
148
148
  }
149
149
 
150
- // src/roles/test/setup/nest.ts
150
+ // src/roles/test/setup/jest-parity.ts
151
151
  installJestErrorEquality(expect);
152
152
  installJestMockReset(vi);
153
153
  installJestConstructorSemantics(vi);
@@ -156,4 +156,4 @@ installJestFakeTimerOptions(vi);
156
156
  installJestRejectedFunction();
157
157
  installJestGlobal(vi);
158
158
  await installWorkspaceSetup();
159
- //# sourceMappingURL=nest.js.map
159
+ //# sourceMappingURL=jest-parity.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/jest-parity.ts","../../../../src/roles/test/setup/constructor-semantics.ts","../../../../src/roles/test/setup/deep-mock-reset.ts","../../../../src/roles/test/setup/error-equality.ts","../../../../src/roles/test/setup/fake-timers.ts","../../../../src/roles/test/setup/jest-global.ts","../../../../src/roles/test/setup/mock-reset.ts","../../../../src/roles/test/setup/rejected-function.ts"],"sourcesContent":["/**\n * The setup any migrated suite runs before its tests, as ONE entry point.\n *\n * Referenced by name from a generated config, never by a relative path:\n * `setupFiles: ['@hublo/sentinel/test/setup/jest-parity']`. Measured: Vitest resolves a bare\n * package specifier there, so nothing has to know where sentinel sits relative to the module.\n *\n * ⚠️ This was exported as `test/setup/nest` until 23/09, which was wrong in the one place it is\n * read. Nothing below is Nest-specific: every line restores a `jest.*` semantic under Vitest, plus\n * the `.env` cascade. But the name is COMMITTED into each adopting module's config, so a React\n * module ended up with `setupFiles: ['@hublo/sentinel/test/setup/nest', './jest.setup.js']` sitting\n * in its repository, which reads like the bug where a module was handed the wrong family's preset.\n * A reviewer cannot tell that apart from the real thing without opening our source. The name says\n * what the file does instead: parity with jest.\n *\n * ## What it replaces, line for line\n *\n * The repo's root `jest.setup.after.env.js`, loaded by 99 of the 111 jest configs:\n *\n * require('dotenv-flow').config({ silent: true, purge_dotenv: true })\n * const { Settings } = require('luxon')\n * const { server } = require('./libs/nest/tests/src/msw/server')\n * jest.mock('dynamoose')\n * jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')\n * beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))\n * afterEach(() => { if (global.gc) global.gc() })\n * afterAll(() => server.close())\n * Settings.defaultZone = 'utc'\n *\n * Everything here is a transcription of that, not an improvement on it. Where the original made a\n * choice that looks questionable, the choice is carried across and reported, because a migration\n * that also changes behaviour cannot be verified against its own baseline.\n *\n * ONE line is not transcribed: `server.listen()` also runs when this file is evaluated, not only\n * inside `beforeAll`. The runners differ on when a module's exports can still be patched, so the\n * original placement silently disabled interception under Vitest and let tests reach the real\n * internet. `./msw.js` carries the measurement.\n *\n * ## Two halves, and why one of them is loaded defensively\n *\n * The msw lifecycle used to live here and now has its own file, `./msw-lifecycle.js`, added to a\n * module's `setupFiles` only when that module actually uses msw. Installing it everywhere patches\n * `http`/`https` and refuses unmatched requests for modules that never asked: measured on\n * `libs/nest/starter`, 11 of 11 became 10 of 11 with `TypeError: Invalid URL` in the interceptor,\n * on a test fetching the Fastify server the suite starts itself.\n *\n * `dotenv-flow` and luxon's zone react to the REPO, not to the runner: they would read the same\n * under jest, Vitest or `node:test`. So `./workspace.js` loads them only if they are installed and\n * skips them in silence otherwise. That defensiveness is not a precaution bolted on, it IS the\n * statement that these belong to whoever installed them. Elsewhere, sentinel installs and that half\n * does nothing.\n *\n * `dotenv-flow` cannot simply be dropped in favour of Vite's own `.env` handling, which was the\n * first thing checked. Measured on Vitest 4: it does read the cascade and sets `MODE=test`, but it\n * exposes only `VITE_`-prefixed values, and only on `import.meta.env`. `process.env` is left\n * untouched, and Nest code reads unprefixed `process.env`.\n *\n * ## What is NOT here\n *\n * The two `jest.mock` calls. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it; hoisting it into a shared setup is what makes a test pass for a reason nobody\n * can see. The migration reports them so the module relying on one declares it.\n *\n * And `Settings.defaultZone = 'utc'` is carried as luxon's own setting, never translated to\n * `process.env.TZ`. One configures luxon, the other the whole process, `Date` and `Intl` included.\n * Swapping them would change what the suite does while claiming to migrate it, and timezone is not\n * a detail: measured on `host-admin`, a machine's zone accounted for a large part of 345 local\n * failures that did not exist in CI.\n */\nimport { expect, vi } from 'vitest'\n\nimport { installJestConstructorSemantics } from './constructor-semantics.js'\nimport { installDeepMockReset } from './deep-mock-reset.js'\nimport { installJestErrorEquality } from './error-equality.js'\nimport { installJestFakeTimerOptions } from './fake-timers.js'\nimport { installJestGlobal } from './jest-global.js'\nimport { installJestMockReset } from './mock-reset.js'\nimport { installJestRejectedFunction } from './rejected-function.js'\nimport { installWorkspaceSetup } from './workspace.js'\n\ninstallJestErrorEquality(expect)\ninstallJestMockReset(vi)\ninstallJestConstructorSemantics(vi)\ninstallDeepMockReset(vi)\ninstallJestFakeTimerOptions(vi)\ninstallJestRejectedFunction()\ninstallJestGlobal(vi)\n\n/*\n * The `.env` cascade, unconditionally: a suite reading `process.env` needs it whatever setup its\n * jest config named, because Vitest's workers do not inherit what a `globalSetup` set in the main\n * process. Awaited at the top level, so it is loaded before the first test file is imported: a\n * module read at import time would already have captured an unset variable.\n *\n * ⚠️ What is NOT here any more is luxon's UTC zone. That was a decision ONE file at the workspace\n * root made, and only the 97 modules naming it ever had it; it lives in\n * `@hublo/sentinel/test/setup/workspace`, which the generated config adds only for those.\n */\nawait installWorkspaceSetup()\n","/**\n * `new` on a mock, the way jest answered it.\n *\n * ## The divergence, measured on the same source under both runners\n *\n * jest vitest\n * mockImplementation(() => obj), then new the obj TypeError: not a constructor\n * mockImplementation(function(){}), new works works\n * mockReturnValue(obj), then new the obj TypeError, with an explanation\n *\n * jest calls the implementation as a PLAIN function when the mock is constructed and hands back\n * what it returned. Vitest applies real `new` semantics, and an arrow function has no [[Construct]]\n * slot, so it throws.\n *\n * The pattern this breaks is the ordinary way to stand in for a class: automock the module, then\n * say what `new` should give back.\n *\n * jest.mock('@hublo/cloud/event-scheduler-sdk')\n * ;(EventSchedulerSDK as jest.MockedClass<typeof EventSchedulerSDK>)\n * .mockImplementation(() => mockEventSchedulerSDK)\n *\n * Measured across the nest and cloud campaigns: 2 modules, 15 tests.\n * `apps/nest/microservices/client-management` (9, an SDK) and `apps/nest/microservices/hublo-pool`\n * (6, a mocked `Date`).\n *\n * ## Why this is safe to do for everybody, which is the part that matters\n *\n * It only changes a case that THROWS today. An implementation without a `prototype` cannot be\n * constructed at all under Vitest, so no suite anywhere can be relying on what it does: the only\n * behaviours available are \"throws\" and \"answers like jest\". Nothing that works today changes\n * shape, and a constructable implementation is passed through untouched.\n *\n * That bound is asserted by the tests, not just claimed here.\n *\n * ## `mockReturnValue(obj)` then `new`, which this used to leave alone\n *\n * It was left out on the grounds that answering it would decide \"a return value and a constructed\n * instance are the same thing\", a claim about the suite. That reading was wrong on both halves.\n *\n * It is not a claim about the suite, because jest's answer is not ambiguous: it hands back the\n * value, exactly as it does for `mockImplementation(() => obj)`, which this file already restores.\n * Treating the two differently would be the arbitrary choice.\n *\n * And \"no module in this repo hit it\" stopped being true the moment the front family was measured.\n * `libs/front/components` mocks Google Maps the ordinary way and constructs it:\n *\n * AutocompleteService: jest.fn().mockReturnValue({ getPlacePredictions: jest.fn() })\n * // and the hook: new window.google.maps.places.AutocompleteService()\n *\n * 7 tests, all with the same TypeError. The safety bound is unchanged and it is the whole reason\n * this is allowed: Vitest THROWS on that call today, so no suite anywhere can depend on what it\n * does, and the only behaviours available are \"throws\" and \"answers like jest\".\n *\n * Expressed by routing the value through the implementation, rather than by a second mechanism:\n * `mockReturnValue(v)` IS `mockImplementation(() => v)`, so it goes through the same wrapper and\n * `new` works for the same reason.\n */\n\n/** The mocking surface this touches. Vitest's own type is not needed to say it. */\ninterface MockLike {\n mockImplementation(implementation: (...args: unknown[]) => unknown): unknown\n mockImplementationOnce(implementation: (...args: unknown[]) => unknown): unknown\n mockReturnValue(value: unknown): unknown\n mockReturnValueOnce(value: unknown): unknown\n}\n\ninterface ViLike {\n fn(implementation?: (...args: unknown[]) => unknown): MockLike\n /**\n * ⚠️ The REST, not `(target, key)`.\n *\n * Vitest's third argument is the access type, `'get'` or `'set'`, and the first version of the\n * wrapper below forwarded two arguments and dropped it. `vi.spyOn(el, 'scrollWidth', 'get')`\n * then became a spy on the VALUE of an accessor that only exists on a prototype, and jsdom\n * answered `'get scrollWidth' called on an object that is not a valid instance of Element`.\n *\n * Measured on `libs/front/components`, 2 tests, and invisible to every nest module because none\n * of them spies on a DOM accessor.\n */\n spyOn(target: object, ...rest: unknown[]): MockLike\n}\n\n/**\n * Can this function be used with `new`?\n *\n * Asked of the `prototype` property rather than of the source text: an arrow function, a shorthand\n * method and a bound function all lack it, and all three are exactly the cases that throw. A\n * class and a plain `function` have it.\n */\nfunction constructable(value: unknown): boolean {\n return (\n typeof value === 'function' && Object.getOwnPropertyDescriptor(value, 'prototype') !== undefined\n )\n}\n\n/**\n * The same implementation, reachable through `new`.\n *\n * A plain `function` that forwards the call and RETURNS the result. JavaScript's own `new` then\n * hands that object back, which is what jest did, so nothing here imitates jest by hand: it\n * restores the one property the arrow was missing and lets the language do the rest.\n */\nfunction asConstructable(\n implementation: (...args: unknown[]) => unknown,\n): (...args: unknown[]) => unknown {\n if (constructable(implementation)) return implementation\n\n return function forwarded(this: unknown, ...args: unknown[]): unknown {\n return implementation.apply(this, args)\n }\n}\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so every mock they produce accepts `new` the way jest's did.\n *\n * Wrapped at the factory, like `installJestMockReset`, because the behaviour belongs to every mock\n * a suite makes and a suite should not have to ask for it.\n */\nexport function installJestConstructorSemantics(vi: ViLike): void {\n const patch = (mock: MockLike): MockLike => {\n const { mockImplementation, mockImplementationOnce } = mock\n\n mock.mockImplementation = function (implementation) {\n return mockImplementation.call(this, asConstructable(implementation))\n }\n mock.mockImplementationOnce = function (implementation) {\n return mockImplementationOnce.call(this, asConstructable(implementation))\n }\n\n /*\n * Routed through the implementation rather than given a mechanism of its own: the two are the\n * same statement, and one of them already accepts `new`.\n */\n mock.mockReturnValue = function (value) {\n return this.mockImplementation(() => value)\n }\n mock.mockReturnValueOnce = function (value) {\n return this.mockImplementationOnce(() => value)\n }\n return mock\n }\n\n const { fn, spyOn } = vi\n\n vi.fn = function (implementation) {\n return patch(fn.call(this, implementation && asConstructable(implementation)))\n }\n vi.spyOn = function (target, ...rest) {\n return patch(spyOn.call(this, target, ...rest))\n }\n}\n","/**\n * `vi.resetAllMocks()` reaching the deep mocks, the way jest's registry did.\n *\n * ## The divergence, and why it is invisible\n *\n * jest built `jest-mock-extended`'s mocks with `jest.fn()`, so they sat in jest's own registry and\n * `jest.resetAllMocks()` cleared them with everything else. `vitest-mock-extended` builds them its\n * own way, so `vi.resetAllMocks()` walks past them and their call history survives into the next\n * test.\n *\n * Nothing announces it. The suite still runs, and an assertion fails several tests later with a\n * count that is off by exactly what its neighbour did.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does what jest expected:\n *\n * beforeEach(() => mocked.findEvents.mockResolvedValue([]))\n * afterEach(() => vi.resetAllMocks())\n *\n * Eleven tests asserting `toHaveBeenCalledTimes(0)` saw the call left by the one before them. Each\n * PASSES on its own and fails as soon as its neighbour runs first, which is the signature of\n * leakage rather than of a wrong assertion.\n *\n * ## What this installs, and what it leaves alone\n *\n * `resetAllMocks` and `clearAllMocks` do what they did, then extend to the deep mocks: `reset`\n * drops implementations as well as calls, `clear` drops only calls, which is the same distinction\n * the two names already carry.\n *\n * `restoreAllMocks` is NOT extended. It restores spies to their originals, and a deep mock has no\n * original to go back to: it was invented. Extending it would mean deciding what \"restore\" means\n * for something that never existed, which is a claim, not a translation.\n */\nimport { clearDeepMocks, resetDeepMocks } from './mock-extended.js'\n\n/** The part of `vi` this touches. Vitest's own type is not needed to say it. */\ninterface ViLike {\n resetAllMocks(): unknown\n clearAllMocks(): unknown\n}\n\nexport function installDeepMockReset(vi: ViLike): void {\n const { resetAllMocks, clearAllMocks } = vi\n\n vi.resetAllMocks = function extended(this: unknown): unknown {\n const answer = resetAllMocks.call(this)\n resetDeepMocks()\n return answer\n }\n\n vi.clearAllMocks = function extended(this: unknown): unknown {\n const answer = clearAllMocks.call(this)\n clearDeepMocks()\n return answer\n }\n}\n","/**\n * How two `Error` values compare, which the two runners disagree about.\n *\n * A suite that asserts on a thrown or captured error usually writes the error it expects by hand:\n *\n * expect(save).toHaveBeenCalledWith({ error: new AxiosError('Request failed with status code 500'), ... })\n *\n * Under jest that passes whatever else the real error carries. Under Vitest it fails, and the\n * report is 6600 lines of an axios error's `config`, `request` and `response`, which reads like a\n * broken test rather than a runner difference.\n *\n * ## What each runner actually does, measured on the same four cases\n *\n * | two errors | jest 29 | Vitest 4 |\n * | --------------------------------- | -------- | ----------- |\n * | same message, same type | equal | equal |\n * | same message, DIFFERENT types | equal | not equal |\n * | same message, extra properties | equal | not equal |\n * | different messages | not equal| not equal |\n *\n * jest compares errors by their MESSAGE and nothing else: a `TypeError` and a `RangeError` with the\n * same text are equal to it. Vitest compares the type and the own properties too.\n *\n * ## Why the looser rule is the one restored\n *\n * Because it is the one 3481 test files were written against. Tightening it here would turn green\n * tests red during a migration whose whole promise is that the suite means the same thing\n * afterwards, and a baseline gate cannot tell that kind of loss from a real one.\n *\n * The question is reported rather than settled: comparing the type as well would be a better rule,\n * and it may cost nothing on this corpus. That is a measurement to run and a change to make on its\n * own, once the suites no longer move. Measured need so far: `libs/cloud/shared`, whose last\n * missing test was exactly this.\n */\nimport type { expect as ExpectApi } from 'vitest'\n\n/**\n * Restore jest's rule: two errors are equal when their messages are.\n *\n * Returning `undefined` for anything else hands the pair back to the default comparison, which is\n * what an equality tester is expected to do for values it has no opinion about.\n */\nexport function errorsCompareByMessage(left: unknown, right: unknown): boolean | undefined {\n if (left instanceof Error && right instanceof Error) return left.message === right.message\n return undefined\n}\n\nexport function installJestErrorEquality(expect: typeof ExpectApi): void {\n expect.addEqualityTesters([errorsCompareByMessage])\n}\n","/**\n * `useFakeTimers({ doNotFake: [...] })`, which Vitest accepts and ignores.\n *\n * jest names what to LEAVE ALONE, Vitest names what to FAKE. The option Vitest does not know is\n * dropped in silence, so a suite that carefully kept `setTimeout` real gets it faked, and anything\n * awaiting a timer never resolves.\n *\n * Measured on both runners with the same source:\n *\n * useFakeTimers({ doNotFake: ['setTimeout'] }) jest: setTimeout real Vitest: setTimeout FAKED\n * useFakeTimers({ toFake: ['Date'] }) Vitest: setTimeout real\n *\n * Found on `libs/cloud/events-notifications`: 4 tests in one file died on `Test timed out in\n * 5000ms` with nothing else to show, because the code under test awaits a real timer. The repo has\n * 2 files using `doNotFake`, the other in `apps/nest/microservices/institution`.\n *\n * ## The translation, and what it inherits\n *\n * `doNotFake: [a, b]` becomes `toFake: <everything the runner fakes by default> minus [a, b]`. The\n * default set is Vitest's, measured rather than assumed, and NOT jest's, which is wider: jest also\n * fakes `nextTick`, `queueMicrotask` and the animation-frame pair. Subtracting from Vitest's own\n * default is what every other `useFakeTimers()` call in the corpus already gets, so this keeps one\n * behaviour for the whole migration instead of two.\n */\n\n/**\n * What `vi.useFakeTimers()` replaces when told nothing, measured on Vitest 4 by comparing each\n * global before and after the call.\n */\nconst FAKED_BY_DEFAULT = [\n 'setTimeout',\n 'clearTimeout',\n 'setInterval',\n 'clearInterval',\n 'setImmediate',\n 'clearImmediate',\n 'Date',\n 'performance',\n 'hrtime',\n] as const\n\n/** The options both runners take, plus the one only jest knows. */\ninterface TimerOptions {\n toFake?: string[]\n doNotFake?: string[]\n}\n\n/**\n * Turn \"leave these alone\" into \"fake those\", leaving anything else untouched.\n *\n * Exported for its own test: the translation is the whole rule, and asserting it directly says more\n * than asserting that a wrapper was installed.\n */\nexport function withoutJestOnlyOptions<T>(options: T): T {\n const given = options as TimerOptions | undefined\n if (given?.doNotFake === undefined) return options\n\n const { doNotFake, ...rest } = given\n const base = rest.toFake ?? [...FAKED_BY_DEFAULT]\n\n return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) } as T\n}\n\n/**\n * The one function this touches, named by its shape rather than by Vitest's type.\n *\n * `Options` is the caller's own parameter type: the wrapper hands back exactly what it was given,\n * minus the option Vitest does not know, so it must not narrow what the runner accepts.\n */\ninterface FakeTimerApi<Options> {\n useFakeTimers: (options?: Options) => unknown\n}\n\n/** Wrap `vi.useFakeTimers` so a jest-shaped options object still means what it said. */\nexport function installJestFakeTimerOptions<Options>(vi: FakeTimerApi<Options>): void {\n const inherited = vi.useFakeTimers.bind(vi)\n vi.useFakeTimers = (options?: Options) => inherited(withoutJestOnlyOptions(options))\n}\n","/**\n * The `jest` global, kept alive for helpers that a migrating module is not allowed to edit.\n *\n * ## Why a module cannot solve this for itself\n *\n * The codemod rewrites a module's own test files. It does not rewrite files in OTHER projects, and\n * it must not: a shared helper is imported by modules still on jest, so migrating it would break\n * them, and leaving it breaks the migrated one. That is the constraint the whole per-module plan\n * rests on.\n *\n * But those helpers call the jest API at MODULE scope. `libs/front/tests/src/mocks/**` does\n * `jest.fn()` when it is imported, before any test runs, so a migrated module dies on\n * `ReferenceError: jest is not defined` the moment it imports one.\n *\n * Measured repo-wide, excluding documentation: **50 files use the jest API without being test\n * files**, in `jest.setup.js`, `*.mock.ts`, `*.test-helper.ts`, `*.test-wrapper.ts`. Three\n * independent hand migrations reached this same line without knowing about each other:\n * `libs/front/components` (8 shared helpers, 16 sites), `apps/nest/microservices/mission` and\n * `apps/nest/backends-for-frontends/admin`.\n *\n * ## It is `vi`, not a fake jest\n *\n * The global IS Vitest's `vi`, so anything Vitest does not have keeps failing loudly:\n * `jest.requireActual` and `jest.isolateModules` are still errors, and a module relying on them\n * still has to be migrated properly. Handing over a hand-written imitation would turn those into\n * silent wrong behaviour, which is the opposite of the point.\n *\n * ## ⚠️ What it does NOT cover, and this bound is measured\n *\n * `jest.mock()`. Vitest hoists mock registrations above the imports by scanning the source\n * STATICALLY, and that scan only recognises the receivers `vi` and `vitest` (`@vitest/mocker`,\n * `hoistMocksPlugin`). A `jest.mock()` left in place is therefore NOT hoisted: it runs after the\n * imports it was meant to intercept and does nothing at all, in silence. Measured on\n * `apps/front/front-legacy`, where 216 of 427 files call it.\n *\n * So this covers a helper that CALLS the jest API. It does not make an unmigrated test file work,\n * and the codemod's rename stays load-bearing rather than cosmetic.\n */\n\n/** The part of `vi` this installs. Vitest's own type is not needed to say it. */\ntype JestLike = object\n\n/**\n * Put `vi` on `globalThis` under the name `jest`.\n *\n * Assigned rather than defined with a getter: a helper may well write to it (`jest.fn = ...` in a\n * test double), and a getter-only property would throw where jest allowed it.\n */\nexport function installJestGlobal(vi: JestLike): void {\n ;(globalThis as Record<string, unknown>).jest = vi\n}\n","/**\n * What `mockReset()` leaves behind, which is where the two runners disagree most dangerously.\n *\n * jest REMOVES the implementation: a reset spy returns `undefined` and the real function is not\n * called. Vitest puts the ORIGINAL implementation back: a reset spy calls the real function again.\n *\n * Measured on both runners with the same source:\n *\n * after resetAllMocks() on a spy jest: undefined Vitest: the real function\n * after mockReset() on a spy jest: undefined Vitest: the real function\n * after mockReset() on fn(impl) jest: undefined Vitest: impl\n *\n * The shape this breaks is ordinary and common: a suite spies on a provider in `beforeAll` and\n * resets its mocks in `beforeEach`. Under jest the provider stayed neutralised for every test.\n * Under Vitest the first `beforeEach` hands the real provider back, and every test after it runs\n * the real code. Measured on `libs/cloud/events-notifications`, that meant real HTTP: 19 tests\n * failed on `captured a request without a matching request handler` for the hermes API and 23 more\n * timed out waiting on it. Nothing in any report named a reset.\n *\n * 240 files in this repo both spy and reset, in every family: 111 under `apps/nest`, 77 under\n * `libs/cloud`, 31 under `apps/front`.\n *\n * ## Why here and not in the 240 files\n *\n * Because a codemod would have to decide, per spy, whether the suite wanted the real function\n * back, and the answer is in the test's intent rather than in its text. The runner-level rule is\n * the one that was true for all 240 while they were written, so restoring it is the transcription\n * and rewriting them would be the guess.\n *\n * Vitest routes `vi.resetAllMocks()` through each mock's own `mockReset`, measured, so overriding\n * that method covers the bulk form as well as the direct one. `mockRestore` is untouched: it puts\n * the original back under both runners, which is what it is for.\n */\n\n/** The part of a mock this file touches. Vitest's own types are not needed to say it. */\ninterface ResettableMock {\n mockReset: () => unknown\n mockImplementation: (fn: (...args: unknown[]) => unknown) => unknown\n}\n\nfunction isResettable(value: unknown): value is ResettableMock {\n return (\n typeof value === 'function' &&\n typeof (value as Partial<ResettableMock>).mockReset === 'function' &&\n typeof (value as Partial<ResettableMock>).mockImplementation === 'function'\n )\n}\n\n/**\n * Make one mock forget its implementation on reset, as jest's did.\n *\n * The override is installed on the instance rather than on a prototype: mocks are functions with\n * their own properties, and there is no shared prototype to reach.\n */\nfunction resetLikeJest<T>(mock: T): T {\n if (!isResettable(mock)) return mock\n\n const inherited = mock.mockReset.bind(mock)\n mock.mockReset = () => {\n inherited()\n mock.mockImplementation(() => undefined)\n return mock\n }\n return mock\n}\n\n/** The two factories a suite gets its mocks from. */\ntype MockFactories = { fn: (...args: never[]) => unknown; spyOn: (...args: never[]) => unknown }\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so everything they hand out resets the way jest's did.\n *\n * Called with the `vi` a setup file imports, so nothing here reaches for a global.\n */\nexport function installJestMockReset(vi: MockFactories): void {\n for (const name of ['fn', 'spyOn'] as const) {\n const factory = vi[name].bind(vi) as (...args: never[]) => unknown\n vi[name] = ((...args: never[]) => resetLikeJest(factory(...args))) as MockFactories[typeof name]\n }\n}\n","/**\n * A rejected value that is a FUNCTION, which jest calls and Vitest does not.\n *\n * ## The divergence, measured rather than reasoned\n *\n * jest's `toThrow` decides what was thrown like this: under `.rejects` it uses the rejection\n * reason ONLY when that reason is an Error. Otherwise it falls through to the ordinary branch,\n * sees a function, and CALLS it, asserting on whatever that call throws.\n *\n * Probed on this repo under jest 29, with controls:\n *\n * reject(() => { throw new Error('some error') })\n * await expect(…).rejects.toThrow(new Error('some error')) -> passes\n * await expect(…).rejects.toThrow(new Error('other text')) -> fails\n * await expect(…).rejects.toThrow('other text') -> fails\n *\n * The two controls are what make the first line mean something: jest is not passing everything,\n * it really is comparing the message of the error the CALL produced.\n *\n * Vitest treats the rejection reason as the thrown value, so the assertion is made against a\n * function. A function has no `message`, and the failure reads\n * `Cannot read properties of undefined (reading 'indexOf')`, which names nothing near the cause.\n *\n * ## Where it shows, and what it is worth\n *\n * Measured across the nest campaign: 4 tests in 3 modules.\n * `apps/nest/microservices/worker` (1), `apps/nest/microservices/institution` (2) and\n * `apps/nest/microservices/mission` (1). All four write the same shape, a mock rejecting with a\n * thunk that throws, or a `throw <a function>`.\n *\n * ## What this changes, and what it cannot\n *\n * Only a case that CANNOT work today: under `.rejects`, a reason that is a function and not an\n * Error. Vitest has no useful behaviour there, so nothing that passes today changes shape. An\n * Error reason, a string, an object, a rejected value of any other kind, and every assertion\n * outside `.rejects` all reach Vitest's own matcher untouched.\n *\n * ⚠️ It is jest's behaviour, not a good one. A suite reaching it is asserting on a function it\n * never meant to hand over, and it passed by accident of the runner. Reproducing it is what keeps\n * the migration honest: the gate promises the suite means the same thing afterwards, and a test\n * that was green cannot be turned red by us and called a finding. The teams own the cleanup.\n */\nimport { chai } from 'vitest'\n\n/** The part of a chai assertion this touches. Chai's own types are not needed to say it. */\ninterface AssertionLike {\n _obj: unknown\n}\n\ntype Matcher = (this: AssertionLike, ...args: unknown[]) => unknown\n\n/**\n * What calling the function throws, or the function itself when it throws nothing.\n *\n * Returning it unchanged matters: a function that completes is not \"nothing was thrown\", and\n * handing Vitest the same value it had leaves the report exactly as it would have been.\n */\nfunction thrownByCalling(candidate: () => unknown): unknown {\n try {\n candidate()\n } catch (thrown) {\n return thrown\n }\n return candidate\n}\n\nexport function installJestRejectedFunction(): void {\n const { Assertion, util } = chai as unknown as {\n Assertion: { prototype: Record<string, unknown> }\n util: { flag(object: unknown, key: string): unknown }\n }\n\n for (const name of ['toThrow', 'toThrowError']) {\n const original = Assertion.prototype[name] as Matcher | undefined\n if (typeof original !== 'function') continue\n\n Assertion.prototype[name] = function patched(this: AssertionLike, ...args: unknown[]): unknown {\n const reason = this._obj\n const rejected = util.flag(this, 'promise') === 'rejects'\n\n if (rejected && typeof reason === 'function' && !(reason instanceof Error)) {\n this._obj = thrownByCalling(reason as () => unknown)\n }\n\n return original.apply(this, args)\n } as unknown as Matcher\n }\n}\n"],"mappings":"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","names":["vi","vi","expect","vi","vi","vi"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../src/roles/test/setup/workspace-entry.ts"],"sourcesContent":["/**\n * What the WORKSPACE's shared jest setup did, for the modules that actually loaded it.\n *\n * Separate from `setup/nest` on purpose, and the split is the whole point. `setup/nest` carries the\n * runner shims: `jest.*` semantics restored under Vitest, which every migrated module needs whatever\n * its config said. This entry carries the ZONE that `jest.setup.after.env.js` pinned at the workspace root, which\n * only a module whose jest config NAMED that file ever had:\n *\n * require('dotenv-flow').config(...) the .env cascade\n * Settings.defaultZone = 'utc' luxon's default zone\n *\n * Measured on this repo: 97 modules name the root setup, 7 name a setup of their own instead. For\n * those 7 the pin was never applied, and applying it in the migration changes what the suite does\n * while claiming to move it.\n *\n * `libs/front/logic` is the one that said so out loud. It has its own setup, uses luxon, and its\n * test is literally called \"formats them in the local zone\":\n *\n * expect(formatTimeOfDay(new Date(2024, 2, 1, 8, 5))).toBe('08:05')\n * // pinned to utc: '07:05'\n *\n * One test, and it would have been one silent hour of offset in any suite that did not assert it.\n */\nimport { pinWorkspaceTimezone } from './workspace.js'\n\n/*\n * Awaited at the top level, so the environment is loaded before the first test file is imported.\n * Deferring it to a `beforeAll` would be too late: a module read at import time would already have\n * captured an unset variable.\n */\nawait pinWorkspaceTimezone()\n"],"mappings":";;;;;;AA8BA,MAAM,qBAAqB;","names":[]}
1
+ {"version":3,"sources":["../../../../src/roles/test/setup/workspace-entry.ts"],"sourcesContent":["/**\n * What the WORKSPACE's shared jest setup did, for the modules that actually loaded it.\n *\n * Separate from `setup/jest-parity` on purpose, and the split is the whole point. `setup/jest-parity` carries the\n * runner shims: `jest.*` semantics restored under Vitest, which every migrated module needs whatever\n * its config said. This entry carries the ZONE that `jest.setup.after.env.js` pinned at the workspace root, which\n * only a module whose jest config NAMED that file ever had:\n *\n * require('dotenv-flow').config(...) the .env cascade\n * Settings.defaultZone = 'utc' luxon's default zone\n *\n * Measured on this repo: 97 modules name the root setup, 7 name a setup of their own instead. For\n * those 7 the pin was never applied, and applying it in the migration changes what the suite does\n * while claiming to move it.\n *\n * `libs/front/logic` is the one that said so out loud. It has its own setup, uses luxon, and its\n * test is literally called \"formats them in the local zone\":\n *\n * expect(formatTimeOfDay(new Date(2024, 2, 1, 8, 5))).toBe('08:05')\n * // pinned to utc: '07:05'\n *\n * One test, and it would have been one silent hour of offset in any suite that did not assert it.\n */\nimport { pinWorkspaceTimezone } from './workspace.js'\n\n/*\n * Awaited at the top level, so the environment is loaded before the first test file is imported.\n * Deferring it to a `beforeAll` would be too late: a module read at import time would already have\n * captured an unset variable.\n */\nawait pinWorkspaceTimezone()\n"],"mappings":";;;;;;AA8BA,MAAM,qBAAqB;","names":[]}
@@ -24,5 +24,9 @@ interface SharedTestOptions {
24
24
  /** Merged over the base. For what a module genuinely needs to differ on, nothing else. */
25
25
  overrides?: ViteUserConfig;
26
26
  }
27
+ /** The config, with the module's own overrides merged over it. */
28
+ declare function sharedTestConfig(options: SharedTestOptions & {
29
+ flavour: TestFlavour;
30
+ }): ViteUserConfig;
27
31
 
28
- export type { SharedTestOptions as S };
32
+ export { type SharedTestOptions, type TestFlavour, sharedTestConfig };
@@ -0,0 +1,8 @@
1
+ import {
2
+ sharedTestConfig
3
+ } from "../../chunk-MT7VQXRA.js";
4
+ import "../../chunk-3TDUIKVQ.js";
5
+ export {
6
+ sharedTestConfig
7
+ };
8
+ //# sourceMappingURL=shared-test-config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -133,21 +133,65 @@ next to `treeshake: { moduleSideEffects: true }`.
133
133
 
134
134
  ## The check that decides whether you are done
135
135
 
136
- Not "the suite is green". **The suite gives the same result as before**, compared against a baseline
137
- taken BEFORE the migration:
136
+ Not "the suite is green". **The suite runs the same tests as before**. You do not have to arrange
137
+ that comparison; the two commands do it between them:
138
138
 
139
139
  ```console
140
- $ nx run <module>:test # jest, and write the numbers down
141
- $ sentinel --init --test
142
- $ nx run <module>:test # vitest
140
+ $ sentinel --init --test # runs jest ONCE first, records every test name, then migrates
141
+ $ pnpm install # the module's dependencies changed
142
+ $ sentinel --run --test # runs vitest, compares against that recording, then spends it
143
+ ```
144
+
145
+ A codemod touching thousands of files cannot be reviewed by hand. This comparison is the review.
146
+
147
+ ### Why it cannot be one command
148
+
149
+ The reference only exists BEFORE the migration, and the thing that produces it, the jest config, is
150
+ deleted by the very plan being applied. After `--init` there is nothing left to ask. And the two
151
+ runs cannot sit in one command either, because between them the module's `package.json` changed and
152
+ its dependencies have to be installed.
153
+
154
+ The recording lives outside the repository, in your temp directory, keyed by the module's path. It
155
+ survives that install and there is nothing to add to `.gitignore`.
156
+
157
+ ### What is compared: NAMES, never counts
158
+
159
+ "184 before, 184 after" does not say they are the same 184. Every defect this chapter found was a
160
+ name that moved, not a number that changed, and a count cannot see a suite that is green and
161
+ shorter.
162
+
163
+ So the gate refuses a run that lost a name or invented one, and says which:
164
+
165
+ ```console
166
+ $ sentinel --run --test
167
+ 1 test(s) no longer exist: useHublerNetworkProfilesQuery builds the URL with employment
168
+ statuses only. (reference: jest.config.ts, 2026-09-23T21:10:00.000Z) The reference is KEPT
169
+ until a run passes it, so this does not go green by running again. Fix the suite and re-run,
170
+ or delete /var/folders/.../sentinel-test-reference/009b573302c6e435.json to abandon the proof
171
+ on purpose.
143
172
  ```
144
173
 
145
- Take the baseline first, and compare the failing SETS rather than the counts. On `mission` the Jest
146
- baseline was not green (10 suites failed on `setTimeout is not defined`), and all ten pass under
147
- Vitest, so a count comparison would have read as a regression where there was an improvement.
174
+ Two behaviours are worth knowing before you meet them:
175
+
176
+ | situation | what happens |
177
+ | -------------------------------------- | --------------------------------------------------------------------------------- |
178
+ | the run passes | the recording is spent and removed; it answers one question once |
179
+ | the run is refused | the recording is **KEPT**, so running again cannot turn it green by itself |
180
+ | jest was already red before | still migrated, and said plainly: a broken jest run can move, it cannot be proved |
181
+ | a test was only RENAMED by the codemod | matched by two passes, exact first then permissive, and reported as renamed |
182
+
183
+ The refusal keeping the recording is deliberate. It used to be removed either way, and a second
184
+ identical run then came back green and silent: a gate you clear by running the command twice is an
185
+ assurance that does not exist, which is worse than no assurance. To abandon the proof on purpose,
186
+ delete the file the message names.
187
+
188
+ ### The one thing it cannot judge
148
189
 
149
- A codemod touching thousands of files cannot be reviewed by hand. The suite against its baseline is
150
- the review.
190
+ A reference that was already broken. If the recorded run is fully red, or holds a file that would
191
+ not load, or claims more failures than it names, there is nothing to compare against and the gate
192
+ says so instead of pretending. On `mission` the jest baseline was not green at all, 10 suites
193
+ failing on `setTimeout is not defined`, and all ten pass under Vitest: a comparison of counts would
194
+ have read that improvement as a regression.
151
195
 
152
196
  ### And the typecheck, which the suite cannot stand in for
153
197
 
@@ -3,8 +3,10 @@
3
3
  The commands, what an adopted module ends up looking like, and how versions move. For
4
4
  migrating a module see [`lint-adoption.md`](lint-adoption.md),
5
5
  [`format-adoption.md`](format-adoption.md),
6
- [`typescript-adoption.md`](typescript-adoption.md) and
7
- [`build-adoption.md`](build-adoption.md).
6
+ [`typescript-adoption.md`](typescript-adoption.md),
7
+ [`build-adoption.md`](build-adoption.md) and
8
+ [`test-adoption.md`](test-adoption.md), which also carries the reference gate `--test` runs a
9
+ migration against.
8
10
 
9
11
  ## The grid
10
12
 
@@ -17,14 +19,17 @@ sentinel --run --ci # from the root -> affected only
17
19
  sentinel --run # no target -> every wired target
18
20
  ```
19
21
 
20
- | Verb | `--lint` | `--format` | `--typescript` |
21
- | ----------- | --------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- |
22
- | `--init` | stub + scripts + nx cache metadata, removes the ESLint config and the module's ESLint deps, then runs oxlint's autofix once | materializes the preset + scripts + nx cache metadata, removes the Prettier config and deps, then formats the module once | writes/extends the tsconfig + `typecheck` script |
23
- | `--run` | lints; `--fix` autofixes in the same pass | **checks**; `--fix` writes | typechecks |
24
- | `--inspect` | rule count, what is disabled or downgraded and **why**, adoption, drift | effective options, every option that departs from the standard and why, adoption, drift | resolved options, what is deferred, adoption, drift |
25
- | `--report` | error and warning counts with a per-rule breakdown | how many files are unformatted, and which | error counts |
26
- | `--status` | deprecated, `--inspect` answers this and more | same | same |
27
- | `--migrate` | planned, refused for now | same | same |
22
+ Read it by target, since that is what you pick first. `--report` and `--status` are deprecated
23
+ everywhere (`--run --json` and `--inspect` answer them), and `--migrate` is refused everywhere.
24
+
25
+ | Target | `--init` writes | `--run` does | `--inspect` shows |
26
+ | -------------- | --------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------- |
27
+ | `--lint` | stub + scripts + nx metadata, removes the ESLint config and the module's ESLint deps, then autofixes once | lints; `--fix` autofixes in the same pass | rule count, what is disabled or downgraded and **why**, drift |
28
+ | `--format` | materializes the preset + scripts + nx metadata, removes the Prettier config and deps, then formats once | **checks**; `--fix` writes | effective options, every departure from the standard and why |
29
+ | `--typescript` | writes/extends the tsconfig + `typecheck` script | typechecks | resolved options, what is deferred, drift |
30
+ | `--build` | points the module's Vite config at sentinel's toolchain + scripts + nx metadata | builds | runner, config file, whose Vite, how many overrides, adoption |
31
+ | `--dev` | adopts the BUILD too: one plan writes both `build` and `serve` | serves; long-running, so never swept by an unqualified `--run` | refuses, and sends you to `--inspect --build`: it is that config |
32
+ | `--test` | records the jest reference, THEN the Vitest config + scripts + nx metadata, and migrates every spec file | runs Vitest and compares against that reference | runner, which configs were found, adoption state, drift |
28
33
 
29
34
  `--json` works for every verb, and stdout carries **only** the envelope, so `| jq` always
30
35
  parses. `--dry-run` applies to `--init` and writes nothing.
@@ -120,6 +125,8 @@ Four files, and none of them holds a rule.
120
125
  "format": "sentinel --run --format",
121
126
  "format:fix": "sentinel --run --format --fix",
122
127
  "typecheck": "sentinel --run --typescript",
128
+ // `--` so the runner can still be asked your own question: `pnpm test -- --coverage`
129
+ "test": "sentinel --run --test --",
123
130
  },
124
131
  "devDependencies": { "@hublo/sentinel": "1.1.0" },
125
132
  }
@@ -139,6 +146,14 @@ even when the module also has a `project.json`.
139
146
  // the writer is never cached: a cache hit would change nothing and leave the files
140
147
  // unformatted while nx reports success
141
148
  "format:fix": { "cache": false },
149
+ // `outputs` is CARRIED from the target being replaced, never invented: without one, a
150
+ // cache hit restores nothing and the coverage directory is silently empty, so a target
151
+ // that declares none is left uncached on purpose
152
+ "test": {
153
+ "cache": true,
154
+ "inputs": ["default", "^default", "{projectRoot}/vitest.config.mts"],
155
+ "outputs": ["{workspaceRoot}/coverage/libs/front/api"],
156
+ },
142
157
  },
143
158
  }
144
159
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.4.0-alpha.3",
3
+ "version": "1.4.0-alpha.4",
4
4
  "description": "One CLI that guards code health across Hublo repos: shared lint/typescript/build/test presets, static & dynamic analysis, and architecture checks.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -54,9 +54,9 @@
54
54
  "types": "./dist/roles/test/nest/toolchain.d.ts",
55
55
  "import": "./dist/roles/test/nest/toolchain.js"
56
56
  },
57
- "./test/setup/nest": {
58
- "types": "./dist/roles/test/setup/nest.d.ts",
59
- "import": "./dist/roles/test/setup/nest.js"
57
+ "./test/setup/jest-parity": {
58
+ "types": "./dist/roles/test/setup/jest-parity.d.ts",
59
+ "import": "./dist/roles/test/setup/jest-parity.js"
60
60
  },
61
61
  "./test/msw": {
62
62
  "types": "./dist/roles/test/setup/msw-server.d.ts",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../../../../src/roles/test/setup/nest.ts","../../../../src/roles/test/setup/constructor-semantics.ts","../../../../src/roles/test/setup/deep-mock-reset.ts","../../../../src/roles/test/setup/error-equality.ts","../../../../src/roles/test/setup/fake-timers.ts","../../../../src/roles/test/setup/jest-global.ts","../../../../src/roles/test/setup/mock-reset.ts","../../../../src/roles/test/setup/rejected-function.ts"],"sourcesContent":["/**\n * The setup a migrated Nest suite runs before its tests, as ONE entry point.\n *\n * Referenced by name from a generated config (`setupFiles: ['@hublo/sentinel/test/setup/nest']`),\n * never by a relative path. Measured: Vitest resolves a bare package specifier there, so nothing\n * has to know where sentinel sits relative to the module.\n *\n * ## What it replaces, line for line\n *\n * The repo's root `jest.setup.after.env.js`, loaded by 99 of the 111 jest configs:\n *\n * require('dotenv-flow').config({ silent: true, purge_dotenv: true })\n * const { Settings } = require('luxon')\n * const { server } = require('./libs/nest/tests/src/msw/server')\n * jest.mock('dynamoose')\n * jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')\n * beforeAll(() => server.listen({ onUnhandledRequest: 'error' }))\n * afterEach(() => { if (global.gc) global.gc() })\n * afterAll(() => server.close())\n * Settings.defaultZone = 'utc'\n *\n * Everything here is a transcription of that, not an improvement on it. Where the original made a\n * choice that looks questionable, the choice is carried across and reported, because a migration\n * that also changes behaviour cannot be verified against its own baseline.\n *\n * ONE line is not transcribed: `server.listen()` also runs when this file is evaluated, not only\n * inside `beforeAll`. The runners differ on when a module's exports can still be patched, so the\n * original placement silently disabled interception under Vitest and let tests reach the real\n * internet. `./msw.js` carries the measurement.\n *\n * ## Two halves, and why one of them is loaded defensively\n *\n * The msw lifecycle used to live here and now has its own file, `./msw-lifecycle.js`, added to a\n * module's `setupFiles` only when that module actually uses msw. Installing it everywhere patches\n * `http`/`https` and refuses unmatched requests for modules that never asked: measured on\n * `libs/nest/starter`, 11 of 11 became 10 of 11 with `TypeError: Invalid URL` in the interceptor,\n * on a test fetching the Fastify server the suite starts itself.\n *\n * `dotenv-flow` and luxon's zone react to the REPO, not to the runner: they would read the same\n * under jest, Vitest or `node:test`. So `./workspace.js` loads them only if they are installed and\n * skips them in silence otherwise. That defensiveness is not a precaution bolted on, it IS the\n * statement that these belong to whoever installed them. Elsewhere, sentinel installs and that half\n * does nothing.\n *\n * `dotenv-flow` cannot simply be dropped in favour of Vite's own `.env` handling, which was the\n * first thing checked. Measured on Vitest 4: it does read the cascade and sets `MODE=test`, but it\n * exposes only `VITE_`-prefixed values, and only on `import.meta.env`. `process.env` is left\n * untouched, and Nest code reads unprefixed `process.env`.\n *\n * ## What is NOT here\n *\n * The two `jest.mock` calls. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it; hoisting it into a shared setup is what makes a test pass for a reason nobody\n * can see. The migration reports them so the module relying on one declares it.\n *\n * And `Settings.defaultZone = 'utc'` is carried as luxon's own setting, never translated to\n * `process.env.TZ`. One configures luxon, the other the whole process, `Date` and `Intl` included.\n * Swapping them would change what the suite does while claiming to migrate it, and timezone is not\n * a detail: measured on `host-admin`, a machine's zone accounted for a large part of 345 local\n * failures that did not exist in CI.\n */\nimport { expect, vi } from 'vitest'\n\nimport { installJestConstructorSemantics } from './constructor-semantics.js'\nimport { installDeepMockReset } from './deep-mock-reset.js'\nimport { installJestErrorEquality } from './error-equality.js'\nimport { installJestFakeTimerOptions } from './fake-timers.js'\nimport { installJestGlobal } from './jest-global.js'\nimport { installJestMockReset } from './mock-reset.js'\nimport { installJestRejectedFunction } from './rejected-function.js'\nimport { installWorkspaceSetup } from './workspace.js'\n\ninstallJestErrorEquality(expect)\ninstallJestMockReset(vi)\ninstallJestConstructorSemantics(vi)\ninstallDeepMockReset(vi)\ninstallJestFakeTimerOptions(vi)\ninstallJestRejectedFunction()\ninstallJestGlobal(vi)\n\n/*\n * The `.env` cascade, unconditionally: a suite reading `process.env` needs it whatever setup its\n * jest config named, because Vitest's workers do not inherit what a `globalSetup` set in the main\n * process. Awaited at the top level, so it is loaded before the first test file is imported: a\n * module read at import time would already have captured an unset variable.\n *\n * ⚠️ What is NOT here any more is luxon's UTC zone. That was a decision ONE file at the workspace\n * root made, and only the 97 modules naming it ever had it; it lives in\n * `@hublo/sentinel/test/setup/workspace`, which the generated config adds only for those.\n */\nawait installWorkspaceSetup()\n","/**\n * `new` on a mock, the way jest answered it.\n *\n * ## The divergence, measured on the same source under both runners\n *\n * jest vitest\n * mockImplementation(() => obj), then new the obj TypeError: not a constructor\n * mockImplementation(function(){}), new works works\n * mockReturnValue(obj), then new the obj TypeError, with an explanation\n *\n * jest calls the implementation as a PLAIN function when the mock is constructed and hands back\n * what it returned. Vitest applies real `new` semantics, and an arrow function has no [[Construct]]\n * slot, so it throws.\n *\n * The pattern this breaks is the ordinary way to stand in for a class: automock the module, then\n * say what `new` should give back.\n *\n * jest.mock('@hublo/cloud/event-scheduler-sdk')\n * ;(EventSchedulerSDK as jest.MockedClass<typeof EventSchedulerSDK>)\n * .mockImplementation(() => mockEventSchedulerSDK)\n *\n * Measured across the nest and cloud campaigns: 2 modules, 15 tests.\n * `apps/nest/microservices/client-management` (9, an SDK) and `apps/nest/microservices/hublo-pool`\n * (6, a mocked `Date`).\n *\n * ## Why this is safe to do for everybody, which is the part that matters\n *\n * It only changes a case that THROWS today. An implementation without a `prototype` cannot be\n * constructed at all under Vitest, so no suite anywhere can be relying on what it does: the only\n * behaviours available are \"throws\" and \"answers like jest\". Nothing that works today changes\n * shape, and a constructable implementation is passed through untouched.\n *\n * That bound is asserted by the tests, not just claimed here.\n *\n * ## `mockReturnValue(obj)` then `new`, which this used to leave alone\n *\n * It was left out on the grounds that answering it would decide \"a return value and a constructed\n * instance are the same thing\", a claim about the suite. That reading was wrong on both halves.\n *\n * It is not a claim about the suite, because jest's answer is not ambiguous: it hands back the\n * value, exactly as it does for `mockImplementation(() => obj)`, which this file already restores.\n * Treating the two differently would be the arbitrary choice.\n *\n * And \"no module in this repo hit it\" stopped being true the moment the front family was measured.\n * `libs/front/components` mocks Google Maps the ordinary way and constructs it:\n *\n * AutocompleteService: jest.fn().mockReturnValue({ getPlacePredictions: jest.fn() })\n * // and the hook: new window.google.maps.places.AutocompleteService()\n *\n * 7 tests, all with the same TypeError. The safety bound is unchanged and it is the whole reason\n * this is allowed: Vitest THROWS on that call today, so no suite anywhere can depend on what it\n * does, and the only behaviours available are \"throws\" and \"answers like jest\".\n *\n * Expressed by routing the value through the implementation, rather than by a second mechanism:\n * `mockReturnValue(v)` IS `mockImplementation(() => v)`, so it goes through the same wrapper and\n * `new` works for the same reason.\n */\n\n/** The mocking surface this touches. Vitest's own type is not needed to say it. */\ninterface MockLike {\n mockImplementation(implementation: (...args: unknown[]) => unknown): unknown\n mockImplementationOnce(implementation: (...args: unknown[]) => unknown): unknown\n mockReturnValue(value: unknown): unknown\n mockReturnValueOnce(value: unknown): unknown\n}\n\ninterface ViLike {\n fn(implementation?: (...args: unknown[]) => unknown): MockLike\n /**\n * ⚠️ The REST, not `(target, key)`.\n *\n * Vitest's third argument is the access type, `'get'` or `'set'`, and the first version of the\n * wrapper below forwarded two arguments and dropped it. `vi.spyOn(el, 'scrollWidth', 'get')`\n * then became a spy on the VALUE of an accessor that only exists on a prototype, and jsdom\n * answered `'get scrollWidth' called on an object that is not a valid instance of Element`.\n *\n * Measured on `libs/front/components`, 2 tests, and invisible to every nest module because none\n * of them spies on a DOM accessor.\n */\n spyOn(target: object, ...rest: unknown[]): MockLike\n}\n\n/**\n * Can this function be used with `new`?\n *\n * Asked of the `prototype` property rather than of the source text: an arrow function, a shorthand\n * method and a bound function all lack it, and all three are exactly the cases that throw. A\n * class and a plain `function` have it.\n */\nfunction constructable(value: unknown): boolean {\n return (\n typeof value === 'function' && Object.getOwnPropertyDescriptor(value, 'prototype') !== undefined\n )\n}\n\n/**\n * The same implementation, reachable through `new`.\n *\n * A plain `function` that forwards the call and RETURNS the result. JavaScript's own `new` then\n * hands that object back, which is what jest did, so nothing here imitates jest by hand: it\n * restores the one property the arrow was missing and lets the language do the rest.\n */\nfunction asConstructable(\n implementation: (...args: unknown[]) => unknown,\n): (...args: unknown[]) => unknown {\n if (constructable(implementation)) return implementation\n\n return function forwarded(this: unknown, ...args: unknown[]): unknown {\n return implementation.apply(this, args)\n }\n}\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so every mock they produce accepts `new` the way jest's did.\n *\n * Wrapped at the factory, like `installJestMockReset`, because the behaviour belongs to every mock\n * a suite makes and a suite should not have to ask for it.\n */\nexport function installJestConstructorSemantics(vi: ViLike): void {\n const patch = (mock: MockLike): MockLike => {\n const { mockImplementation, mockImplementationOnce } = mock\n\n mock.mockImplementation = function (implementation) {\n return mockImplementation.call(this, asConstructable(implementation))\n }\n mock.mockImplementationOnce = function (implementation) {\n return mockImplementationOnce.call(this, asConstructable(implementation))\n }\n\n /*\n * Routed through the implementation rather than given a mechanism of its own: the two are the\n * same statement, and one of them already accepts `new`.\n */\n mock.mockReturnValue = function (value) {\n return this.mockImplementation(() => value)\n }\n mock.mockReturnValueOnce = function (value) {\n return this.mockImplementationOnce(() => value)\n }\n return mock\n }\n\n const { fn, spyOn } = vi\n\n vi.fn = function (implementation) {\n return patch(fn.call(this, implementation && asConstructable(implementation)))\n }\n vi.spyOn = function (target, ...rest) {\n return patch(spyOn.call(this, target, ...rest))\n }\n}\n","/**\n * `vi.resetAllMocks()` reaching the deep mocks, the way jest's registry did.\n *\n * ## The divergence, and why it is invisible\n *\n * jest built `jest-mock-extended`'s mocks with `jest.fn()`, so they sat in jest's own registry and\n * `jest.resetAllMocks()` cleared them with everything else. `vitest-mock-extended` builds them its\n * own way, so `vi.resetAllMocks()` walks past them and their call history survives into the next\n * test.\n *\n * Nothing announces it. The suite still runs, and an assertion fails several tests later with a\n * count that is off by exactly what its neighbour did.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does what jest expected:\n *\n * beforeEach(() => mocked.findEvents.mockResolvedValue([]))\n * afterEach(() => vi.resetAllMocks())\n *\n * Eleven tests asserting `toHaveBeenCalledTimes(0)` saw the call left by the one before them. Each\n * PASSES on its own and fails as soon as its neighbour runs first, which is the signature of\n * leakage rather than of a wrong assertion.\n *\n * ## What this installs, and what it leaves alone\n *\n * `resetAllMocks` and `clearAllMocks` do what they did, then extend to the deep mocks: `reset`\n * drops implementations as well as calls, `clear` drops only calls, which is the same distinction\n * the two names already carry.\n *\n * `restoreAllMocks` is NOT extended. It restores spies to their originals, and a deep mock has no\n * original to go back to: it was invented. Extending it would mean deciding what \"restore\" means\n * for something that never existed, which is a claim, not a translation.\n */\nimport { clearDeepMocks, resetDeepMocks } from './mock-extended.js'\n\n/** The part of `vi` this touches. Vitest's own type is not needed to say it. */\ninterface ViLike {\n resetAllMocks(): unknown\n clearAllMocks(): unknown\n}\n\nexport function installDeepMockReset(vi: ViLike): void {\n const { resetAllMocks, clearAllMocks } = vi\n\n vi.resetAllMocks = function extended(this: unknown): unknown {\n const answer = resetAllMocks.call(this)\n resetDeepMocks()\n return answer\n }\n\n vi.clearAllMocks = function extended(this: unknown): unknown {\n const answer = clearAllMocks.call(this)\n clearDeepMocks()\n return answer\n }\n}\n","/**\n * How two `Error` values compare, which the two runners disagree about.\n *\n * A suite that asserts on a thrown or captured error usually writes the error it expects by hand:\n *\n * expect(save).toHaveBeenCalledWith({ error: new AxiosError('Request failed with status code 500'), ... })\n *\n * Under jest that passes whatever else the real error carries. Under Vitest it fails, and the\n * report is 6600 lines of an axios error's `config`, `request` and `response`, which reads like a\n * broken test rather than a runner difference.\n *\n * ## What each runner actually does, measured on the same four cases\n *\n * | two errors | jest 29 | Vitest 4 |\n * | --------------------------------- | -------- | ----------- |\n * | same message, same type | equal | equal |\n * | same message, DIFFERENT types | equal | not equal |\n * | same message, extra properties | equal | not equal |\n * | different messages | not equal| not equal |\n *\n * jest compares errors by their MESSAGE and nothing else: a `TypeError` and a `RangeError` with the\n * same text are equal to it. Vitest compares the type and the own properties too.\n *\n * ## Why the looser rule is the one restored\n *\n * Because it is the one 3481 test files were written against. Tightening it here would turn green\n * tests red during a migration whose whole promise is that the suite means the same thing\n * afterwards, and a baseline gate cannot tell that kind of loss from a real one.\n *\n * The question is reported rather than settled: comparing the type as well would be a better rule,\n * and it may cost nothing on this corpus. That is a measurement to run and a change to make on its\n * own, once the suites no longer move. Measured need so far: `libs/cloud/shared`, whose last\n * missing test was exactly this.\n */\nimport type { expect as ExpectApi } from 'vitest'\n\n/**\n * Restore jest's rule: two errors are equal when their messages are.\n *\n * Returning `undefined` for anything else hands the pair back to the default comparison, which is\n * what an equality tester is expected to do for values it has no opinion about.\n */\nexport function errorsCompareByMessage(left: unknown, right: unknown): boolean | undefined {\n if (left instanceof Error && right instanceof Error) return left.message === right.message\n return undefined\n}\n\nexport function installJestErrorEquality(expect: typeof ExpectApi): void {\n expect.addEqualityTesters([errorsCompareByMessage])\n}\n","/**\n * `useFakeTimers({ doNotFake: [...] })`, which Vitest accepts and ignores.\n *\n * jest names what to LEAVE ALONE, Vitest names what to FAKE. The option Vitest does not know is\n * dropped in silence, so a suite that carefully kept `setTimeout` real gets it faked, and anything\n * awaiting a timer never resolves.\n *\n * Measured on both runners with the same source:\n *\n * useFakeTimers({ doNotFake: ['setTimeout'] }) jest: setTimeout real Vitest: setTimeout FAKED\n * useFakeTimers({ toFake: ['Date'] }) Vitest: setTimeout real\n *\n * Found on `libs/cloud/events-notifications`: 4 tests in one file died on `Test timed out in\n * 5000ms` with nothing else to show, because the code under test awaits a real timer. The repo has\n * 2 files using `doNotFake`, the other in `apps/nest/microservices/institution`.\n *\n * ## The translation, and what it inherits\n *\n * `doNotFake: [a, b]` becomes `toFake: <everything the runner fakes by default> minus [a, b]`. The\n * default set is Vitest's, measured rather than assumed, and NOT jest's, which is wider: jest also\n * fakes `nextTick`, `queueMicrotask` and the animation-frame pair. Subtracting from Vitest's own\n * default is what every other `useFakeTimers()` call in the corpus already gets, so this keeps one\n * behaviour for the whole migration instead of two.\n */\n\n/**\n * What `vi.useFakeTimers()` replaces when told nothing, measured on Vitest 4 by comparing each\n * global before and after the call.\n */\nconst FAKED_BY_DEFAULT = [\n 'setTimeout',\n 'clearTimeout',\n 'setInterval',\n 'clearInterval',\n 'setImmediate',\n 'clearImmediate',\n 'Date',\n 'performance',\n 'hrtime',\n] as const\n\n/** The options both runners take, plus the one only jest knows. */\ninterface TimerOptions {\n toFake?: string[]\n doNotFake?: string[]\n}\n\n/**\n * Turn \"leave these alone\" into \"fake those\", leaving anything else untouched.\n *\n * Exported for its own test: the translation is the whole rule, and asserting it directly says more\n * than asserting that a wrapper was installed.\n */\nexport function withoutJestOnlyOptions<T>(options: T): T {\n const given = options as TimerOptions | undefined\n if (given?.doNotFake === undefined) return options\n\n const { doNotFake, ...rest } = given\n const base = rest.toFake ?? [...FAKED_BY_DEFAULT]\n\n return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) } as T\n}\n\n/**\n * The one function this touches, named by its shape rather than by Vitest's type.\n *\n * `Options` is the caller's own parameter type: the wrapper hands back exactly what it was given,\n * minus the option Vitest does not know, so it must not narrow what the runner accepts.\n */\ninterface FakeTimerApi<Options> {\n useFakeTimers: (options?: Options) => unknown\n}\n\n/** Wrap `vi.useFakeTimers` so a jest-shaped options object still means what it said. */\nexport function installJestFakeTimerOptions<Options>(vi: FakeTimerApi<Options>): void {\n const inherited = vi.useFakeTimers.bind(vi)\n vi.useFakeTimers = (options?: Options) => inherited(withoutJestOnlyOptions(options))\n}\n","/**\n * The `jest` global, kept alive for helpers that a migrating module is not allowed to edit.\n *\n * ## Why a module cannot solve this for itself\n *\n * The codemod rewrites a module's own test files. It does not rewrite files in OTHER projects, and\n * it must not: a shared helper is imported by modules still on jest, so migrating it would break\n * them, and leaving it breaks the migrated one. That is the constraint the whole per-module plan\n * rests on.\n *\n * But those helpers call the jest API at MODULE scope. `libs/front/tests/src/mocks/**` does\n * `jest.fn()` when it is imported, before any test runs, so a migrated module dies on\n * `ReferenceError: jest is not defined` the moment it imports one.\n *\n * Measured repo-wide, excluding documentation: **50 files use the jest API without being test\n * files**, in `jest.setup.js`, `*.mock.ts`, `*.test-helper.ts`, `*.test-wrapper.ts`. Three\n * independent hand migrations reached this same line without knowing about each other:\n * `libs/front/components` (8 shared helpers, 16 sites), `apps/nest/microservices/mission` and\n * `apps/nest/backends-for-frontends/admin`.\n *\n * ## It is `vi`, not a fake jest\n *\n * The global IS Vitest's `vi`, so anything Vitest does not have keeps failing loudly:\n * `jest.requireActual` and `jest.isolateModules` are still errors, and a module relying on them\n * still has to be migrated properly. Handing over a hand-written imitation would turn those into\n * silent wrong behaviour, which is the opposite of the point.\n *\n * ## ⚠️ What it does NOT cover, and this bound is measured\n *\n * `jest.mock()`. Vitest hoists mock registrations above the imports by scanning the source\n * STATICALLY, and that scan only recognises the receivers `vi` and `vitest` (`@vitest/mocker`,\n * `hoistMocksPlugin`). A `jest.mock()` left in place is therefore NOT hoisted: it runs after the\n * imports it was meant to intercept and does nothing at all, in silence. Measured on\n * `apps/front/front-legacy`, where 216 of 427 files call it.\n *\n * So this covers a helper that CALLS the jest API. It does not make an unmigrated test file work,\n * and the codemod's rename stays load-bearing rather than cosmetic.\n */\n\n/** The part of `vi` this installs. Vitest's own type is not needed to say it. */\ntype JestLike = object\n\n/**\n * Put `vi` on `globalThis` under the name `jest`.\n *\n * Assigned rather than defined with a getter: a helper may well write to it (`jest.fn = ...` in a\n * test double), and a getter-only property would throw where jest allowed it.\n */\nexport function installJestGlobal(vi: JestLike): void {\n ;(globalThis as Record<string, unknown>).jest = vi\n}\n","/**\n * What `mockReset()` leaves behind, which is where the two runners disagree most dangerously.\n *\n * jest REMOVES the implementation: a reset spy returns `undefined` and the real function is not\n * called. Vitest puts the ORIGINAL implementation back: a reset spy calls the real function again.\n *\n * Measured on both runners with the same source:\n *\n * after resetAllMocks() on a spy jest: undefined Vitest: the real function\n * after mockReset() on a spy jest: undefined Vitest: the real function\n * after mockReset() on fn(impl) jest: undefined Vitest: impl\n *\n * The shape this breaks is ordinary and common: a suite spies on a provider in `beforeAll` and\n * resets its mocks in `beforeEach`. Under jest the provider stayed neutralised for every test.\n * Under Vitest the first `beforeEach` hands the real provider back, and every test after it runs\n * the real code. Measured on `libs/cloud/events-notifications`, that meant real HTTP: 19 tests\n * failed on `captured a request without a matching request handler` for the hermes API and 23 more\n * timed out waiting on it. Nothing in any report named a reset.\n *\n * 240 files in this repo both spy and reset, in every family: 111 under `apps/nest`, 77 under\n * `libs/cloud`, 31 under `apps/front`.\n *\n * ## Why here and not in the 240 files\n *\n * Because a codemod would have to decide, per spy, whether the suite wanted the real function\n * back, and the answer is in the test's intent rather than in its text. The runner-level rule is\n * the one that was true for all 240 while they were written, so restoring it is the transcription\n * and rewriting them would be the guess.\n *\n * Vitest routes `vi.resetAllMocks()` through each mock's own `mockReset`, measured, so overriding\n * that method covers the bulk form as well as the direct one. `mockRestore` is untouched: it puts\n * the original back under both runners, which is what it is for.\n */\n\n/** The part of a mock this file touches. Vitest's own types are not needed to say it. */\ninterface ResettableMock {\n mockReset: () => unknown\n mockImplementation: (fn: (...args: unknown[]) => unknown) => unknown\n}\n\nfunction isResettable(value: unknown): value is ResettableMock {\n return (\n typeof value === 'function' &&\n typeof (value as Partial<ResettableMock>).mockReset === 'function' &&\n typeof (value as Partial<ResettableMock>).mockImplementation === 'function'\n )\n}\n\n/**\n * Make one mock forget its implementation on reset, as jest's did.\n *\n * The override is installed on the instance rather than on a prototype: mocks are functions with\n * their own properties, and there is no shared prototype to reach.\n */\nfunction resetLikeJest<T>(mock: T): T {\n if (!isResettable(mock)) return mock\n\n const inherited = mock.mockReset.bind(mock)\n mock.mockReset = () => {\n inherited()\n mock.mockImplementation(() => undefined)\n return mock\n }\n return mock\n}\n\n/** The two factories a suite gets its mocks from. */\ntype MockFactories = { fn: (...args: never[]) => unknown; spyOn: (...args: never[]) => unknown }\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so everything they hand out resets the way jest's did.\n *\n * Called with the `vi` a setup file imports, so nothing here reaches for a global.\n */\nexport function installJestMockReset(vi: MockFactories): void {\n for (const name of ['fn', 'spyOn'] as const) {\n const factory = vi[name].bind(vi) as (...args: never[]) => unknown\n vi[name] = ((...args: never[]) => resetLikeJest(factory(...args))) as MockFactories[typeof name]\n }\n}\n","/**\n * A rejected value that is a FUNCTION, which jest calls and Vitest does not.\n *\n * ## The divergence, measured rather than reasoned\n *\n * jest's `toThrow` decides what was thrown like this: under `.rejects` it uses the rejection\n * reason ONLY when that reason is an Error. Otherwise it falls through to the ordinary branch,\n * sees a function, and CALLS it, asserting on whatever that call throws.\n *\n * Probed on this repo under jest 29, with controls:\n *\n * reject(() => { throw new Error('some error') })\n * await expect(…).rejects.toThrow(new Error('some error')) -> passes\n * await expect(…).rejects.toThrow(new Error('other text')) -> fails\n * await expect(…).rejects.toThrow('other text') -> fails\n *\n * The two controls are what make the first line mean something: jest is not passing everything,\n * it really is comparing the message of the error the CALL produced.\n *\n * Vitest treats the rejection reason as the thrown value, so the assertion is made against a\n * function. A function has no `message`, and the failure reads\n * `Cannot read properties of undefined (reading 'indexOf')`, which names nothing near the cause.\n *\n * ## Where it shows, and what it is worth\n *\n * Measured across the nest campaign: 4 tests in 3 modules.\n * `apps/nest/microservices/worker` (1), `apps/nest/microservices/institution` (2) and\n * `apps/nest/microservices/mission` (1). All four write the same shape, a mock rejecting with a\n * thunk that throws, or a `throw <a function>`.\n *\n * ## What this changes, and what it cannot\n *\n * Only a case that CANNOT work today: under `.rejects`, a reason that is a function and not an\n * Error. Vitest has no useful behaviour there, so nothing that passes today changes shape. An\n * Error reason, a string, an object, a rejected value of any other kind, and every assertion\n * outside `.rejects` all reach Vitest's own matcher untouched.\n *\n * ⚠️ It is jest's behaviour, not a good one. A suite reaching it is asserting on a function it\n * never meant to hand over, and it passed by accident of the runner. Reproducing it is what keeps\n * the migration honest: the gate promises the suite means the same thing afterwards, and a test\n * that was green cannot be turned red by us and called a finding. The teams own the cleanup.\n */\nimport { chai } from 'vitest'\n\n/** The part of a chai assertion this touches. Chai's own types are not needed to say it. */\ninterface AssertionLike {\n _obj: unknown\n}\n\ntype Matcher = (this: AssertionLike, ...args: unknown[]) => unknown\n\n/**\n * What calling the function throws, or the function itself when it throws nothing.\n *\n * Returning it unchanged matters: a function that completes is not \"nothing was thrown\", and\n * handing Vitest the same value it had leaves the report exactly as it would have been.\n */\nfunction thrownByCalling(candidate: () => unknown): unknown {\n try {\n candidate()\n } catch (thrown) {\n return thrown\n }\n return candidate\n}\n\nexport function installJestRejectedFunction(): void {\n const { Assertion, util } = chai as unknown as {\n Assertion: { prototype: Record<string, unknown> }\n util: { flag(object: unknown, key: string): unknown }\n }\n\n for (const name of ['toThrow', 'toThrowError']) {\n const original = Assertion.prototype[name] as Matcher | undefined\n if (typeof original !== 'function') continue\n\n Assertion.prototype[name] = function patched(this: AssertionLike, ...args: unknown[]): unknown {\n const reason = this._obj\n const rejected = util.flag(this, 'promise') === 'rejects'\n\n if (rejected && typeof reason === 'function' && !(reason instanceof Error)) {\n this._obj = thrownByCalling(reason as () => unknown)\n }\n\n return original.apply(this, args)\n } as unknown as Matcher\n }\n}\n"],"mappings":"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","names":["vi","vi","expect","vi","vi","vi"]}