@hublo/sentinel 1.4.0-alpha.39 → 1.4.0-alpha.40

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.
@@ -15,10 +15,10 @@ import {
15
15
  palette,
16
16
  registerAdapters,
17
17
  resolve
18
- } from "../chunk-CTF6XMW4.js";
18
+ } from "../chunk-USVNSL76.js";
19
19
  import {
20
20
  resolveContext
21
- } from "../chunk-AT5AEZ7M.js";
21
+ } from "../chunk-TDQGNNCV.js";
22
22
  import "../chunk-O7REVMOC.js";
23
23
  import "../chunk-QXFCZON7.js";
24
24
  import {
@@ -145,7 +145,7 @@ async function runValidate(ctx) {
145
145
  );
146
146
  }
147
147
  if (!ctx.targets.includes("test")) return 1;
148
- const { validateModuleTests } = await import("../validate-3BLRDD6D.js");
148
+ const { validateModuleTests } = await import("../validate-JUMFX4ZF.js");
149
149
  const outcomes = modules.flatMap(
150
150
  (module) => validateModuleTests(module.name, module.root)
151
151
  );
@@ -747,7 +747,7 @@ function resolveVitest(cwd) {
747
747
 
748
748
  // src/roles/test/adapters/vitest/vitest.adapter.ts
749
749
  import { spawnSync } from "node:child_process";
750
- import { isAbsolute as isAbsolute2, resolve as resolve8 } from "node:path";
750
+ import { isAbsolute as isAbsolute2, resolve as resolve9 } from "node:path";
751
751
 
752
752
  // src/core/config/tool-args.ts
753
753
  import { existsSync as existsSync6 } from "node:fs";
@@ -897,12 +897,12 @@ function rewriteToolchainImports(toolchain, source, fileName) {
897
897
  }
898
898
 
899
899
  // src/roles/test/jest-migration.ts
900
- import { existsSync as existsSync18, readFileSync as readFileSync19, readdirSync as readdirSync6 } from "node:fs";
900
+ import { existsSync as existsSync19, readFileSync as readFileSync20, readdirSync as readdirSync6 } from "node:fs";
901
901
  import { createRequire as createRequire5 } from "node:module";
902
902
  import { dirname as dirname11, join as join24, relative as relative5 } from "node:path";
903
903
 
904
904
  // src/roles/test/codemod/migrate-module.ts
905
- import { readdirSync as readdirSync4, readFileSync as readFileSync14, statSync as statSync4 } from "node:fs";
905
+ import { readdirSync as readdirSync4, readFileSync as readFileSync15, statSync as statSync4 } from "node:fs";
906
906
  import { join as join17 } from "node:path";
907
907
  import { parseSync as parseSync8 } from "oxc-parser";
908
908
 
@@ -935,11 +935,9 @@ function vitestSupportName(relative6) {
935
935
  return `${directory}${VITEST_PREFIX}${base.slice(JEST_PREFIX.length)}`;
936
936
  }
937
937
 
938
- // src/roles/test/codemod/inject-type-import.ts
939
- import { parseSync as parseSync3 } from "oxc-parser";
940
-
941
- // src/roles/test/codemod/rename.ts
942
- import { parseSync as parseSync2 } from "oxc-parser";
938
+ // src/roles/test/codemod/inherited-automocks.ts
939
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
940
+ import { resolve as resolve3 } from "node:path";
943
941
 
944
942
  // src/shared/ast.ts
945
943
  var SKIP = /* @__PURE__ */ Symbol("skip-subtree");
@@ -957,7 +955,89 @@ function walk(node, visit) {
957
955
  }
958
956
  var MEMBER_TYPES = /* @__PURE__ */ new Set(["MemberExpression", "StaticMemberExpression"]);
959
957
 
958
+ // src/roles/test/codemod/edits.ts
959
+ function applyEdits(source, edits) {
960
+ let result = source;
961
+ for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
962
+ result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
963
+ }
964
+ return result;
965
+ }
966
+
967
+ // src/roles/test/codemod/inherited-automocks.ts
968
+ function bareModuleMocks(program) {
969
+ const found = [];
970
+ walk(program, (node) => {
971
+ if (node.type !== "CallExpression") return;
972
+ const callee = node.callee;
973
+ if (callee === void 0 || !MEMBER_TYPES.has(callee.type)) return;
974
+ const object = callee.object;
975
+ const property = callee.property;
976
+ if (object?.type !== "Identifier" || object.name !== "jest" && object.name !== "vi") return;
977
+ if (property?.type !== "Identifier" || property.name !== "mock") return;
978
+ const args = node.arguments ?? [];
979
+ if (args.length !== 1) return;
980
+ const [specifier] = args;
981
+ if (specifier?.type === "StringLiteral" || specifier?.type === "Literal") {
982
+ found.push(String(specifier.value));
983
+ }
984
+ });
985
+ return found;
986
+ }
987
+ function inheritedAutomocks(cwd, setupFiles, parse4) {
988
+ const found = /* @__PURE__ */ new Set();
989
+ for (const named of setupFiles) {
990
+ const path = resolve3(cwd, named.replace("<rootDir>/", "./"));
991
+ if (path.startsWith(`${cwd}/`)) continue;
992
+ if (!existsSync10(path)) continue;
993
+ let source;
994
+ try {
995
+ source = readFileSync8(path, "utf8");
996
+ } catch {
997
+ continue;
998
+ }
999
+ const program = parse4(path, source);
1000
+ if (program === void 0) continue;
1001
+ for (const specifier of bareModuleMocks(program)) found.add(specifier);
1002
+ }
1003
+ return [...found].sort();
1004
+ }
1005
+ function importsSpecifier(program, specifier) {
1006
+ for (const statement2 of program.body ?? []) {
1007
+ if (statement2.type !== "ImportDeclaration") continue;
1008
+ const source = statement2.source;
1009
+ if (source !== void 0 && String(source.value) === specifier) return true;
1010
+ }
1011
+ return false;
1012
+ }
1013
+ function declareInheritedAutomocks(source, program, specifiers) {
1014
+ const alreadyMocked = new Set(bareModuleMocks(program));
1015
+ const missing = specifiers.filter(
1016
+ (specifier) => !alreadyMocked.has(specifier) && importsSpecifier(program, specifier)
1017
+ );
1018
+ if (missing.length === 0) return source;
1019
+ const imports = (program.body ?? []).filter(
1020
+ (node) => node.type === "ImportDeclaration"
1021
+ );
1022
+ const last = imports[imports.length - 1];
1023
+ const at = last === void 0 ? 0 : last.end;
1024
+ const semi = /^import\s[^\n]*;\s*$/m.test(source) ? ";" : "";
1025
+ const block = missing.map((specifier) => `vi.mock('${specifier}')${semi}`).join("\n");
1026
+ const edits = [
1027
+ { start: at, end: at, text: last === void 0 ? `${block}
1028
+
1029
+ ` : `
1030
+
1031
+ ${block}` }
1032
+ ];
1033
+ return applyEdits(source, edits);
1034
+ }
1035
+
1036
+ // src/roles/test/codemod/inject-type-import.ts
1037
+ import { parseSync as parseSync3 } from "oxc-parser";
1038
+
960
1039
  // src/roles/test/codemod/rename.ts
1040
+ import { parseSync as parseSync2 } from "oxc-parser";
961
1041
  var VITEST_TYPE_SOURCE = "vitest";
962
1042
  var RUNTIME_RENAMES = /* @__PURE__ */ new Set([
963
1043
  // Mocks
@@ -1143,15 +1223,6 @@ ${source}`;
1143
1223
  import { dirname as dirname7 } from "node:path";
1144
1224
  import { parseSync as parseSync7 } from "oxc-parser";
1145
1225
 
1146
- // src/roles/test/codemod/edits.ts
1147
- function applyEdits(source, edits) {
1148
- let result = source;
1149
- for (const edit of [...edits].sort((left, right) => right.start - left.start)) {
1150
- result = `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`;
1151
- }
1152
- return result;
1153
- }
1154
-
1155
1226
  // src/roles/test/codemod/assertion-return-value.ts
1156
1227
  function rootedAtExpect(node) {
1157
1228
  let current = node;
@@ -1766,7 +1837,12 @@ function simpleDeclarations(program) {
1766
1837
  const id = declarator.id;
1767
1838
  const init = declarator.init;
1768
1839
  if (id?.type !== "Identifier" || init === void 0 || init === null) continue;
1769
- found.set(id.name, { statement: statement2, init, kind: statement2.kind ?? "const" });
1840
+ found.set(id.name, {
1841
+ statement: statement2,
1842
+ init,
1843
+ kind: statement2.kind ?? "const",
1844
+ annotation: id.typeAnnotation?.typeAnnotation
1845
+ });
1770
1846
  }
1771
1847
  return found;
1772
1848
  }
@@ -1819,7 +1895,11 @@ function hoistCapturedBindings(source, program, factories, bindings, imported) {
1819
1895
  const first = ordered[0];
1820
1896
  const semi = source.slice(first.statement.start, first.statement.end).trimEnd().endsWith(";");
1821
1897
  const end = semi ? ";" : "";
1822
- const initOf = ({ init }) => source.slice(init.start, init.end);
1898
+ const initOf = ({ init, annotation }) => {
1899
+ const value = source.slice(init.start, init.end);
1900
+ if (annotation?.start === void 0) return value;
1901
+ return `${value} as ${source.slice(annotation.start, annotation.end)}`;
1902
+ };
1823
1903
  const constants = ordered.filter(({ kind }) => kind === "const");
1824
1904
  const objectLine = constants.length === 0 ? void 0 : `const { ${constants.map(({ name }) => name).join(", ")} } = vi.hoisted(() => ({ ${constants.map((entry) => `${entry.name}: ${initOf(entry)}`).join(", ")} }))${end}
1825
1905
  `;
@@ -1842,7 +1922,11 @@ function hoistCapturedBindings(source, program, factories, bindings, imported) {
1842
1922
  return { ...span, text: index === 0 ? hoistedLine : "" };
1843
1923
  });
1844
1924
  const rewritten = applyEdits(source, edits);
1845
- return { source: rewritten, hoisted: ordered.map(({ name }) => name), refusals: [] };
1925
+ return {
1926
+ source: rewritten,
1927
+ hoisted: ordered.map(({ name }) => name),
1928
+ refusals: []
1929
+ };
1846
1930
  }
1847
1931
 
1848
1932
  // src/roles/test/codemod/hook-return-value.ts
@@ -1880,17 +1964,54 @@ function rewriteHookReturnValue(source, program) {
1880
1964
  }
1881
1965
 
1882
1966
  // src/roles/test/codemod/named-rules.ts
1883
- import { existsSync as existsSync10, readFileSync as readFileSync9 } from "node:fs";
1967
+ import { existsSync as existsSync11, readFileSync as readFileSync10 } from "node:fs";
1884
1968
  import { createRequire as createRequire2 } from "node:module";
1885
1969
  import { dirname as dirname3, join as join11 } from "node:path";
1886
1970
 
1971
+ // src/roles/test/codemod/namespace-import.ts
1972
+ function namesInUse(program) {
1973
+ const used = /* @__PURE__ */ new Set();
1974
+ walk(program, (node) => {
1975
+ if (node.type === "Identifier") used.add(node.name);
1976
+ });
1977
+ return used;
1978
+ }
1979
+ function readableName(specifier) {
1980
+ const last = specifier.split("/").filter(Boolean).pop() ?? "module";
1981
+ const withoutExtension = last.replace(/\.(?:m|c)?[jt]sx?$/, "");
1982
+ const camel = withoutExtension.split(/[^A-Za-z0-9]+/).filter(Boolean).map((part, index) => index === 0 ? part : part[0]?.toUpperCase() + part.slice(1)).join("");
1983
+ return /^[A-Za-z_$]/.test(camel) ? camel : `module${camel}`;
1984
+ }
1985
+ function namespaceBindings(program) {
1986
+ const taken = namesInUse(program);
1987
+ const bySpecifier = /* @__PURE__ */ new Map();
1988
+ return {
1989
+ bindingFor(specifier) {
1990
+ const existing = bySpecifier.get(specifier);
1991
+ if (existing !== void 0) return existing;
1992
+ const base = `${readableName(specifier)}Mock`;
1993
+ let name = base;
1994
+ for (let suffix = 2; taken.has(name); suffix += 1) name = `${base}${suffix}`;
1995
+ taken.add(name);
1996
+ bySpecifier.set(specifier, name);
1997
+ return name;
1998
+ },
1999
+ planned() {
2000
+ return [...bySpecifier].map(([specifier, binding]) => ({
2001
+ specifier,
2002
+ binding
2003
+ }));
2004
+ }
2005
+ };
2006
+ }
2007
+
1887
2008
  // src/roles/test/codemod/static-import-for-require.ts
1888
- import { readFileSync as readFileSync8, statSync } from "node:fs";
1889
- import { dirname as dirname2, join as join10, resolve as resolve3 } from "node:path";
2009
+ import { readFileSync as readFileSync9, statSync } from "node:fs";
2010
+ import { dirname as dirname2, join as join10, resolve as resolve4 } from "node:path";
1890
2011
  import { parseSync as parseSync4 } from "oxc-parser";
1891
2012
  var EXTENSIONS = [".ts", ".tsx", ".mts", ".cts", ".js", ".jsx", ".mjs", ".cjs"];
1892
2013
  function resolveRelative(fromFile, specifier) {
1893
- const base = resolve3(dirname2(fromFile), specifier);
2014
+ const base = resolve4(dirname2(fromFile), specifier);
1894
2015
  const candidates = [
1895
2016
  base,
1896
2017
  ...EXTENSIONS.map((extension) => `${base}${extension}`),
@@ -1920,17 +2041,17 @@ function relativeSpecifiers(source) {
1920
2041
  function reachesBack(fromFile, specifier) {
1921
2042
  const entry = resolveRelative(fromFile, specifier);
1922
2043
  if (entry === void 0) return true;
1923
- const target = resolve3(fromFile);
2044
+ const target = resolve4(fromFile);
1924
2045
  const seen = /* @__PURE__ */ new Set();
1925
2046
  const stack = [entry];
1926
2047
  while (stack.length > 0 && seen.size < 200) {
1927
2048
  const file = stack.pop();
1928
2049
  if (seen.has(file)) continue;
1929
2050
  seen.add(file);
1930
- if (resolve3(file) === target) return true;
2051
+ if (resolve4(file) === target) return true;
1931
2052
  let source;
1932
2053
  try {
1933
- source = readFileSync8(file, "utf8");
2054
+ source = readFileSync9(file, "utf8");
1934
2055
  } catch {
1935
2056
  continue;
1936
2057
  }
@@ -2053,9 +2174,9 @@ function isCommonJsPackage(fromFile, specifier) {
2053
2174
  let dir = dirname3(resolved);
2054
2175
  for (let depth = 0; depth < 12; depth += 1) {
2055
2176
  const manifest = join11(dir, "package.json");
2056
- if (existsSync10(manifest)) {
2177
+ if (existsSync11(manifest)) {
2057
2178
  try {
2058
- return JSON.parse(readFileSync9(manifest, "utf8")).type !== "module";
2179
+ return JSON.parse(readFileSync10(manifest, "utf8")).type !== "module";
2059
2180
  } catch {
2060
2181
  return false;
2061
2182
  }
@@ -2173,12 +2294,20 @@ function typeArgumentText(source, call) {
2173
2294
  if (node?.start === void 0 || node.end === void 0) return "";
2174
2295
  return source.slice(node.start, node.end);
2175
2296
  }
2297
+ function typeArgumentOrTheModulesOwn(source, call) {
2298
+ const own = typeArgumentText(source, call);
2299
+ if (own !== "") return own;
2300
+ const [specifier] = call.arguments ?? [];
2301
+ if (specifier?.type !== "StringLiteral" && specifier?.type !== "Literal") return "";
2302
+ return `<typeof import(${source.slice(specifier.start, specifier.end)})>`;
2303
+ }
2176
2304
  function applyNamedRules(source, program, fileName = "") {
2177
2305
  const edits = [];
2178
2306
  const applied = /* @__PURE__ */ new Set();
2179
2307
  const asyncify = /* @__PURE__ */ new Set();
2180
2308
  const refusals = [];
2181
2309
  const staticImports = [];
2310
+ const namespaces = namespaceBindings(program);
2182
2311
  const mocked = mockedSpecifiers(program);
2183
2312
  const methods = methodValues(program);
2184
2313
  const cannotAwaitHere = (enclosing, what, call) => {
@@ -2196,6 +2325,18 @@ function applyNamedRules(source, program, fileName = "") {
2196
2325
  });
2197
2326
  return true;
2198
2327
  };
2328
+ const spiedOn = /* @__PURE__ */ new Set();
2329
+ walk(program, (node) => {
2330
+ if (node.type !== "CallExpression") return;
2331
+ const callee = node.callee;
2332
+ if (callee === void 0 || !MEMBER_TYPES.has(callee.type)) return;
2333
+ const object = callee.object;
2334
+ const property = callee.property;
2335
+ if (object?.type !== "Identifier" || object.name !== "jest" && object.name !== "vi") return;
2336
+ if (property?.type !== "Identifier" || property.name !== "spyOn") return;
2337
+ const [target] = node.arguments ?? [];
2338
+ if (target !== void 0) spiedOn.add(target);
2339
+ });
2199
2340
  const wholeStatement = /* @__PURE__ */ new Map();
2200
2341
  walk(program, (node) => {
2201
2342
  if (node.type !== "ExpressionStatement") return;
@@ -2263,14 +2404,23 @@ function applyNamedRules(source, program, fileName = "") {
2263
2404
  const [specifier] = node.arguments ?? [];
2264
2405
  const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
2265
2406
  if (literal !== void 0 && mocked.has(literal)) {
2266
- if (cannotAwaitHere(enclosing, "jest.requireMock", node)) return;
2267
2407
  edits.push({
2268
2408
  start: node.start,
2269
2409
  end: node.end,
2270
- text: placed(node, `await import(${argumentText(source, node)})`)
2410
+ /*
2411
+ * `vi.mocked(...)`, which is the identity at runtime and everything at compile time.
2412
+ * `jest.requireMock` returned `any`, so `.mockReset()` and `.mockImplementation()` on
2413
+ * what came out of it always compiled. A namespace import is typed as the REAL module,
2414
+ * so those calls stop compiling: 7 errors in `bff-backoffice`, on the same file whose
2415
+ * dynamic import cost 146 lint errors. `vi.mocked` restores the mock-shaped type
2416
+ * without restoring `any`, so a typo in the member is still caught.
2417
+ *
2418
+ * A call expression needs no parentheses: `vi.mocked(x).y` and `vi.mocked(x)()` read
2419
+ * exactly where the original call read.
2420
+ */
2421
+ text: `vi.mocked(${namespaces.bindingFor(literal)})`
2271
2422
  });
2272
2423
  applied.add("jest.requireMock");
2273
- if (enclosing !== void 0 && enclosing.async !== true) asyncify.add(enclosing);
2274
2424
  return;
2275
2425
  }
2276
2426
  }
@@ -2282,7 +2432,7 @@ function applyNamedRules(source, program, fileName = "") {
2282
2432
  end: node.end,
2283
2433
  text: placed(
2284
2434
  node,
2285
- `await vi.${asyncReplacement}${typeArgumentText(source, node)}(${argumentText(source, node)})`
2435
+ `await vi.${asyncReplacement}${typeArgumentOrTheModulesOwn(source, node)}(${argumentText(source, node)})`
2286
2436
  )
2287
2437
  });
2288
2438
  applied.add(`jest.${member}`);
@@ -2293,7 +2443,7 @@ function applyNamedRules(source, program, fileName = "") {
2293
2443
  if (cannotAwaitHere(enclosing, "require", node)) return;
2294
2444
  const [specifier] = node.arguments ?? [];
2295
2445
  const literal = specifier?.type === "StringLiteral" || specifier?.type === "Literal" ? String(specifier.value) : void 0;
2296
- const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
2446
+ const interop = literal !== void 0 && !readsANameOffIt.has(node) && !handedToTheRunnerAsAModule.has(node) && !wholeStatement.has(node) && !spiedOn.has(node) && isCommonJsPackage(fileName, literal) ? ".default" : "";
2297
2447
  edits.push({
2298
2448
  start: node.start,
2299
2449
  end: node.end,
@@ -2326,7 +2476,10 @@ function applyNamedRules(source, program, fileName = "") {
2326
2476
  for (const fn of asyncify) {
2327
2477
  edits.push({ start: fn.start, end: fn.start, text: "async " });
2328
2478
  }
2329
- if (staticImports.length > 0) {
2479
+ const namespaceLines = namespaces.planned().map(
2480
+ ({ specifier, binding }) => `import * as ${binding} from '${specifier}'${fileUsesSemicolons(source) ? ";" : ""}`
2481
+ );
2482
+ if (staticImports.length > 0 || namespaceLines.length > 0) {
2330
2483
  const first = (program.body ?? []).find(
2331
2484
  (node) => node.type === "ImportDeclaration"
2332
2485
  );
@@ -2339,11 +2492,20 @@ function applyNamedRules(source, program, fileName = "") {
2339
2492
  byModule.set(parsed[2], names);
2340
2493
  }
2341
2494
  const semi = staticImports[0]?.endsWith(";") === true ? ";" : "";
2342
- const block = `${[...byModule].map(([specifier, names]) => `import { ${[...names].join(", ")} } from '${specifier}'${semi}`).join("\n")}
2495
+ const block = `${[
2496
+ ...namespaceLines,
2497
+ ...[...byModule].map(
2498
+ ([specifier, names]) => `import { ${[...names].join(", ")} } from '${specifier}'${semi}`
2499
+ )
2500
+ ].join("\n")}
2343
2501
  `;
2344
2502
  edits.push(
2345
2503
  first === void 0 ? { start: 0, end: 0, text: `${block}
2346
- ` } : { start: first.start, end: first.start, text: block }
2504
+ ` } : {
2505
+ start: first.start,
2506
+ end: first.start,
2507
+ text: block
2508
+ }
2347
2509
  );
2348
2510
  }
2349
2511
  return { source: applyEdits(source, edits), applied: [...applied].sort(), refusals };
@@ -2847,7 +3009,7 @@ function propagateAwait(fileName, source, original, elsewhere = /* @__PURE__ */
2847
3009
  }
2848
3010
 
2849
3011
  // src/roles/test/codemod/reexport-barrel-mock.ts
2850
- import { readFileSync as readFileSync10, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
3012
+ import { readFileSync as readFileSync11, readdirSync as readdirSync2, statSync as statSync2 } from "node:fs";
2851
3013
  import { createRequire as createRequire3 } from "node:module";
2852
3014
  import { dirname as dirname4, join as join12 } from "node:path";
2853
3015
  var PACKAGE_SUBPATH = /^(@[^/]+\/[^/]+|[^@.][^/]*)\/(.+)$/;
@@ -2905,7 +3067,7 @@ function barrelsFor(fromFile, name, target) {
2905
3067
  let body;
2906
3068
  try {
2907
3069
  if (!statSync2(path).isFile()) continue;
2908
- body = readFileSync10(path, "utf8");
3070
+ body = readFileSync11(path, "utf8");
2909
3071
  } catch {
2910
3072
  continue;
2911
3073
  }
@@ -2986,15 +3148,15 @@ function repointMswServer(fileName, source) {
2986
3148
  }
2987
3149
 
2988
3150
  // src/roles/test/codemod/setup-static-imports.ts
2989
- import { dirname as dirname6, resolve as resolve6 } from "node:path";
3151
+ import { dirname as dirname6, resolve as resolve7 } from "node:path";
2990
3152
 
2991
3153
  // src/core/context.ts
2992
- import { existsSync as existsSync11 } from "node:fs";
2993
- import { dirname as dirname5, join as join14, resolve as resolve4 } from "node:path";
3154
+ import { existsSync as existsSync12 } from "node:fs";
3155
+ import { dirname as dirname5, join as join14, resolve as resolve5 } from "node:path";
2994
3156
 
2995
3157
  // src/core/discover-modules.ts
2996
3158
  import { execFileSync as execFileSync2 } from "node:child_process";
2997
- import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync11, rmSync as rmSync2 } from "node:fs";
3159
+ import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync12, rmSync as rmSync2 } from "node:fs";
2998
3160
  import { tmpdir as tmpdir2 } from "node:os";
2999
3161
  import { join as join13 } from "node:path";
3000
3162
  function runNx(cwd, args) {
@@ -3018,7 +3180,7 @@ function readGraph(cwd) {
3018
3180
  const file = join13(dir, "graph.json");
3019
3181
  try {
3020
3182
  runNx(cwd, ["graph", "--file", file]);
3021
- const parsed = JSON.parse(readFileSync11(file, "utf8"));
3183
+ const parsed = JSON.parse(readFileSync12(file, "utf8"));
3022
3184
  const nodes = parsed.graph?.nodes;
3023
3185
  if (!nodes || typeof nodes !== "object") {
3024
3186
  throw new Error(
@@ -3068,12 +3230,12 @@ ${raw.slice(0, 300)}`
3068
3230
  // src/core/context.ts
3069
3231
  var MODULE_MARKERS = ["package.json", "project.json"];
3070
3232
  function isModuleDir(cwd) {
3071
- return MODULE_MARKERS.some((marker) => existsSync11(join14(cwd, marker)));
3233
+ return MODULE_MARKERS.some((marker) => existsSync12(join14(cwd, marker)));
3072
3234
  }
3073
3235
  function findModuleRoot(startDir, workspaceRoot) {
3074
- let dir = resolve4(startDir);
3236
+ let dir = resolve5(startDir);
3075
3237
  for (; ; ) {
3076
- if (workspaceRoot !== void 0 && dir === resolve4(workspaceRoot)) return void 0;
3238
+ if (workspaceRoot !== void 0 && dir === resolve5(workspaceRoot)) return void 0;
3077
3239
  if (isModuleDir(dir)) return dir;
3078
3240
  const parent = dirname5(dir);
3079
3241
  if (parent === dir) return void 0;
@@ -3081,7 +3243,7 @@ function findModuleRoot(startDir, workspaceRoot) {
3081
3243
  }
3082
3244
  }
3083
3245
  function resolveContext(cwd, opts) {
3084
- const atRoot = existsSync11(join14(cwd, WORKSPACE_ROOT_MARKER));
3246
+ const atRoot = existsSync12(join14(cwd, WORKSPACE_ROOT_MARKER));
3085
3247
  if (!atRoot && isModuleDir(cwd)) {
3086
3248
  if (opts.module) {
3087
3249
  throw new Error(
@@ -3109,16 +3271,16 @@ function resolveContext(cwd, opts) {
3109
3271
  }
3110
3272
 
3111
3273
  // src/roles/test/codemod/workspace-specifier.ts
3112
- import { existsSync as existsSync12, readFileSync as readFileSync12 } from "node:fs";
3113
- import { join as join15, relative as relative2, resolve as resolve5 } from "node:path";
3274
+ import { existsSync as existsSync13, readFileSync as readFileSync13 } from "node:fs";
3275
+ import { join as join15, relative as relative2, resolve as resolve6 } from "node:path";
3114
3276
  var EXTENSIONS2 = ["", ".ts", ".tsx", ".js", ".jsx", ".mts", ".cts", "/index.ts", "/index.js"];
3115
3277
  function pathEntries(workspaceRoot) {
3116
3278
  const file = join15(workspaceRoot, "tsconfig.base.json");
3117
- if (!existsSync12(file)) return [];
3279
+ if (!existsSync13(file)) return [];
3118
3280
  let paths;
3119
3281
  try {
3120
3282
  paths = parseJsonc(
3121
- readFileSync12(file, "utf8"),
3283
+ readFileSync13(file, "utf8"),
3122
3284
  "tsconfig.base.json"
3123
3285
  )?.compilerOptions?.paths;
3124
3286
  } catch {
@@ -3131,14 +3293,14 @@ function pathEntries(workspaceRoot) {
3131
3293
  const wildcard = key.endsWith("/*") && target.endsWith("/*");
3132
3294
  entries.push({
3133
3295
  key: wildcard ? key.slice(0, -2) : key,
3134
- target: resolve5(workspaceRoot, wildcard ? target.slice(0, -2) : target),
3296
+ target: resolve6(workspaceRoot, wildcard ? target.slice(0, -2) : target),
3135
3297
  wildcard
3136
3298
  });
3137
3299
  }
3138
3300
  return entries.sort((left, right) => right.target.length - left.target.length);
3139
3301
  }
3140
3302
  function workspaceSpecifier(workspaceRoot, file) {
3141
- const absolute = resolve5(file);
3303
+ const absolute = resolve6(file);
3142
3304
  for (const entry of pathEntries(workspaceRoot)) {
3143
3305
  if (!entry.wildcard) {
3144
3306
  if (EXTENSIONS2.some((extension) => `${entry.target}${extension}` === absolute)) {
@@ -3169,7 +3331,7 @@ function importable(written, fileName, workspaceRoot) {
3169
3331
  if (!written.startsWith(".")) return written;
3170
3332
  const fileDir = dirname6(fileName);
3171
3333
  const moduleRoot = findModuleRoot(fileDir, workspaceRoot) ?? fileDir;
3172
- const target = resolve6(fileDir, written);
3334
+ const target = resolve7(fileDir, written);
3173
3335
  if (target.startsWith(`${moduleRoot}/`)) return written;
3174
3336
  if (workspaceRoot === void 0) return void 0;
3175
3337
  return workspaceSpecifier(workspaceRoot, target);
@@ -3454,10 +3616,11 @@ function migrateTestFile(fileName, source, defaultImported = /* @__PURE__ */ new
3454
3616
  }
3455
3617
 
3456
3618
  // src/roles/test/codemod/test-support-files.ts
3457
- import { existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
3458
- import { dirname as dirname8, join as join16, relative as relative3, resolve as resolve7 } from "node:path";
3619
+ import { existsSync as existsSync14, readFileSync as readFileSync14, readdirSync as readdirSync3, statSync as statSync3 } from "node:fs";
3620
+ import { dirname as dirname8, join as join16, relative as relative3, resolve as resolve8 } from "node:path";
3621
+ var THE_SHIM_CANNOT_ANSWER = [/\brequire\s*\(/, /\bjest\.(?:fn|Mock|SpyInstance)\s*</];
3459
3622
  var SKIPPED = /* @__PURE__ */ new Set(["node_modules", "dist", "coverage", ".git", ".vite"]);
3460
- var TEST_FILE2 = /\.(spec|test)\.[cm]?[jt]sx?$/;
3623
+ var TEST_FILE2 = /[.-](spec|test)\.[cm]?[jt]sx?$/;
3461
3624
  var SOURCE_FILE = /\.[cm]?[jt]sx?$/;
3462
3625
  var EXTENSIONS3 = [".ts", ".tsx", ".js", ".jsx", ".mts", ".cts"];
3463
3626
  function relativeSpecifiers2(source) {
@@ -3474,14 +3637,14 @@ function relativeSpecifiers2(source) {
3474
3637
  return found;
3475
3638
  }
3476
3639
  function resolveSpecifier(fromFile, specifier) {
3477
- const base = resolve7(dirname8(fromFile), specifier);
3478
- if (existsSync13(base) && statSync3(base).isFile()) return base;
3640
+ const base = resolve8(dirname8(fromFile), specifier);
3641
+ if (existsSync14(base) && statSync3(base).isFile()) return base;
3479
3642
  for (const extension of EXTENSIONS3) {
3480
- if (existsSync13(`${base}${extension}`)) return `${base}${extension}`;
3643
+ if (existsSync14(`${base}${extension}`)) return `${base}${extension}`;
3481
3644
  }
3482
3645
  for (const extension of EXTENSIONS3) {
3483
3646
  const index = join16(base, `index${extension}`);
3484
- if (existsSync13(index)) return index;
3647
+ if (existsSync14(index)) return index;
3485
3648
  }
3486
3649
  return void 0;
3487
3650
  }
@@ -3508,7 +3671,7 @@ function moduleSources(cwd) {
3508
3671
  }
3509
3672
  function read(path) {
3510
3673
  try {
3511
- return readFileSync13(path, "utf8");
3674
+ return readFileSync14(path, "utf8");
3512
3675
  } catch {
3513
3676
  return void 0;
3514
3677
  }
@@ -3517,10 +3680,10 @@ function setupFilesInside(cwd, named) {
3517
3680
  const kept = [];
3518
3681
  for (const raw of named) {
3519
3682
  const withoutToken = raw.replaceAll("<rootDir>/", "").replaceAll("<rootDir>", ".");
3520
- const full = resolve7(cwd, withoutToken);
3683
+ const full = resolve8(cwd, withoutToken);
3521
3684
  const inside = relative3(cwd, full);
3522
3685
  if (inside.startsWith("..") || inside === "") continue;
3523
- if (!existsSync13(full)) continue;
3686
+ if (!existsSync14(full)) continue;
3524
3687
  kept.push(inside.replaceAll("\\", "/"));
3525
3688
  }
3526
3689
  return kept;
@@ -3555,18 +3718,18 @@ function listTestSupportFiles(cwd, setupFiles = []) {
3555
3718
  queue.push(target);
3556
3719
  }
3557
3720
  }
3558
- const reachable = [...testOnly].filter((file) => /\brequire\s*\(/.test(contents.get(file) ?? "")).map((file) => relative3(cwd, file).replaceAll("\\", "/"));
3721
+ const reachable = [...testOnly].filter((file) => THE_SHIM_CANNOT_ANSWER.some((it) => it.test(contents.get(file) ?? ""))).map((file) => relative3(cwd, file).replaceAll("\\", "/"));
3559
3722
  return [.../* @__PURE__ */ new Set([...reachable, ...setupFilesInside(cwd, setupFiles)])].sort();
3560
3723
  }
3561
3724
 
3562
3725
  // src/roles/test/codemod/migrate-module.ts
3563
- function parse3(source) {
3564
- const { program, errors } = parseSync8("file.ts", source, { sourceType: "module" });
3726
+ function parse3(fileName, source) {
3727
+ const { program, errors } = parseSync8(fileName, source, { sourceType: "module" });
3565
3728
  return errors.length > 0 ? void 0 : program;
3566
3729
  }
3567
3730
  function read2(path) {
3568
3731
  try {
3569
- return readFileSync14(path, "utf8");
3732
+ return readFileSync15(path, "utf8");
3570
3733
  } catch {
3571
3734
  return void 0;
3572
3735
  }
@@ -3604,8 +3767,14 @@ function defaultImportedSpecifiers(cwd) {
3604
3767
  walk2(cwd);
3605
3768
  return found;
3606
3769
  }
3770
+ function declaredInheritedMocks(fileName, source, inherited) {
3771
+ if (inherited.length === 0) return source;
3772
+ const program = parse3(fileName, source);
3773
+ return program === void 0 ? source : declareInheritedAutomocks(source, program, inherited);
3774
+ }
3607
3775
  function migrateModuleTests(cwd, setupFiles = []) {
3608
3776
  const modulesOwnMsw = ownsMswLifecycle(cwd, setupFiles);
3777
+ const inherited = inheritedAutomocks(cwd, setupFiles, parse3);
3609
3778
  const operations = [];
3610
3779
  const blockers = [];
3611
3780
  const collected = [];
@@ -3640,8 +3809,8 @@ function migrateModuleTests(cwd, setupFiles = []) {
3640
3809
  const renamedTo = vitestSupportName(relative6);
3641
3810
  if (!migrated.changed && renamedTo === relative6) continue;
3642
3811
  if (renamedTo !== relative6) operations.push({ kind: "delete", path: relative6 });
3643
- const before = parse3(source);
3644
- const after = parse3(migrated.source);
3812
+ const before = parse3(join17(cwd, relative6), source);
3813
+ const after = parse3(join17(cwd, relative6), migrated.source);
3645
3814
  if (before !== void 0 && after !== void 0) {
3646
3815
  const wasAsync = asyncFunctionNames(before);
3647
3816
  for (const name of asyncFunctionNames(after)) {
@@ -3680,14 +3849,19 @@ function migrateModuleTests(cwd, setupFiles = []) {
3680
3849
  files++;
3681
3850
  renamed += migrated.renamed;
3682
3851
  if (migrated.hoisted.length > 0) hoisted++;
3683
- operations.push({ kind: "write", path: relative6, contents: migrated.source });
3852
+ const withInheritedMocks = declaredInheritedMocks(
3853
+ join17(cwd, relative6),
3854
+ migrated.source,
3855
+ inherited
3856
+ );
3857
+ operations.push({ kind: "write", path: relative6, contents: withInheritedMocks });
3684
3858
  }
3685
3859
  const warnings = [...new Set(collected)];
3686
3860
  return blockers.length > 0 ? { operations: [], files, renamed, hoisted, blockers, warnings } : { operations, files, renamed, hoisted, blockers: [], warnings };
3687
3861
  }
3688
3862
 
3689
3863
  // src/roles/test/nest/generate-config.ts
3690
- import { readdirSync as readdirSync5, readFileSync as readFileSync15, statSync as statSync5 } from "node:fs";
3864
+ import { readdirSync as readdirSync5, readFileSync as readFileSync16, statSync as statSync5 } from "node:fs";
3691
3865
  import { join as join18, relative as relative4 } from "node:path";
3692
3866
  var NEST_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/nest";
3693
3867
  var REACT_TEST_PRESET_SPECIFIER = "@hublo/sentinel/test/react";
@@ -3748,7 +3922,7 @@ function readModuleTraits(cwd) {
3748
3922
  if (!/\.tsx?$/.test(entry)) continue;
3749
3923
  let source;
3750
3924
  try {
3751
- source = readFileSync15(path, "utf8");
3925
+ source = readFileSync16(path, "utf8");
3752
3926
  } catch {
3753
3927
  continue;
3754
3928
  }
@@ -3883,7 +4057,7 @@ ${overrides.join("\n")}
3883
4057
  }
3884
4058
 
3885
4059
  // src/roles/test/nest/read-jest-config.ts
3886
- import { existsSync as existsSync14, readFileSync as readFileSync16 } from "node:fs";
4060
+ import { existsSync as existsSync15, readFileSync as readFileSync17 } from "node:fs";
3887
4061
  import { join as join19 } from "node:path";
3888
4062
  var CARRIED = [
3889
4063
  "testEnvironment",
@@ -4074,7 +4248,7 @@ function extendedConfig(source, cwd) {
4074
4248
  (name) => name.replace(/^\.\//, "")
4075
4249
  );
4076
4250
  const file = candidates.find(
4077
- (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync14(join19(cwd, name))
4251
+ (name) => /^jest[\w.-]*\.(ts|js)$/.test(name) && existsSync15(join19(cwd, name))
4078
4252
  );
4079
4253
  if (file === void 0) continue;
4080
4254
  if (!new RegExp(String.raw`\{\s*\.\.\.${binding}\b`).test(source)) continue;
@@ -4101,11 +4275,11 @@ function sideEffects(source, extended) {
4101
4275
  return found;
4102
4276
  }
4103
4277
  function readJestConfig(cwd, named) {
4104
- const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync14(join19(cwd, name)));
4105
- if (file === void 0 || !existsSync14(join19(cwd, file))) return void 0;
4278
+ const file = named ?? JEST_CONFIG_FILES.find((name) => existsSync15(join19(cwd, name)));
4279
+ if (file === void 0 || !existsSync15(join19(cwd, file))) return void 0;
4106
4280
  let source;
4107
4281
  try {
4108
- source = readFileSync16(join19(cwd, file), "utf8");
4282
+ source = readFileSync17(join19(cwd, file), "utf8");
4109
4283
  } catch {
4110
4284
  return { file, refusals: [`${file} could not be read`], notes: [] };
4111
4285
  }
@@ -4186,7 +4360,7 @@ function readJestConfig(cwd, named) {
4186
4360
  }
4187
4361
 
4188
4362
  // src/roles/test/nest/run-modes.ts
4189
- import { existsSync as existsSync15 } from "node:fs";
4363
+ import { existsSync as existsSync16 } from "node:fs";
4190
4364
  import { basename as basename2, join as join20 } from "node:path";
4191
4365
  var JEST_EXECUTOR = "@nx/jest:jest";
4192
4366
  var PRIMARY_CONFIG_FILE = "vitest.config.mts";
@@ -4208,7 +4382,7 @@ function runModes(cwd, fallback) {
4208
4382
  const jestConfig = entry.options?.jestConfig;
4209
4383
  if (jestConfig === void 0) continue;
4210
4384
  const name = configName(jestConfig);
4211
- if (!existsSync15(join20(cwd, name))) continue;
4385
+ if (!existsSync16(join20(cwd, name))) continue;
4212
4386
  declared.push({
4213
4387
  target,
4214
4388
  jestConfig: name,
@@ -4233,11 +4407,11 @@ function runModes(cwd, fallback) {
4233
4407
  }
4234
4408
 
4235
4409
  // src/roles/test/written-file-formatter.ts
4236
- import { existsSync as existsSync17 } from "node:fs";
4410
+ import { existsSync as existsSync18 } from "node:fs";
4237
4411
  import { dirname as dirname10, join as join23 } from "node:path";
4238
4412
 
4239
4413
  // src/roles/format/read-adoption.ts
4240
- import { existsSync as existsSync16, readFileSync as readFileSync17 } from "node:fs";
4414
+ import { existsSync as existsSync17, readFileSync as readFileSync18 } from "node:fs";
4241
4415
  import { join as join21 } from "node:path";
4242
4416
 
4243
4417
  // src/roles/format/config-policy.ts
@@ -4345,10 +4519,10 @@ function sameValue(a, b) {
4345
4519
  }
4346
4520
  function readFormatAdoption(cwd) {
4347
4521
  const path = join21(cwd, FORMAT_CONFIG_FILE);
4348
- if (!existsSync16(path)) return NOT_ADOPTED(null);
4522
+ if (!existsSync17(path)) return NOT_ADOPTED(null);
4349
4523
  let parsed;
4350
4524
  try {
4351
- parsed = parseJsonc(readFileSync17(path, "utf8"), FORMAT_CONFIG_FILE);
4525
+ parsed = parseJsonc(readFileSync18(path, "utf8"), FORMAT_CONFIG_FILE);
4352
4526
  } catch (error) {
4353
4527
  const reason = error instanceof Error ? error.message : String(error);
4354
4528
  return NOT_ADOPTED(FORMAT_CONFIG_FILE, `${FORMAT_CONFIG_FILE} could not be parsed (${reason})`);
@@ -4392,7 +4566,7 @@ function readFormatAdoption(cwd) {
4392
4566
  }
4393
4567
 
4394
4568
  // src/roles/format/resolve-oxfmt.ts
4395
- import { readFileSync as readFileSync18 } from "node:fs";
4569
+ import { readFileSync as readFileSync19 } from "node:fs";
4396
4570
  import { createRequire as createRequire4 } from "node:module";
4397
4571
  import { dirname as dirname9, join as join22 } from "node:path";
4398
4572
  function resolveOxfmt(cwd) {
@@ -4424,7 +4598,7 @@ function readConfigSchema() {
4424
4598
  try {
4425
4599
  const manifest = createRequire4(import.meta.url).resolve("oxfmt/package.json");
4426
4600
  const schemaPath = join22(dirname9(manifest), "configuration_schema.json");
4427
- return JSON.parse(readFileSync18(schemaPath, "utf8"));
4601
+ return JSON.parse(readFileSync19(schemaPath, "utf8"));
4428
4602
  } catch {
4429
4603
  return void 0;
4430
4604
  }
@@ -4449,7 +4623,7 @@ function prettierConfigFrom(cwd) {
4449
4623
  let dir = cwd;
4450
4624
  for (; ; ) {
4451
4625
  for (const name of PRETTIER_CONFIGS) {
4452
- if (existsSync17(join23(dir, name))) return join23(dir, name);
4626
+ if (existsSync18(join23(dir, name))) return join23(dir, name);
4453
4627
  }
4454
4628
  const parent = dirname10(dir);
4455
4629
  if (parent === dir) return void 0;
@@ -4480,11 +4654,11 @@ var SPEC_TSCONFIG = "tsconfig.spec.json";
4480
4654
  var VITEST_GLOBAL_TYPES = "vitest/globals";
4481
4655
  function specTypesOperation(cwd) {
4482
4656
  const path = join24(cwd, SPEC_TSCONFIG);
4483
- if (!existsSync18(path)) return void 0;
4657
+ if (!existsSync19(path)) return void 0;
4484
4658
  let config;
4485
4659
  try {
4486
4660
  config = parseJsonc(
4487
- readFileSync19(path, "utf8"),
4661
+ readFileSync20(path, "utf8"),
4488
4662
  SPEC_TSCONFIG
4489
4663
  );
4490
4664
  } catch {
@@ -4506,7 +4680,7 @@ function shippedTypesPath(cwd, declaration) {
4506
4680
  try {
4507
4681
  const manifest = createRequire5(join24(cwd, "noop.cjs")).resolve("@hublo/sentinel/package.json");
4508
4682
  const file = join24(dirname11(manifest), declaration);
4509
- if (existsSync18(file)) {
4683
+ if (existsSync19(file)) {
4510
4684
  const path = relative5(cwd, file);
4511
4685
  return path.startsWith(".") ? path : `./${path}`;
4512
4686
  }
@@ -4535,7 +4709,7 @@ function helpersStillCallingJest(cwd) {
4535
4709
  if (/^(jest|vitest)\./.test(entry.name)) continue;
4536
4710
  let source;
4537
4711
  try {
4538
- source = readFileSync19(path, "utf8");
4712
+ source = readFileSync20(path, "utf8");
4539
4713
  } catch {
4540
4714
  continue;
4541
4715
  }
@@ -4572,7 +4746,7 @@ function tsconfigFileListOperations(cwd) {
4572
4746
  let config;
4573
4747
  try {
4574
4748
  config = parseJsonc(
4575
- readFileSync19(join24(cwd, name), "utf8"),
4749
+ readFileSync20(join24(cwd, name), "utf8"),
4576
4750
  name
4577
4751
  );
4578
4752
  } catch {
@@ -4874,11 +5048,11 @@ function plan(context) {
4874
5048
  }
4875
5049
 
4876
5050
  // src/roles/test/written-file-linter.ts
4877
- import { existsSync as existsSync23 } from "node:fs";
5051
+ import { existsSync as existsSync24 } from "node:fs";
4878
5052
  import { join as join29 } from "node:path";
4879
5053
 
4880
5054
  // src/roles/lint/module-baseline.ts
4881
- import { existsSync as existsSync19, readFileSync as readFileSync20 } from "node:fs";
5055
+ import { existsSync as existsSync20, readFileSync as readFileSync21 } from "node:fs";
4882
5056
  import { join as join25 } from "node:path";
4883
5057
  var LINT_BASELINE_FILE = ".oxlintrc.baseline.json";
4884
5058
  var LINT_MEASURE_FILE = ".oxlintrc.measure.json";
@@ -4924,10 +5098,10 @@ function describeHolds(holds) {
4924
5098
  }
4925
5099
  function heldRules(cwd) {
4926
5100
  const path = join25(cwd, LINT_BASELINE_FILE);
4927
- if (!existsSync19(path)) return [];
5101
+ if (!existsSync20(path)) return [];
4928
5102
  try {
4929
5103
  const parsed = parseJsonc(
4930
- readFileSync20(path, "utf8"),
5104
+ readFileSync21(path, "utf8"),
4931
5105
  LINT_BASELINE_FILE
4932
5106
  );
4933
5107
  return Object.keys(parsed.rules ?? {});
@@ -4976,7 +5150,7 @@ function lintTarget() {
4976
5150
  }
4977
5151
 
4978
5152
  // src/core/config/read-adoption.ts
4979
- import { existsSync as existsSync21, readFileSync as readFileSync22 } from "node:fs";
5153
+ import { existsSync as existsSync22, readFileSync as readFileSync23 } from "node:fs";
4980
5154
  import { join as join27 } from "node:path";
4981
5155
 
4982
5156
  // src/core/config/owned-keys.ts
@@ -4992,12 +5166,12 @@ function localOnlyKeys(config, permitted, presetSets) {
4992
5166
  }
4993
5167
 
4994
5168
  // src/core/config/resolve-config-target.ts
4995
- import { existsSync as existsSync20, readFileSync as readFileSync21 } from "node:fs";
5169
+ import { existsSync as existsSync21, readFileSync as readFileSync22 } from "node:fs";
4996
5170
  import { join as join26 } from "node:path";
4997
5171
  function readExtends(absolutePath) {
4998
5172
  let parsed;
4999
5173
  try {
5000
- parsed = parseJsonc(readFileSync21(absolutePath, "utf8"), absolutePath);
5174
+ parsed = parseJsonc(readFileSync22(absolutePath, "utf8"), absolutePath);
5001
5175
  } catch {
5002
5176
  return [];
5003
5177
  }
@@ -5012,7 +5186,7 @@ function resolveConfigTarget(moduleDir, { candidates, markers, fallback }) {
5012
5186
  let existingExtendsSomething = false;
5013
5187
  for (const candidate of candidates) {
5014
5188
  const absolutePath = join26(moduleDir, candidate);
5015
- if (!existsSync20(absolutePath)) continue;
5189
+ if (!existsSync21(absolutePath)) continue;
5016
5190
  const chain = readExtends(absolutePath);
5017
5191
  if (existing === void 0) {
5018
5192
  existing = candidate;
@@ -5045,12 +5219,12 @@ var NOT_ADOPTED2 = (configFile, unreadable = null) => ({
5045
5219
  });
5046
5220
  function readAdoption(cwd, options) {
5047
5221
  const target = resolveConfigTarget(cwd, options);
5048
- if (target.reason === "none" || !existsSync21(join27(cwd, target.path))) {
5222
+ if (target.reason === "none" || !existsSync22(join27(cwd, target.path))) {
5049
5223
  return NOT_ADOPTED2(target.reason === "none" ? null : target.path);
5050
5224
  }
5051
5225
  let parsed;
5052
5226
  try {
5053
- parsed = parseJsonc(readFileSync22(join27(cwd, target.path), "utf8"), target.path);
5227
+ parsed = parseJsonc(readFileSync23(join27(cwd, target.path), "utf8"), target.path);
5054
5228
  } catch (error) {
5055
5229
  const reason = error instanceof Error ? error.message : String(error);
5056
5230
  return NOT_ADOPTED2(target.path, `${target.path} could not be parsed (${reason})`);
@@ -5084,7 +5258,7 @@ function readLintAdoption(cwd) {
5084
5258
  }
5085
5259
 
5086
5260
  // src/roles/lint/resolve-oxlint.ts
5087
- import { existsSync as existsSync22 } from "node:fs";
5261
+ import { existsSync as existsSync23 } from "node:fs";
5088
5262
  import { createRequire as createRequire6 } from "node:module";
5089
5263
  import { delimiter as delimiter2, dirname as dirname12, join as join28 } from "node:path";
5090
5264
  import { fileURLToPath } from "node:url";
@@ -5122,7 +5296,7 @@ function tsgolintShim(cwd) {
5122
5296
  } catch {
5123
5297
  candidates.push(resolveBin(dirname12(fileURLToPath(import.meta.url)), "tsgolint") ?? "");
5124
5298
  }
5125
- return candidates.find((candidate) => candidate !== "" && existsSync22(candidate));
5299
+ return candidates.find((candidate) => candidate !== "" && existsSync23(candidate));
5126
5300
  }
5127
5301
  function oxlintSearchPath(cwd) {
5128
5302
  return binSearchPath(cwd);
@@ -5136,7 +5310,7 @@ function writtenFileLinter(cwd) {
5136
5310
  return { bin: oxlint, args: ["--fix"], origin: "this module's own oxlint config" };
5137
5311
  }
5138
5312
  }
5139
- const config = ESLINT_CONFIG_FILES.map((name) => join29(cwd, name)).find((path) => existsSync23(path));
5313
+ const config = ESLINT_CONFIG_FILES.map((name) => join29(cwd, name)).find((path) => existsSync24(path));
5140
5314
  const eslint = resolveBin(cwd, "eslint");
5141
5315
  if (config !== void 0 && eslint !== void 0) {
5142
5316
  return {
@@ -5356,7 +5530,7 @@ function fromWorkspaceRoot(moduleDir, options, paths) {
5356
5530
  if (root === void 0 || root === moduleDir) {
5357
5531
  return { cwd: moduleDir, options: [...options], paths: [...paths] };
5358
5532
  }
5359
- const absolute = (value) => isAbsolute2(value) ? value : resolve8(moduleDir, value);
5533
+ const absolute = (value) => isAbsolute2(value) ? value : resolve9(moduleDir, value);
5360
5534
  const moved = [];
5361
5535
  let previousWasConfig = false;
5362
5536
  for (const option of options) {
@@ -53,7 +53,7 @@ import {
53
53
  selfCommand,
54
54
  splitToolArgs,
55
55
  walk
56
- } from "./chunk-AT5AEZ7M.js";
56
+ } from "./chunk-TDQGNNCV.js";
57
57
  import {
58
58
  parseJsonc
59
59
  } from "./chunk-QXFCZON7.js";
package/dist/index.js CHANGED
@@ -6,10 +6,10 @@ import {
6
6
  registerAdapters,
7
7
  resolve,
8
8
  setDefaultRunner
9
- } from "./chunk-CTF6XMW4.js";
9
+ } from "./chunk-USVNSL76.js";
10
10
  import {
11
11
  BaseAdapter
12
- } from "./chunk-AT5AEZ7M.js";
12
+ } from "./chunk-TDQGNNCV.js";
13
13
  import "./chunk-O7REVMOC.js";
14
14
  import "./chunk-QXFCZON7.js";
15
15
  import "./chunk-NW6UHNYX.js";
@@ -156,6 +156,13 @@ function installJestRejectedFunction() {
156
156
  }
157
157
  }
158
158
 
159
+ // src/roles/test/setup/worker-id.ts
160
+ function installWorkerId(env) {
161
+ if (env.JEST_WORKER_ID !== void 0) return;
162
+ if (env.VITEST_POOL_ID === void 0) return;
163
+ env.JEST_WORKER_ID = env.VITEST_POOL_ID;
164
+ }
165
+
159
166
  // src/roles/test/setup/jest-parity.ts
160
167
  installJestErrorEquality(expect);
161
168
  installJestMockReset(vi);
@@ -164,5 +171,6 @@ installDeepMockReset(vi);
164
171
  installJestFakeTimerOptions(vi);
165
172
  installJestRejectedFunction();
166
173
  installJestGlobal(vi);
174
+ installWorkerId(process.env);
167
175
  await installWorkspaceSetup();
168
176
  //# sourceMappingURL=jest-parity.js.map
@@ -1 +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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-constructor-semantics')) return\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","import { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'deep-mock-reset')) return\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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `expect` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(expect, 'jest-error-equality')) return\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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-fake-timer-options')) return\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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-mock-reset')) return\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
+ {"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","../../../../src/roles/test/setup/worker-id.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 { installWorkerId } from './worker-id.js'\nimport { installWorkspaceSetup } from './workspace.js'\n\ninstallJestErrorEquality(expect)\ninstallJestMockReset(vi)\ninstallJestConstructorSemantics(vi)\ninstallDeepMockReset(vi)\ninstallJestFakeTimerOptions(vi)\ninstallJestRejectedFunction()\ninstallJestGlobal(vi)\ninstallWorkerId(process.env)\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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-constructor-semantics')) return\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","import { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'deep-mock-reset')) return\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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `expect` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(expect, 'jest-error-equality')) return\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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-fake-timer-options')) return\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\nimport { alreadyInstalled } from './install-once.js'\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 // A setup file runs once per TEST FILE, and without isolation `vi` is the worker's\n // single object, so a second pass would wrap the first pass's wrapper.\n if (alreadyInstalled(vi, 'jest-mock-reset')) return\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","/**\n * `JEST_WORKER_ID`, which jest set and Vitest does not, and which suites use to stop sharing.\n *\n * ## What breaks without it, and how quietly\n *\n * A suite that needs a resource of its own per worker derives its name from this variable. In this\n * repository:\n *\n * const workerId = process.env.JEST_WORKER_ID ?? '1'\n *\n * Under jest that is 1, 2, 3, so each worker gets `recruitment_test_1`, `_2`, `_3` and nobody\n * treads on anybody. Under Vitest the variable does not exist, every worker takes the `'1'`\n * fallback, and they all write to ONE database while truncating it under each other.\n *\n * Measured on `apps/nest/microservices/recruitment` in the whole-monorepo sweep: 143 of 1228\n * functional tests failed, and not on assertions. `Foreign key constraint violated`,\n * `Unique constraint failed`, and PostgreSQL `40P01 deadlock detected` with two processes waiting\n * on each other's locks. Every log line said `Using database: recruitment_test_1`, whatever the\n * worker. The same suite is green on its own branch, because there it is the only thing running.\n *\n * Nothing in the migration mentions this variable, which is what makes it the migration's problem:\n * a suite that did not share a database now shares one, and says so only through the failures of\n * whichever test lost the race.\n *\n * ## Why `VITEST_POOL_ID` and not `VITEST_WORKER_ID`\n *\n * Measured, because the two look interchangeable and are not. With 8 files and `maxWorkers: 2`:\n *\n * VITEST_POOL_ID 1, 2 bounded by the worker count, 1-based\n * VITEST_WORKER_ID 0, 1, 2, 3, 4, 5... one per file, 0-based\n *\n * `JEST_WORKER_ID` is bounded and 1-based, so `VITEST_POOL_ID` is its counterpart. Taking the\n * other one would turn \"one database per worker\" into one database per FILE, and a 0 would collide\n * with nothing but read as falsy in code that tests the variable rather than compares it.\n */\n\n/** The part of the environment this touches. Node's own types are not needed to say it. */\ninterface Environment {\n JEST_WORKER_ID?: string\n VITEST_POOL_ID?: string\n}\n\n/**\n * Point `JEST_WORKER_ID` at Vitest's pool id, unless something already set it.\n *\n * Set from a SETUP file, which runs before the test file's module graph is evaluated, because a\n * module reading the variable at import time would otherwise capture the fallback.\n */\nexport function installWorkerId(env: Environment): void {\n if (env.JEST_WORKER_ID !== undefined) return\n if (env.VITEST_POOL_ID === undefined) return\n\n env.JEST_WORKER_ID = env.VITEST_POOL_ID\n}\n"],"mappings":"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","names":["vi","vi","expect","vi","vi","vi"]}
@@ -7,7 +7,7 @@ import {
7
7
  readTestAdoption,
8
8
  resolveVitest,
9
9
  suiteOf
10
- } from "./chunk-AT5AEZ7M.js";
10
+ } from "./chunk-TDQGNNCV.js";
11
11
  import "./chunk-O7REVMOC.js";
12
12
  import "./chunk-QXFCZON7.js";
13
13
  import "./chunk-NW6UHNYX.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hublo/sentinel",
3
- "version": "1.4.0-alpha.39",
3
+ "version": "1.4.0-alpha.40",
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",