@hublo/sentinel 1.4.0-alpha.34 → 1.4.0-alpha.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-TKDVAYYV.js → chunk-DWVRVPMN.js} +4 -11
- package/dist/chunk-DWVRVPMN.js.map +1 -0
- package/dist/chunk-ELZHIN6E.js +16 -0
- package/dist/chunk-ELZHIN6E.js.map +1 -0
- package/dist/chunk-H3GSGAX2.js +11 -0
- package/dist/chunk-H3GSGAX2.js.map +1 -0
- package/dist/{chunk-PWV3BMDA.js → chunk-MMKBDO5V.js} +11 -2
- package/dist/chunk-MMKBDO5V.js.map +1 -0
- package/dist/chunk-NUOQXAYR.js +14 -0
- package/dist/chunk-NUOQXAYR.js.map +1 -0
- package/dist/{chunk-LF7QWHJ4.js → chunk-SOCLCHJK.js} +95 -6
- package/dist/chunk-SOCLCHJK.js.map +1 -0
- package/dist/roles/test/nest/toolchain.js +1 -1
- package/dist/roles/test/react/toolchain.js +1 -1
- package/dist/roles/test/setup/file-boundary-close.d.ts +2 -0
- package/dist/roles/test/setup/file-boundary-close.js +8 -0
- package/dist/roles/test/setup/file-boundary-close.js.map +1 -0
- package/dist/roles/test/setup/file-boundary.d.ts +2 -0
- package/dist/roles/test/setup/file-boundary.js +62 -0
- package/dist/roles/test/setup/file-boundary.js.map +1 -0
- package/dist/roles/test/setup/jest-parity.js +10 -1
- package/dist/roles/test/setup/jest-parity.js.map +1 -1
- package/dist/roles/test/setup/msw-lifecycle.js +2 -1
- package/dist/roles/test/setup/msw-lifecycle.js.map +1 -1
- package/dist/roles/test/setup/msw-server.js +2 -1
- package/dist/roles/test/setup/msw-server.js.map +1 -1
- package/dist/roles/test/setup/workspace-entry.js +2 -1
- package/dist/roles/test/setup/workspace-entry.js.map +1 -1
- package/dist/roles/test/shared-test-config.d.ts +13 -0
- package/dist/roles/test/shared-test-config.js +1 -1
- package/package.json +9 -1
- package/dist/chunk-LF7QWHJ4.js.map +0 -1
- package/dist/chunk-PWV3BMDA.js.map +0 -1
- package/dist/chunk-TKDVAYYV.js.map +0 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fromModule
|
|
3
|
+
} from "./chunk-ELZHIN6E.js";
|
|
1
4
|
import {
|
|
2
5
|
findWorkspaceRoot
|
|
3
6
|
} from "./chunk-NW6UHNYX.js";
|
|
4
7
|
|
|
5
8
|
// src/roles/test/setup/workspace.ts
|
|
6
|
-
import { createRequire } from "node:module";
|
|
7
|
-
import { join } from "node:path";
|
|
8
9
|
function isMissingModule(error) {
|
|
9
10
|
return error?.code === "ERR_MODULE_NOT_FOUND";
|
|
10
11
|
}
|
|
@@ -18,14 +19,6 @@ async function optional(specifier, use) {
|
|
|
18
19
|
if (!isMissingModule(error)) throw error;
|
|
19
20
|
}
|
|
20
21
|
}
|
|
21
|
-
function fromModule(specifier) {
|
|
22
|
-
try {
|
|
23
|
-
const require2 = createRequire(join(process.cwd(), "noop.cjs"));
|
|
24
|
-
return require2(specifier);
|
|
25
|
-
} catch {
|
|
26
|
-
return void 0;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
22
|
async function loadEnvironment() {
|
|
30
23
|
await optional("dotenv-flow", (module) => {
|
|
31
24
|
const flow = module;
|
|
@@ -67,4 +60,4 @@ export {
|
|
|
67
60
|
installWorkspaceSetup,
|
|
68
61
|
pinWorkspaceTimezone
|
|
69
62
|
};
|
|
70
|
-
//# sourceMappingURL=chunk-
|
|
63
|
+
//# sourceMappingURL=chunk-DWVRVPMN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/workspace.ts"],"sourcesContent":["/**\n * The rest of what a migrated suite needs before its tests, loaded DEFENSIVELY.\n *\n * ## Why this is in sentinel and not in the repo\n *\n * Because it is config, and an app must depend on its own config and nothing outside it. The first\n * version of the generated config referenced `'../../../../vitest.setup.mts'`, a relative path\n * climbing four levels to the workspace root — which is exactly the external link this whole\n * migration exists to remove. A module that reaches outside itself cannot move on its own.\n *\n * ## Why loading is conditional, and why that is the point rather than a precaution\n *\n * Unlike the msw lifecycle beside it, nothing here reacts to the RUNNER. `dotenv-flow` and luxon's\n * zone would behave identically under jest, Vitest or `node:test`; what they react to is the REPO,\n * which chose those two packages. So the conditional load is the statement of ownership: sentinel\n * offers the behaviour, and the repo that installed the package decides whether it happens.\n *\n * That is also what keeps sentinel installable elsewhere. Neither package can be a hard dependency,\n * so each is loaded only if present and skipped in silence if not. Elsewhere this file does nothing;\n * here both are present and the behaviour is the one the jest setup had.\n *\n * Silence is the right answer for an absent package and the wrong one for a broken package, so a\n * load that fails for any other reason is re-thrown rather than swallowed.\n */\n\nimport { findWorkspaceRoot } from '../../../core/workspace-prep.js'\n\nimport { fromModule } from './module-copy.js'\n\n/** Was this module absent, as opposed to present and broken? */\nfunction isMissingModule(error: unknown): boolean {\n return (error as { code?: string } | null)?.code === 'ERR_MODULE_NOT_FOUND'\n}\n\n/**\n * Optional by construction: `import()` inside a try, so a missing package is a no-op rather than a\n * failure at load time. A static import would make every one of them a hard dependency.\n */\nasync function optional(specifier: string, use: (module: unknown) => void): Promise<void> {\n try {\n use(await import(/* @vite-ignore */ specifier))\n } catch (error) {\n if (!isMissingModule(error)) throw error\n }\n}\n\n/**\n * `dotenv-flow`, which the root jest setup loaded first.\n *\n * Vite's own `.env` handling does NOT replace it, which was checked before writing this. Measured on\n * Vitest 4 against a fixture holding `.env` and `.env.test`: the cascade is read and `MODE` is\n * `test`, but only `VITE_`-prefixed values are exposed, and only on `import.meta.env`. `process.env`\n * comes back untouched, and the code under test reads unprefixed `process.env`.\n *\n * `purge_dotenv` is carried across: it clears variables a previous load left behind, and without it\n * a value from one run leaks into the next. Not a detail in a suite that asserts on URLs.\n */\nasync function loadEnvironment(): Promise<void> {\n // A plain dependency of sentinel now: it mutates `process.env`, which is global, so WHICH copy\n // runs does not matter. It is the other half of replacing the root setup, and the root is going\n // away (Héla, 2026-09-21: \"mon but est de supprimer la config root\").\n await optional('dotenv-flow', (module) => {\n const flow = module as {\n config?: (options: Record<string, unknown>) => void\n default?: unknown\n }\n const config =\n flow.config ?? (flow.default as { config?: (o: Record<string, unknown>) => void })?.config\n /*\n * From the WORKSPACE ROOT, not from the module.\n *\n * dotenv-flow reads its cascade from the current directory, and the two runners do not share\n * one: jest ran from the repo root, where `.env`, `.env.test` and `.env.token` live, while\n * `sentinel --run --test` runs from the module, where there is nothing to read. Measured on\n * `libs/cloud/events-notifications`: from the module, a service call waits for an endpoint its\n * env never named and the test dies on a 5 s timeout; from the root, it answers in 200 ms.\n *\n * The root is found rather than configured: the setup cannot be handed a value, since a module\n * that overrides `test.env` would replace whatever the preset injected there.\n */\n const workspaceRoot = findWorkspaceRoot(process.cwd())\n config?.({\n silent: true,\n purge_dotenv: true,\n ...(workspaceRoot === undefined ? {} : { path: workspaceRoot }),\n })\n })\n}\n\n/**\n * Luxon's default zone, carried as luxon's own setting and NOT translated to `process.env.TZ`.\n *\n * They are different instructions: one configures luxon, the other the whole process, `Date` and\n * `Intl` included. Swapping them would change what a suite does while claiming to migrate it, and\n * timezone is not a detail here — a machine's zone accounted for a large part of 345 local failures\n * on `host-admin` that did not exist in CI.\n */\nasync function pinLuxonZone(): Promise<void> {\n // The module's copy, deliberately: `Settings` is per-copy state, so sentinel's own luxon is the\n // one package this setup must NOT configure.\n /*\n * Through the RUNNER's resolver first, which is what the tests use. `fromModule` goes through\n * `require`, and a package shipping both builds hands it the CJS one while Vite hands the tests\n * the ESM one. Two copies, two `Settings`, and the pin lands on the one nobody reads: measured on\n * `libs/cloud/events-notifications`, whose suite ran in `Europe/Paris` and compared timestamps\n * two hours apart. The preset aliases luxon to one path so both routes agree.\n */\n let pinned = false\n await optional('luxon', (module) => {\n const settings = (module as { Settings?: { defaultZone: string } }).Settings\n if (settings === undefined) return\n settings.defaultZone = 'utc'\n pinned = true\n })\n if (pinned) return\n\n const own = fromModule('luxon') as { Settings?: { defaultZone: string } } | undefined\n if (own?.Settings !== undefined) {\n own.Settings.defaultZone = 'utc'\n return\n }\n\n // Said out loud rather than skipped: a suite whose dates silently run in the machine's zone is\n // the kind of failure that gets blamed on the migration months later.\n process.stderr.write(\n 'sentinel (test): luxon was not found in this module, so the UTC default zone is NOT pinned. ' +\n 'Dates will follow the machine zone, as they did not under jest.\\n',\n )\n}\n\n/**\n * Everything the workspace needs beyond the msw lifecycle.\n *\n * NOT included: the `jest.mock('dynamoose')` and `jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')`\n * calls the jest setup made. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it, and hoisting it into a shared setup is what makes a test pass for a reason\n * nobody can see. The migration reports them instead, so the module that relies on one declares it.\n */\nexport async function installWorkspaceSetup(): Promise<void> {\n await loadEnvironment()\n}\n\n/**\n * Luxon's default zone, split out because it is not in the same class as the rest.\n *\n * ⚠️ These two were one function until 23/09 and moving them together broke three modules.\n *\n * The `.env` cascade is needed by any suite whose code reads `process.env`, whatever setup its\n * jest config named: under jest those modules got it from a `globalSetup` running in the main\n * process, and Vitest's workers do not inherit it the same way. Removing it cost\n * `libs/front/logic` 23 tests and `libs/front/api` 44, all with `missing env var\n * NEXT_PUBLIC_MONOREPO_BASE_URL`.\n *\n * The UTC pin is a decision ONE file at the workspace root made, and only the modules naming that\n * file ever had it. So it is conditional and the cascade is not.\n */\nexport async function pinWorkspaceTimezone(): Promise<void> {\n await pinLuxonZone()\n}\n"],"mappings":";;;;;;;;AA8BA,SAAS,gBAAgB,OAAyB;AAChD,SAAQ,OAAoC,SAAS;AACvD;AAMA,eAAe,SAAS,WAAmB,KAA+C;AACxF,MAAI;AACF,QAAI,MAAM;AAAA;AAAA,MAA0B;AAAA,KAAU;AAAA,EAChD,SAAS,OAAO;AACd,QAAI,CAAC,gBAAgB,KAAK,EAAG,OAAM;AAAA,EACrC;AACF;AAaA,eAAe,kBAAiC;AAI9C,QAAM,SAAS,eAAe,CAAC,WAAW;AACxC,UAAM,OAAO;AAIb,UAAM,SACJ,KAAK,UAAW,KAAK,SAA+D;AAatF,UAAM,gBAAgB,kBAAkB,QAAQ,IAAI,CAAC;AACrD,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,GAAI,kBAAkB,SAAY,CAAC,IAAI,EAAE,MAAM,cAAc;AAAA,IAC/D,CAAC;AAAA,EACH,CAAC;AACH;AAUA,eAAe,eAA8B;AAU3C,MAAI,SAAS;AACb,QAAM,SAAS,SAAS,CAAC,WAAW;AAClC,UAAM,WAAY,OAAkD;AACpE,QAAI,aAAa,OAAW;AAC5B,aAAS,cAAc;AACvB,aAAS;AAAA,EACX,CAAC;AACD,MAAI,OAAQ;AAEZ,QAAM,MAAM,WAAW,OAAO;AAC9B,MAAI,KAAK,aAAa,QAAW;AAC/B,QAAI,SAAS,cAAc;AAC3B;AAAA,EACF;AAIA,UAAQ,OAAO;AAAA,IACb;AAAA,EAEF;AACF;AAUA,eAAsB,wBAAuC;AAC3D,QAAM,gBAAgB;AACxB;AAgBA,eAAsB,uBAAsC;AAC1D,QAAM,aAAa;AACrB;","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/roles/test/setup/module-copy.ts
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function fromModule(specifier) {
|
|
5
|
+
try {
|
|
6
|
+
const require2 = createRequire(join(process.cwd(), "noop.cjs"));
|
|
7
|
+
return require2(specifier);
|
|
8
|
+
} catch {
|
|
9
|
+
return void 0;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
fromModule
|
|
15
|
+
};
|
|
16
|
+
//# sourceMappingURL=chunk-ELZHIN6E.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/module-copy.ts"],"sourcesContent":["/**\n * Reaching the MODULE's copy of a package rather than sentinel's.\n *\n * Under pnpm, an import written inside sentinel resolves from sentinel's own directory, so it sees\n * only sentinel's dependencies. That is right for anything whose effect is GLOBAL, and wrong for\n * anything whose effect is per-copy. Measured 2026-09-21: neither `luxon` nor `dotenv-flow`\n * resolves from sentinel while both resolve from the module under test, so a silent optional\n * import skipped both and a migrated suite lost the `.env` cascade and the UTC pin without a word.\n *\n * Extracted from `./workspace.js`, which found the problem, so the second caller\n * (`./file-boundary.js`, reaching for the module's Testing Library) does not rediscover it.\n */\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\n\n/** The module's own copy of `specifier`, or `undefined` when it does not have one. */\nexport function fromModule(specifier: string): unknown | undefined {\n try {\n const require = createRequire(join(process.cwd(), 'noop.cjs'))\n return require(specifier)\n } catch {\n return undefined\n }\n}\n"],"mappings":";AAYA,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AAGd,SAAS,WAAW,WAAwC;AACjE,MAAI;AACF,UAAMA,WAAU,cAAc,KAAK,QAAQ,IAAI,GAAG,UAAU,CAAC;AAC7D,WAAOA,SAAQ,SAAS;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;","names":["require"]}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// src/roles/test/setup/file-boundary-state.ts
|
|
2
|
+
var CURRENT_FILE_MOCKS = /* @__PURE__ */ Symbol.for("@hublo/sentinel/test/current-file-mocks");
|
|
3
|
+
var PHASE = /* @__PURE__ */ Symbol.for("@hublo/sentinel/test/boundary-phase");
|
|
4
|
+
var SHARED_WORKER = /* @__PURE__ */ Symbol.for("@hublo/sentinel/test/shared-worker");
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
CURRENT_FILE_MOCKS,
|
|
8
|
+
PHASE,
|
|
9
|
+
SHARED_WORKER
|
|
10
|
+
};
|
|
11
|
+
//# sourceMappingURL=chunk-H3GSGAX2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/file-boundary-state.ts"],"sourcesContent":["/**\n * The three keys `./file-boundary.js` and `./file-boundary-close.js` share.\n *\n * They live on `globalThis` rather than in this module's scope for one reason: with isolation off\n * a module is evaluated once per worker, but with isolation ON it is re-evaluated per file, and\n * the two halves of the boundary must find the same state in both cases. `Symbol.for` keeps that\n * true even if pnpm's layout hands the two halves different copies of this file.\n */\n\n/**\n * Mocks the test file currently running built, and the only thing the global reset APIs act on.\n *\n * Mocks a SETUP file built are not recorded at all rather than recorded separately: nothing needs\n * to iterate them, and a setup file runs per test file, so keeping them grew without bound.\n */\nexport const CURRENT_FILE_MOCKS = Symbol.for('@hublo/sentinel/test/current-file-mocks')\n\n/** `'infrastructure'` while the setup files run, `'file'` from the test file onwards. */\nexport const PHASE = Symbol.for('@hublo/sentinel/test/boundary-phase')\n\n/**\n * Set by `./file-boundary.js`, which the preset installs only when a suite shares a worker.\n *\n * Read by anything whose lifecycle differs between the two, msw's `close()` being the first: its\n * boundary is the worker, and only per-file isolation ever made the worker and the file the same\n * thing.\n */\nexport const SHARED_WORKER = Symbol.for('@hublo/sentinel/test/shared-worker')\n"],"mappings":";AAeO,IAAM,qBAAqB,uBAAO,IAAI,yCAAyC;AAG/E,IAAM,QAAQ,uBAAO,IAAI,qCAAqC;AAS9D,IAAM,gBAAgB,uBAAO,IAAI,oCAAoC;","names":[]}
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SHARED_WORKER
|
|
3
|
+
} from "./chunk-H3GSGAX2.js";
|
|
4
|
+
|
|
1
5
|
// src/roles/test/setup/msw.ts
|
|
2
6
|
import { setupServer } from "msw/node";
|
|
3
7
|
var server = setupServer();
|
|
@@ -5,11 +9,16 @@ var LISTEN_OPTIONS = { onUnhandledRequest: "error" };
|
|
|
5
9
|
function installMswLifecycle(hooks) {
|
|
6
10
|
server.listen(LISTEN_OPTIONS);
|
|
7
11
|
hooks.beforeAll(() => server.listen(LISTEN_OPTIONS));
|
|
8
|
-
hooks.afterAll(() =>
|
|
12
|
+
hooks.afterAll(() => {
|
|
13
|
+
if (!sharesAWorker()) server.close();
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function sharesAWorker() {
|
|
17
|
+
return globalThis[SHARED_WORKER] === true;
|
|
9
18
|
}
|
|
10
19
|
|
|
11
20
|
export {
|
|
12
21
|
server,
|
|
13
22
|
installMswLifecycle
|
|
14
23
|
};
|
|
15
|
-
//# sourceMappingURL=chunk-
|
|
24
|
+
//# sourceMappingURL=chunk-MMKBDO5V.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/msw.ts"],"sourcesContent":["/**\n * The HTTP interception a test suite runs against, owned here rather than by each module.\n *\n * ## Why sentinel ships msw at all\n *\n * The goal is that a future migration is ONE version bump of sentinel. Today's migration is\n * painful for the opposite reason: the runner and everything around it are declared in 133 places,\n * so moving them means moving 133 modules. A tool that sits between the runner and the tests, as\n * msw does, has to travel with the runner or it becomes the next thing nobody can move.\n *\n * It also puts the compatibility question where someone can answer it. When Vitest moves, msw\n * either still works with it or does not, and that is a question for the tooling team with one\n * test suite, not for thirty teams discovering it separately.\n *\n * ## Why version 1.3.3, which is not the newest\n *\n * Measured across the repo: 523 test files use the msw 1 API (`rest.get`), and NOT ONE uses the\n * msw 2 API (`http.get`). Shipping msw 2 would break all 523 on the day they migrate, turning one\n * migration into two. Shipping 1.3.3 keeps them working, and the eventual 1 to 2 move becomes\n * OURS: studied once, then automated or documented, which is the whole point of owning it.\n *\n * The one module on msw 2 is `apps/front/hr-management`, which is Svelte, and the test role defers\n * Svelte for its own reasons.\n */\nimport { setupServer } from 'msw/node'\n\nimport { SHARED_WORKER } from './file-boundary-state.js'\n\n/**\n * No default handlers, deliberately.\n *\n * The repo's own server had `defaultHandlers = []` too, so nothing is lost. A handler here would\n * be a Hublo endpoint inside a package meant to be usable by anyone, which is the line between\n * what sentinel owns (the config) and what an app owns (what it mocks).\n *\n * A suite adds its own with `server.use(...)`, which is how all 600 files that touch this already\n * work.\n */\nexport const server = setupServer()\n\n/** The options the repo's own setup passed, kept in one place since two calls now use them. */\nconst LISTEN_OPTIONS = { onUnhandledRequest: 'error' } as const\n\n/**\n * The lifecycle, as the root jest setup had it, plus ONE deviation the runners forced.\n *\n * `onUnhandledRequest: 'error'` is carried over rather than softened: a test that reaches a real\n * network is a test whose result depends on something outside the repo, and it should say so\n * loudly. The repo already ran this way, so nothing changes for a migrating suite.\n *\n * Registered through the globals Vitest exposes, because the preset sets `globals: true` and a\n * setup file that imported them would need them resolvable from wherever it is loaded.\n *\n * ## Why `listen()` also runs here, at module scope\n *\n * msw intercepts by REPLACING `request` and `get` on the `http` and `https` modules. Under jest,\n * `import * as https from 'https'` compiled to `require('https')`, so the module object was read\n * at call time and a patch applied later was seen. Under Vitest the same line is a real ESM\n * namespace import of a CJS builtin, and Node builds that namespace ONCE, from the exports as they\n * stand at first import. A patch applied afterwards never reaches it.\n *\n * `beforeAll` runs after the test file and everything it imports have been evaluated, so the\n * namespace is already frozen by then: the provider keeps calling the ORIGINAL `https.request` and\n * the request leaves the machine. Measured on `libs/cloud/hermes`: `send-push` reached the REAL\n * OneSignal API, which answered `400 Failed to parse app_id from request`, a message that exists\n * nowhere in this repo. msw never saw the call, so `onUnhandledRequest: 'error'` never fired\n * either, and the only visible symptom was `expected \"vi.fn()\" to be called 50 times, but got 0`.\n *\n * A setup file is evaluated BEFORE the test module graph, so patching here happens before the\n * namespace is built. Same file, same suite, 0 of 7 to 7 of 7.\n *\n * The `beforeAll` call is kept as well: it is what restores interception for a suite that closed\n * the server itself, and msw's own `listen()` is safe to call twice.\n */\nexport function installMswLifecycle(hooks: {\n beforeAll: (fn: () => void) => void\n afterEach: (fn: () => void) => void\n afterAll: (fn: () => void) => void\n}): void {\n server.listen(LISTEN_OPTIONS)\n hooks.beforeAll(() => server.listen(LISTEN_OPTIONS))\n\n /*\n * ⚠️ `close()` is per WORKER, not per file, and only isolation ever made those the same thing.\n *\n * Closing disposes the interceptors msw installed on `http`, `https`, `fetch` and `XMLHttpRequest`\n * and drops the registry it keeps on a global symbol. With a file-per-registry that was free: the\n * next file rebuilt everything from nothing. Sharing a worker, the next `listen()` finds a\n * disposed registry that is still THERE, so interception silently stops and the suite reads\n * whatever the previous file's handlers last said.\n *\n * Measured on `front-components` with `isolate: false`: one test, `useFeatureFlags`, and it took\n * a while to name because a global sweep was hiding it by deleting msw's four symbols 210 times\n * a run. Removing the per-file close instead: 230 files, 1418 tests, green, and green again\n * across four shuffled file orders.\n *\n * Not closing at all is safe: the worker exits when its files are done, and the process takes the\n * patched builtins with it. A module that wrote its own `afterAll(() => server.close())` before\n * adopting this needs the same line removed, which `--validate` surfaces as the suite failing.\n */\n hooks.afterAll(() => {\n if (!sharesAWorker()) server.close()\n })\n\n /*\n * `server.resetHandlers()` is NOT called here, and that is a transcription and not a judgement.\n *\n * The root jest setup has the line commented out. Enabling it would be an improvement in the\n * abstract — a handler one test adds leaks into the next, and the failure then appears in\n * whichever test happens to run after, which is the hardest kind to trace. But some suite may\n * depend on that persistence today, and a migration is the wrong moment to find out.\n *\n * So the behaviour is carried across exactly, and the question is reported rather than settled\n * here: it belongs to whoever owns those suites, as its own reviewed change.\n */\n}\n\n/** Whether the files of this suite share a worker, which `./file-boundary.js` is what declares. */\nfunction sharesAWorker(): boolean {\n return (globalThis as Record<symbol, unknown>)[SHARED_WORKER] === true\n}\n"],"mappings":";;;;;AAwBA,SAAS,mBAAmB;AAcrB,IAAM,SAAS,YAAY;AAGlC,IAAM,iBAAiB,EAAE,oBAAoB,QAAQ;AAiC9C,SAAS,oBAAoB,OAI3B;AACP,SAAO,OAAO,cAAc;AAC5B,QAAM,UAAU,MAAM,OAAO,OAAO,cAAc,CAAC;AAoBnD,QAAM,SAAS,MAAM;AACnB,QAAI,CAAC,cAAc,EAAG,QAAO,MAAM;AAAA,EACrC,CAAC;AAaH;AAGA,SAAS,gBAAyB;AAChC,SAAQ,WAAuC,aAAa,MAAM;AACpE;","names":[]}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// src/roles/test/setup/install-once.ts
|
|
2
|
+
var INSTALLED = /* @__PURE__ */ Symbol.for("@hublo/sentinel/test/setup/installed");
|
|
3
|
+
function alreadyInstalled(target, name) {
|
|
4
|
+
const marked = target;
|
|
5
|
+
const installed = marked[INSTALLED] ??= /* @__PURE__ */ new Set();
|
|
6
|
+
if (installed.has(name)) return true;
|
|
7
|
+
installed.add(name);
|
|
8
|
+
return false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export {
|
|
12
|
+
alreadyInstalled
|
|
13
|
+
};
|
|
14
|
+
//# sourceMappingURL=chunk-NUOQXAYR.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/install-once.ts"],"sourcesContent":["/**\n * The guard every setup installer needs, because a setup file runs ONCE PER TEST FILE.\n *\n * ## The defect this exists to close\n *\n * Vitest re-executes each entry of `setupFiles` for every test file. Under the isolation Vitest\n * gives by default that is harmless: the file also gets a fresh module registry, so the `vi` a\n * setup wraps is a fresh `vi`, and last file's wrapper went away with last file's registry.\n *\n * Without isolation the registry is the worker's, so `vi` is ONE object for every file, and an\n * installer that wraps `vi.fn` wraps last file's wrapper. Measured on `front-components`, 230 test\n * files: the chain reaches 230 deep and the suite dies with\n * `RangeError: Maximum call stack size exceeded`. What makes it worth a named guard rather than a\n * fix at each call site is how it fails. 26 files never ran, and the reporter said\n * `204 passed (204)` with no red: the loss was only visible by comparing the test COUNT against\n * the run before it.\n *\n * `installJestErrorEquality` fails the same way more quietly, adding one more equality tester to\n * `expect` per file rather than overflowing anything.\n *\n * ## Why the mark goes on the target and not on `globalThis`\n *\n * The mark has to answer \"is the object I am about to wrap already wrapped\", and only the object\n * itself can answer that. On `globalThis` the answer would be wrong in the isolated case: Vitest\n * builds a new environment per file, but a `node`-environment worker keeps one global object, so a\n * flag set there would survive into a file whose `vi` is brand new and unwrapped, and the\n * installer would skip the work it still had to do.\n */\n\n/** Stable across copies of this module, which pnpm's layout makes possible. */\nconst INSTALLED = Symbol.for('@hublo/sentinel/test/setup/installed')\n\ntype Marked = Record<symbol, Set<string> | undefined>\n\n/**\n * Records `name` against `target` and says whether it was already there.\n *\n * Written as a question an installer asks on its first line, `if (alreadyInstalled(vi, '...'))\n * return`, so the guarantee travels with the installer instead of depending on every caller\n * remembering it.\n */\nexport function alreadyInstalled(target: object, name: string): boolean {\n const marked = target as Marked\n const installed = (marked[INSTALLED] ??= new Set<string>())\n\n if (installed.has(name)) return true\n\n installed.add(name)\n return false\n}\n"],"mappings":";AA8BA,IAAM,YAAY,uBAAO,IAAI,sCAAsC;AAW5D,SAAS,iBAAiB,QAAgB,MAAuB;AACtE,QAAM,SAAS;AACf,QAAM,YAAa,OAAO,SAAS,MAAM,oBAAI,IAAY;AAEzD,MAAI,UAAU,IAAI,IAAI,EAAG,QAAO;AAEhC,YAAU,IAAI,IAAI;AAClB,SAAO;AACT;","names":[]}
|
|
@@ -6,9 +6,65 @@ import {
|
|
|
6
6
|
// src/roles/test/shared-test-config.ts
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { createRequire } from "node:module";
|
|
9
|
-
import { dirname, join } from "node:path";
|
|
9
|
+
import { dirname, join as join2 } from "node:path";
|
|
10
10
|
import { fileURLToPath } from "node:url";
|
|
11
11
|
|
|
12
|
+
// src/roles/test/isolation-split.ts
|
|
13
|
+
import { globSync, readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
var DECLARES_A_MODULE_MOCK = /\b(?:vi|jest)\.(?:mock|doMock)\s*\(/;
|
|
16
|
+
function testFilesUnder(root, include) {
|
|
17
|
+
return globSync([...include], { cwd: root, exclude: ["**/node_modules/**"] }).map(String).sort();
|
|
18
|
+
}
|
|
19
|
+
function splitByModuleMock(root, files) {
|
|
20
|
+
const mocked = [];
|
|
21
|
+
const shared = [];
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const source = readSource(join(root, file));
|
|
24
|
+
if (source !== void 0 && DECLARES_A_MODULE_MOCK.test(source)) mocked.push(file);
|
|
25
|
+
else shared.push(file);
|
|
26
|
+
}
|
|
27
|
+
return { mocked, shared };
|
|
28
|
+
}
|
|
29
|
+
function asIsolationSplit(config, root) {
|
|
30
|
+
const include = config.test?.include ?? [];
|
|
31
|
+
const files = testFilesUnder(root, include);
|
|
32
|
+
if (files.length === 0) return { ...config, test: { ...config.test, isolate: false } };
|
|
33
|
+
const { mocked, shared } = splitByModuleMock(root, files);
|
|
34
|
+
return {
|
|
35
|
+
...config,
|
|
36
|
+
test: {
|
|
37
|
+
// Kept at the root, where a project cannot answer for the whole run.
|
|
38
|
+
coverage: config.test?.coverage,
|
|
39
|
+
projects: [project(config, "shared", shared, false), project(config, "mocked", mocked, true)]
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function project(config, name, include, isolate) {
|
|
44
|
+
return {
|
|
45
|
+
...config,
|
|
46
|
+
test: {
|
|
47
|
+
...config.test,
|
|
48
|
+
name,
|
|
49
|
+
include,
|
|
50
|
+
isolate,
|
|
51
|
+
/*
|
|
52
|
+
* A project with nothing to run is still cheaper than a conditional here: Vitest starts it,
|
|
53
|
+
* finds no file and reports none. Leaving both projects in place keeps the shape of the
|
|
54
|
+
* config the same for every module, which is what makes a generated config reviewable.
|
|
55
|
+
*/
|
|
56
|
+
coverage: void 0
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
function readSource(path) {
|
|
61
|
+
try {
|
|
62
|
+
return readFileSync(path, "utf8");
|
|
63
|
+
} catch {
|
|
64
|
+
return void 0;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
12
68
|
// src/roles/test/msw-server-redirect.ts
|
|
13
69
|
var WORKSPACE_MSW_SERVER = /(^|\/)msw\/server\.[cm]?[jt]sx?$/;
|
|
14
70
|
function mswServerRedirect(serverFile, workspaceRoot) {
|
|
@@ -105,25 +161,25 @@ function mswAliases() {
|
|
|
105
161
|
}
|
|
106
162
|
function luxonAlias(root, workspaceRoot) {
|
|
107
163
|
for (const base of [root, workspaceRoot]) {
|
|
108
|
-
const manifest =
|
|
164
|
+
const manifest = join2(base, "node_modules", "luxon", "package.json");
|
|
109
165
|
if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }];
|
|
110
166
|
}
|
|
111
167
|
return [];
|
|
112
168
|
}
|
|
113
169
|
function axiosAlias(root, workspaceRoot) {
|
|
114
170
|
for (const base of [root, workspaceRoot]) {
|
|
115
|
-
const manifest =
|
|
171
|
+
const manifest = join2(base, "node_modules", "axios", "package.json");
|
|
116
172
|
if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }];
|
|
117
173
|
}
|
|
118
174
|
return [];
|
|
119
175
|
}
|
|
120
176
|
function prismaRuntimeAlias(workspaceRoot) {
|
|
121
|
-
const prisma =
|
|
177
|
+
const prisma = join2(workspaceRoot, "node_modules", "@prisma");
|
|
122
178
|
if (!existsSync(prisma)) return [];
|
|
123
179
|
return [
|
|
124
180
|
{
|
|
125
181
|
find: /^@prisma\/([^/]+)\/runtime\/library$/,
|
|
126
|
-
replacement:
|
|
182
|
+
replacement: join2(prisma, "$1", "runtime", "library.js")
|
|
127
183
|
}
|
|
128
184
|
];
|
|
129
185
|
}
|
|
@@ -388,6 +444,39 @@ function asAliasArray(alias) {
|
|
|
388
444
|
}));
|
|
389
445
|
}
|
|
390
446
|
function sharedTestConfig(options) {
|
|
447
|
+
return withIsolationChoice(mergedConfig(options), options);
|
|
448
|
+
}
|
|
449
|
+
function withIsolationChoice(config, options) {
|
|
450
|
+
if (options.isolate !== false) return config;
|
|
451
|
+
if (options.flavour === "nest") {
|
|
452
|
+
throw new Error(
|
|
453
|
+
"sentinel test: `isolate: false` is not available for a Nest suite. Nest registers its metadata as an import side effect, so sharing a module registry lets one suite see what another registered. Measured on `agency`, it would save 7% (43s to 40s) where a React module saves 71%, so the trade is not worth making. Remove the option."
|
|
454
|
+
);
|
|
455
|
+
}
|
|
456
|
+
const bracketed = {
|
|
457
|
+
...config,
|
|
458
|
+
test: {
|
|
459
|
+
...config.test,
|
|
460
|
+
/*
|
|
461
|
+
* The boundary brackets the module's own setup files rather than joining them. The closing
|
|
462
|
+
* entry has to run after the module's, because what it records as "infrastructure" is
|
|
463
|
+
* whatever the setup files built, and `setupFiles` order is the only way to say "last".
|
|
464
|
+
*/
|
|
465
|
+
setupFiles: [
|
|
466
|
+
"@hublo/sentinel/test/setup/file-boundary",
|
|
467
|
+
...asSetupList(config.test?.setupFiles),
|
|
468
|
+
"@hublo/sentinel/test/setup/file-boundary-close"
|
|
469
|
+
]
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
return asIsolationSplit(bracketed, options.root);
|
|
473
|
+
}
|
|
474
|
+
function asSetupList(setupFiles) {
|
|
475
|
+
if (typeof setupFiles === "string") return [setupFiles];
|
|
476
|
+
if (Array.isArray(setupFiles)) return setupFiles;
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
function mergedConfig(options) {
|
|
391
480
|
const base = baseConfig(options);
|
|
392
481
|
if (options.overrides === void 0) return base;
|
|
393
482
|
return {
|
|
@@ -459,4 +548,4 @@ export {
|
|
|
459
548
|
jestExportConditions,
|
|
460
549
|
sharedTestConfig
|
|
461
550
|
};
|
|
462
|
-
//# sourceMappingURL=chunk-
|
|
551
|
+
//# sourceMappingURL=chunk-SOCLCHJK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/shared-test-config.ts","../src/roles/test/isolation-split.ts","../src/roles/test/msw-server-redirect.ts","../src/roles/test/react/jest-export-conditions.ts"],"sourcesContent":["/**\n * The Vitest config a Nest module gets, as one call instead of forty lines copied 133 times.\n *\n * The React family needed none of this: those modules already had a Vitest config, so adoption\n * moved its imports and changed nothing else. The Jest family has no config at all, so one has to\n * be WRITTEN, and writing the same four decisions into 133 files is how they drift. A module that\n * genuinely differs still can: `overrides` is merged over the base, the same shape `nestService`\n * uses on the build side.\n *\n * Every line below was established by running a real suite, not by reading a guide. `mission` is\n * the reference: 387 specs, the module the build role refused for having its own webpack config,\n * and the one with the only `isolateModules` in the repo.\n */\nimport { existsSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { Alias as ViteAlias, AliasOptions } from 'vite'\nimport type { ViteUserConfig } from 'vitest/config'\n\nimport { decoratorMetadata } from '../build/nest/decorator-metadata.js'\nimport { tsconfigAliases } from '../build/nest/tsconfig-aliases.js'\nimport type { Alias } from '../build/nest/tsconfig-aliases.js'\n\nimport { asIsolationSplit } from './isolation-split.js'\nimport { mswServerRedirect } from './msw-server-redirect.js'\nimport { jestExportConditions } from './react/jest-export-conditions.js'\n\n/** Which family this config is for: it decides the three things that are not shared. */\nexport type TestFlavour = 'nest' | 'react'\n\nexport interface SharedTestOptions {\n /** Which family this config is for. It decides the three things that are not shared. */\n flavour?: TestFlavour\n /** The module's own directory: where its specs live and what its config is relative to. */\n root: string\n /** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */\n workspaceRoot: string\n /**\n * Lower decorators with TypeScript before oxc sees them, for a module that needs it.\n *\n * ⚠️ Written by `--init --test` from the module's own sources, never by hand, because the\n * condition is not a preference: it is whether a decorator sits on an `abstract` class member,\n * the one construct oxc does not reproduce. See `generate-config.ts` for the measurement.\n *\n * Off by default, and that default is the measured one: on\n * `apps/nest/microservices/mission`, 715 of 716 decorated files need nothing, and running the\n * plugin for all of them took the suite from 98s to 249s.\n */\n lowerDecoratorsWithTypeScript?: boolean\n /**\n * Let the files of this suite share a worker instead of each getting its own registry.\n *\n * Off by default, which is Vitest's own default and jest's semantics. Turning it off buys the\n * whole of `./setup/file-boundary.js` and `./isolation-split.js`: measured on\n * `front-components`, 44.6s isolated against 11.0s split, with all 1418 tests green and the\n * same result across ten shuffled file orders.\n *\n * A module takes it deliberately, and proves it with `--validate`, because the switch is the one\n * thing in this preset that changes what a suite MEANS: a file can see what an earlier file left\n * behind, and only running the suite can say whether any of them does.\n */\n isolate?: boolean\n /** Merged over the base. For what a module genuinely needs to differ on, nothing else. */\n overrides?: ViteUserConfig\n}\n\n/** A file shipped beside this module in the build, or nothing when running from source. */\nfunction siblingFile(relative: string): string | undefined {\n /*\n * ⚠️ Several candidates, because the EMITTED layout is not ours to predict. This file is imported\n * by two toolchains, so the bundler extracts it into a shared chunk at the package root, and it\n * does so even when it is declared as its own entry (the entry then only re-exports the chunk).\n * A single `import.meta.url`-relative path therefore resolves differently depending on where the\n * bundler decided to put the code.\n *\n * That is not theoretical: it silently broke the jest-mock-extended and msw-server adapters, and\n * `apps/cloud/shift-offer` went from 83/83 to 56/83 with \"The property X is not defined on the\n * object\" — a mockDeep shape error that names nothing near the cause. The alias had quietly\n * fallen back to the raw fork.\n *\n * So the file is LOOKED FOR rather than computed, and the first one that exists wins.\n */\n const candidates = [\n relative,\n `roles/test/${relative.replace(/^\\.\\//, '')}`,\n `../${relative.replace(/^\\.\\//, '')}`,\n ]\n for (const candidate of candidates) {\n try {\n const sibling = fileURLToPath(new URL(candidate, import.meta.url))\n if (existsSync(sibling)) return sibling\n } catch {\n continue\n }\n }\n return undefined\n}\n\n/** A package resolved from SENTINEL, so the module gets the copy the setup uses. */\nfunction resolveFrom(specifier: string): string | undefined {\n try {\n return fileURLToPath(import.meta.resolve(specifier))\n } catch {\n try {\n return createRequire(import.meta.url).resolve(specifier)\n } catch {\n return undefined\n }\n }\n}\n\n/**\n * sentinel's own adapter around the fork, which the alias points at when it has been built.\n *\n * The adapter only makes a deep mock answer `in` and `getOwnPropertyDescriptor`, which Vitest's\n * `spyOn` asks and jest's did not; `mock-extended.ts` carries the measurement. Resolved as a\n * sibling file rather than by specifier, because it is not a package.\n *\n * Falls back to the fork itself when there is no build beside this module, which is the case while\n * sentinel's own suite runs from `src`. The alias then behaves exactly as it did before the\n * adapter existed, so a missing build degrades instead of throwing.\n */\nfunction mockExtendedAdapterPath(): string | undefined {\n return siblingFile('./setup/mock-extended.js')\n}\n\n/**\n * The ABSOLUTE path of the Vitest fork, resolved from sentinel rather than named.\n *\n * A bare specifier in an alias is resolved by Vite against the IMPORTER, which here is a spec file\n * inside the adopted module. `vitest-mock-extended` is sentinel's dependency, not the module's, so\n * under pnpm's isolated layout the module cannot see it and the alias points at nothing.\n *\n * Measured on `apps/cloud/agency`: three suites died with \"Cannot find package\n * 'jest-mock-extended'\" while the fork sat in the store, installed and unreachable. Same class of\n * defect as the axios alias below, and the same fix: resolve it where it actually is.\n *\n * Falls back to the bare name, which keeps the previous behaviour rather than throwing while a\n * config is being built.\n */\nfunction vitestMockExtendedPath(): string {\n /*\n * `import.meta.resolve` and not `createRequire().resolve`: the latter follows the `require`\n * condition and hands back the package's CJS entry, whose first line requires `vitest`. Vitest\n * refuses that outright (\"Vitest cannot be imported in a CommonJS module using require()\"), so\n * the alias resolved correctly and then failed one step later. Measured on `apps/cloud/agency`.\n */\n try {\n return fileURLToPath(import.meta.resolve('vitest-mock-extended'))\n } catch {\n try {\n return createRequire(import.meta.url).resolve('vitest-mock-extended')\n } catch {\n return 'vitest-mock-extended'\n }\n }\n}\n\n/**\n * The ONE msw copy a module's tests share with the server that listens.\n *\n * sentinel ships msw so that a module stands alone, and the codemod repoints the SPEC files to it.\n * Helpers are not spec files, so they keep `import { rest } from 'msw'` and build their handlers\n * with the WORKSPACE's copy while the server that listens was built with sentinel's. Same version,\n * two physical instances, and a handler registered on one is invisible to the other.\n *\n * Measured on `apps/cloud/shift-offer`, whose `setup-mocks.ts` registers every handler its suites\n * rely on: 19 tests failed on `captured a request without a matching request handler` for a URL the\n * helper had a handler for. The repo has 159 non-spec files importing msw, and 2 importing the\n * workspace's server directly, so this is not one module's habit.\n *\n * Aliased rather than codemodded, for the same reason as `jest-mock-extended` above: it lets an\n * UNMIGRATED shared helper keep working inside a migrated module, which is what makes the\n * per-module plan possible at all. `^msw$` only: `msw/node` stays the package's own, and it is\n * sentinel's setup that imports it.\n */\n/** sentinel's own server file, the one its setup starts listening on. */\nfunction mswServerFile(): string | undefined {\n return siblingFile('./setup/msw-server.js')\n}\n\nfunction mswAliases(): Alias[] {\n const server = mswServerFile()\n const own = resolveFrom('msw')\n return [\n ...(own === undefined ? [] : [{ find: /^msw$/, replacement: own }]),\n ...(server === undefined ? [] : [{ find: /^@hublo\\/test\\/msw\\/server$/, replacement: server }]),\n ]\n}\n\n/**\n * One PHYSICAL axios, resolved from the MODULE and not from sentinel.\n *\n * pnpm installs axios twice here, same 1.17.0, differing only by a peer. The service client\n * resolves one copy and a provider's `import { AxiosError } from 'axios'` the other, so\n * `if (!(error instanceof AxiosError)) throw error` answers false on a genuine axios error and\n * rethrows it raw. The test then fails on the WRAPPER being absent, naming nothing near the cause.\n *\n * `resolve.dedupe` does NOT fix this, and neither does `deps.inline`: dedupe settles bare\n * specifiers, not two physical paths. The alias has to name the path.\n *\n * The module's OWN `node_modules`, checked directly rather than resolved.\n *\n * `createRequire(...).resolve()` was the first version and it is wrong twice. It walks UP the\n * tree, so a module without axios would be aliased to a PARENT's copy, which is the opposite of\n * pinning its own. And because it consults ambient resolution, the answer depends on where the\n * process runs: it correctly found nothing from a plain CLI run and found something under Vitest,\n * which is how a test of it became a test of its environment.\n *\n * pnpm links a module's direct dependencies into `<module>/node_modules`, so that path is both the\n * right answer and a deterministic one.\n *\n * Measured on `mission`: 378 passing suites to 381 of 387.\n *\n * ## ⚠️ And the workspace root when the module has none of its own\n *\n * The first version stopped there and returned nothing for a module without its own axios, on the\n * reasoning that pinning it to a PARENT's copy is the opposite of pinning its own. That reasoning\n * has an blind spot: a module with no copy of its own does not thereby have no axios PROBLEM. It\n * receives one through its dependencies, and it is still importing it two ways.\n *\n * Measured in `apps/nest/backends-for-frontends/backoffice`, which declares no axios, under this\n * very preset with no alias:\n *\n * the ESM AxiosError === the CJS AxiosError false\n * a CJS error instanceof the CJS class true\n * a CJS error instanceof the ESM class FALSE\n *\n * `@nestjs/axios` is externalised, so Node loads it and it throws the CJS class; the provider does\n * `import { AxiosError } from 'axios'` through Vite and gets the ESM one. Its\n * `if (error instanceof AxiosError) throw new RemoteBackendException(...)` therefore answers false\n * on a genuine axios error and rethrows it raw, and the test fails on the WRAPPER being absent,\n * naming nothing near the cause. Four modules reported it: `admin`, `backoffice`, `mission`,\n * `institution`.\n *\n * So: the module's own copy first, the workspace root second, nearest wins. That is the same shape\n * as `luxonAlias` above, and for the same reason: when a module has nothing of its own, the copy\n * it actually uses is the one to pin. Absent axios everywhere is still not an error.\n */\n/**\n * ONE physical luxon, for the same reason as axios: `Settings` is per-copy state.\n *\n * The setup pins `Settings.defaultZone = 'utc'`, which the root jest setup did for every suite in\n * the repo. It was reaching the copy `require` resolves, the CJS build, while the tests import the\n * one Vite resolves, the ESM build. Two instances, two `Settings`, and the pin landed on the one\n * nobody read.\n *\n * Measured on `libs/cloud/events-notifications`: the suite ran in `Europe/Paris`, so every date\n * came out two hours off and 12 assertions compared timestamps that differed by exactly that.\n *\n * luxon sits at the workspace root here rather than in each module, so both places are checked,\n * nearest first. Absent luxon is not an error: most modules have none.\n */\nfunction luxonAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'luxon', 'package.json')\n if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\nfunction axiosAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'axios', 'package.json')\n if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\n/**\n * A generated Prisma client's runtime, resolved to the file the package actually ships.\n *\n * The generated client reaches its own runtime by package name, and the package's `exports` map\n * answers differently depending on which condition asks:\n *\n * \"./runtime/library\": { \"require\": \"./runtime/library.js\", // shipped\n * \"import\": \"./runtime/library.mjs\" } // NOT shipped\n *\n * jest asked as CJS and got the file. Vitest resolves the same specifier under `import`, is sent\n * to a `.mjs` that does not exist, and the spec file does not load at all, so its tests go missing\n * rather than failing. Node itself spells the answer out: \"Did you mean to import\n * .../runtime/library.js?\".\n *\n * Measured on `apps/nest/microservices/client-management` (1 file) and\n * `apps/nest/microservices/institution` (3 files), on two different generated clients. This repo\n * generates 31 of them, all with the same manifest, so this is a property of the generator rather\n * than of a module.\n *\n * Written as a pattern with a back-reference, not one entry per client: the module does not know\n * which clients its dependencies pull in, and enumerating 31 names would go stale the day a\n * thirty-second schema is added.\n *\n * ⚠️ Not a workaround for a mistake of ours. The package declares a target it does not ship, and\n * pointing at the shipped file is what `require` already did. Nothing else changes: the `.js` IS\n * the runtime, in the same package, at the version installed.\n */\nfunction prismaRuntimeAlias(workspaceRoot: string): Alias[] {\n const prisma = join(workspaceRoot, 'node_modules', '@prisma')\n if (!existsSync(prisma)) return []\n\n return [\n {\n find: /^@prisma\\/([^/]+)\\/runtime\\/library$/,\n replacement: join(prisma, '$1', 'runtime', 'library.js'),\n },\n ]\n}\n\n/**\n * The base every Nest module gets. Four decisions, each with its measurement.\n */\nfunction baseConfig(options: SharedTestOptions): ViteUserConfig {\n const { root, workspaceRoot } = options\n\n return {\n /*\n * NO decorator-metadata plugin, and that is a measured removal rather than an omission.\n *\n * Nest reads constructor parameter types from `emitDecoratorMetadata`, so this preset used to\n * run the BUILD role's SWC plugin to emit it, on the premise that the bundler does not. That\n * premise was true of esbuild and is false of Vite 8, which transforms with oxc: oxc lowers the\n * decorators itself and emits the metadata, provided a tsconfig with `experimentalDecorators`\n * applies to the file.\n *\n * Measured five times before removing it. On `apps/nest/microservices/mission`, with and\n * without the plugin, the output is strictly identical for `@Injectable()` constructors\n * (including a service caught in a two-file import CYCLE, all 8 parameters resolved), for route\n * handler parameters and return types, and for DTO properties; the suite then passes in 98s\n * instead of 249s. Confirmed by a direct `Reflect.getMetadata` probe on\n * `apps/nest/backends-for-frontends/admin` and on `libs/nest/starter`, and by two A/B runs\n * through this very preset: `agency-notification` (100 tests) identical with and without, and\n * `grid-leave` (3335 tests) identical without.\n *\n * One caveat that the same measurement produced, and which belongs to the CONSUMER rather than\n * here: a property typed by an interface or by a type-alias imported with `import type` emits\n * `Object`. That is `emitDecoratorMetadata`'s own behaviour, identical with and without the\n * plugin, not a Vite regression.\n *\n * The BUILD role keeps its plugin. Its context differs, it feeds a shipped artefact, and\n * removing it there needs its own proof.\n *\n * ⚠️ ONE construct escapes oxc, and the exception is why this line is a condition rather than\n * an empty array: a decorator on an `abstract` class member. swc emitted it, oxc erases the\n * member and the decorator with it, silently. There is exactly one such member in this repo,\n * so `lowerDecoratorsWithTypeScript` buys the plugin back for that module alone.\n */\n /*\n * `jestExportConditions` is UNCONDITIONAL, and it is the one plugin every migrated module gets.\n *\n * jest resolved with `['node', 'require', 'default']`, read off the installed `@nx/jest/preset`\n * rather than off its documentation. `development` was never in that list, so a package\n * shipping a separate development build loaded its PRODUCTION file. Vite's own conditions carry\n * `development|production`, so `@emotion/cache` loads `emotion-cache.development.cjs.js`, whose\n * extra stylis plugin calls `console.error(':first-child is potentially unsafe...')`, and with\n * `jest-fail-on-console` in the setup that console call IS a failure.\n *\n * Measured on `libs/front/components`: 2 of its 3 remaining failures, both green again with\n * this. `apps/front/front-legacy` has 100 over 26 files from the same cause.\n *\n * Unconditional rather than reserved for a jsdom module, because it is not a statement about\n * the DOM: it is what this migration is for, running the suite the way the runner it was\n * written for ran it. It changes nothing for a module with no dual-published dependency, which\n * is why nest and cloud were measured identical without it.\n */\n plugins: [\n jestExportConditions(),\n /*\n * The alias beside it covers a file naming the workspace's msw server by its package\n * specifier. This covers the one that reaches the same module RELATIVELY, which an alias\n * cannot: `./server` is a spelling thousands of unrelated files use, so the redirect is\n * decided on the resolved path instead. See `msw-server-redirect.ts` for the measurement.\n */\n ...(mswServerFile() === undefined\n ? []\n : [mswServerRedirect(mswServerFile() as string, workspaceRoot)]),\n ...(options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : []),\n ],\n\n /*\n * The proviso in the paragraph above, made unconditional.\n *\n * oxc lowers decorators from the tsconfig that applies to the file, so a file belonging to NO\n * tsconfig `include` is lowered as if it used the STANDARD decorators, and comes out of Vite as\n * invalid JavaScript. Not merely without metadata: the file fails to parse, and every file that\n * imports it disappears with it.\n *\n * Measured on `apps/nest/microservices/mission`, where ONE uncovered helper,\n * `src/app/test/mission.test-wrapper.ts`, took down 158 of 416 test files. Reproduced on a\n * three-file case: covered file fine, uncovered file `SyntaxError: Invalid or unexpected\n * token`, and this option alone turns it into the same output the covered file gets, metadata\n * included.\n *\n * Declared here rather than left to each module's tsconfig `include`, because the alternative\n * is asking 107 teams to find which of their files no tsconfig covers, which is the work this\n * role exists to do for them.\n *\n * Two things the same measurement established, both deliberate:\n *\n * - it OVERRIDES the tsconfig, it is not a default the tsconfig refines. A file under a\n * tsconfig saying `emitDecoratorMetadata: false` gets metadata anyway. Acceptable because\n * this is the NEST preset and a Nest module is legacy decorators by definition: no tsconfig\n * under `apps/nest`, `libs/nest`, `apps/cloud` or `libs/cloud` sets either option to false.\n * - it needs no polyfill. Without `reflect-metadata` loaded, nothing throws, the metadata is\n * simply unreadable, exactly as before.\n */\n oxc: { decorator: { legacy: true, emitDecoratorMetadata: true } },\n\n resolve: {\n alias: [\n ...axiosAlias(root, workspaceRoot),\n ...luxonAlias(root, workspaceRoot),\n /*\n * Nest only, and measured: 1186 files under `apps/nest` and `libs/nest` mention `@prisma/`,\n * and ZERO under `apps/front` and `libs/front`. A React module paying for an alias to a\n * client it never generates is noise in a file someone has to read.\n */\n ...(options.flavour === 'nest' ? prismaRuntimeAlias(workspaceRoot) : []),\n ...mswAliases(),\n /*\n * `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491\n * files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers\n * breaks every module still on jest and leaving them breaks every module moved to Vitest.\n * Old and new therefore do not cohabit on shared helpers, which would have killed the\n * per-module plan.\n *\n * This one line removes the constraint: an unmigrated helper resolves to the Vitest fork\n * inside an adopted module and keeps resolving to the jest one everywhere else.\n * `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.\n *\n * Measured on `mission`: failing suites went from 159 to 10.\n */\n {\n find: /^jest-mock-extended$/,\n replacement: mockExtendedAdapterPath() ?? vitestMockExtendedPath(),\n },\n ...tsconfigAliases(workspaceRoot),\n ],\n },\n\n test: {\n globals: true,\n environment: 'node',\n root,\n /*\n * What the repo's jest preset actually matched, copied rather than approximated:\n * `**\\/?(*.)+(spec|test).[jt]s?(x)`.\n *\n * Both NAMES, because jest ran both: `**\\/*.spec.ts` alone read green while missing three\n * `.test.ts` files and 23 tests, with nothing saying so. And all four EXTENSIONS, for the\n * same reason one notch further out. Measured over the repo's 7063 test files:\n *\n * .ts 5844 .tsx 1108 .js 99 .mjs/.cjs 12\n *\n * The `.ts`-only form cost nothing on nest and cloud, which have none of the others, and it\n * cost `libs/front/api` three files and 13 tests on the first front module it met. Half the\n * front's test files are `.tsx`.\n *\n * `.mjs` and `.cjs` are deliberately OUT: jest's `[jt]s?(x)` does not match them either, and\n * Vitest's own default include does. Running a file the reference never ran is as wrong as\n * skipping one it did.\n */\n include: ['**/*.spec.[jt]s?(x)', '**/*.test.[jt]s?(x)'],\n /*\n * Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:\n * a decorator writes into a catalog when its module loads. Sharing a module registry across\n * files lets one suite see what another registered, and the failure appears in whichever\n * file happens to run second.\n */\n /*\n * Nest only. A Nest module registers metadata as an import SIDE EFFECT, so sharing a module\n * registry across files lets one suite see what another registered. A React module has no\n * such catalog, and isolation is not free.\n */\n ...(options.flavour === 'nest' ? { isolate: true } : {}),\n /*\n * Concurrency, transposed from what this repo does today rather than chosen.\n *\n * Every jest target inherits `configurations.ci = { ci: true, runInBand: true }` from the\n * `@nx/jest:jest` key in `nx.json`, and CI invokes every test target with\n * `--configuration=ci`. So on CI every suite in this repo runs ONE FILE AT A TIME today. That\n * key belongs to the jest executor and cannot be touched, because the workspace is mixed: it\n * still serves the modules that have not moved.\n *\n * ⚠️ And it is CI-ONLY. A local run omits `--configuration=ci`, so jest runs files in\n * PARALLEL on a developer's machine. A flat `fileParallelism: false` here would make local\n * runs slower than jest, which loses something the module had. Hence the condition rather\n * than the constant: parallel locally, one file at a time on CI, which is jest on both sides.\n *\n * Measured on three files that each hold the clock for 400ms and record their interval:\n *\n * default files overlap, 402ms\n * fileParallelism: false no overlap, 1442ms\n * maxWorkers: 1 no overlap, 1423ms\n *\n * Both candidates give the property that matters. `fileParallelism` is the one that says what\n * the module MEANS (\"do not run my files at the same time\"); `maxWorkers` is a pool size, and\n * it is what a module asking for a CAP gets instead (three BFFs ask for 4).\n *\n * A module that declared its own concurrency overrides this, in both environments, exactly as\n * it does today.\n */\n /*\n * ⚠️ PARALLEL on CI too, since 26/09, and that reverses what the note above decided.\n *\n * The transposition was faithful and expensive. Measured on `libs/front/components`, 1418\n * tests green on every line, on the repo's own 4-core agents:\n *\n * jest, as CI runs it 158.7 s\n * vitest serial, the transposition 644.5 s 4.1x slower\n * vitest, 3 workers 324.7 s 2.05x slower\n *\n * Serialising costs vitest far more than it costs jest, because vitest re-evaluates the\n * module graph per file through Vite's SSR runner: 555 ms of `import` and 478 ms of\n * `environment` per file against 139 ms of actual tests. Parallelism does not remove that\n * cost, it pays it on several cores at once.\n *\n * Every other documented lever was measured and does nothing or breaks the suite:\n * `deps.optimizer.ssr`, `NODE_COMPILE_CACHE` and `fsModuleCache` all cache TRANSFORMATION\n * where the cost is EVALUATION; `pool: threads` is noise; `vmThreads` collapses into\n * `no-isolate` when serial and fails 473 tests; `--no-isolate` fails 351.\n *\n * ⚠️ And NO worker cap, which is deliberate after reading vitest's own sizing:\n *\n * getDefaultThreadsCount: config.watch ? max(numCpus / 2, 1) : max(numCpus - 1, 1)\n *\n * `cores - 1` is 3 on this repo's 4-core agents, which is exactly the pool the 324.7 s above\n * was measured with, and 9 on a 10-core laptop. Pinning a number would slow every developer\n * to serve the agent, and a percentage would only re-describe a default that already adapts.\n * So one variable changes here and the tool keeps sizing its own pool.\n *\n * Memory was the reason to hesitate and it was measured rather than assumed: a full jsdom\n * run holds 1.1 GB at three workers and 1.9 GB at six, on agents with 32 GB. The OOM this\n * repo suffered was three nx tasks near 7.45 GB each, which is not what a test worker costs.\n *\n * A module that declares its own concurrency still overrides this, as before.\n */\n fileParallelism: true,\n\n /*\n * ⚠️ Sentinel's own setup files go through VITE, not round it, and that is load-bearing.\n *\n * Vitest externalizes anything under `node_modules`, so an installed `@hublo/sentinel` is\n * loaded by Node while the module's own code is served by Vite. The two then hold DIFFERENT\n * instances of the same package, and a setup whose whole job is to mutate per-copy state\n * mutates a copy nobody reads.\n *\n * Measured on `apps/nest/microservices/agency`, with the published 1.4.0-alpha.4:\n *\n * zone of the copy sentinel pinned UTC\n * zone of the copy the test imports Europe/Paris\n * same object false\n *\n * Six tests that passed under jest failed, all comparing `DateTime.now().toISO()` against a\n * date the provider had serialised: identical instants, one rendered `+02:00` and the other\n * `Z`. The root jest setup pinned `Settings.defaultZone = 'utc'` and 97 modules named it, so\n * this is not one module's edge case.\n *\n * ⚠️ It hid for a whole campaign because the harness SYMLINKS sentinel to its source tree.\n * The resolved path then holds no `node_modules`, Vitest inlines it, and the pin lands on the\n * right copy: 96 modules reported IDENTIQUE while the real install was red. An instrument\n * that installs the package differently from a team does not measure what a team will see.\n *\n * The narrow form `/@hublo\\/sentinel\\/dist\\/roles\\//` was tried and does NOT work: the pin\n * itself lives in a shared chunk at the dist root, which stays external. It has to be the\n * whole package.\n */\n server: { deps: { inline: [/@hublo\\/sentinel/] } },\n },\n }\n}\n\n/**\n * Vite accepts `alias` as an ARRAY or as an object map, and the two do not combine.\n *\n * The array form is the one that matters here: only it takes a RegExp `find`, which both the axios\n * and the `jest-mock-extended` aliases need. A module overriding with the object form gets its\n * entries converted rather than dropped, because silently losing an override is worse than a shape\n * the caller did not expect.\n */\nfunction asAliasArray(alias: AliasOptions | undefined): ViteAlias[] {\n if (alias === undefined) return []\n if (Array.isArray(alias)) return alias as ViteAlias[]\n return Object.entries(alias as Record<string, string>).map(([find, replacement]) => ({\n find,\n replacement,\n }))\n}\n\n/** A `test.server` block, named off Vitest's own type so the merge cannot drift from it. */\ntype TestServer = NonNullable<ViteUserConfig['test']>['server']\n\n/** The config, with the module's own overrides merged over it. */\nexport function sharedTestConfig(\n options: SharedTestOptions & { flavour: TestFlavour },\n): ViteUserConfig {\n return withIsolationChoice(mergedConfig(options), options)\n}\n\n/**\n * The isolation the module asked for, and everything that has to come with it.\n *\n * Two things travel together and neither works alone: the file boundary, which gives back per\n * file what a shared worker leaks, and the split, which keeps isolation for the files that mock a\n * module. See `./setup/file-boundary.js` and `./isolation-split.js` for what each measured.\n */\nfunction withIsolationChoice(\n config: ViteUserConfig,\n options: SharedTestOptions & { flavour: TestFlavour },\n): ViteUserConfig {\n if (options.isolate !== false) return config\n\n /*\n * Nest does not get this, and the refusal is measured rather than cautious.\n *\n * What it would buy: `agency`, 120 files and 539 tests, goes from 43s to 40s and uses the same\n * peak memory. Seven percent, where the same change takes a jsdom module from 44.6s to 13s,\n * because a Nest suite's cost is its test bodies and a React suite's cost is re-evaluating its\n * import graph once per file.\n *\n * What it would cost is written a few lines up, where `isolate: true` is set for this flavour:\n * Nest registers metadata as an import SIDE EFFECT, so a decorator writes into a catalog when\n * its module loads and a shared registry lets one suite see what another registered. Refused\n * rather than ignored, because a module that asked for it and silently did not get it would\n * read its own config as a lie.\n */\n if (options.flavour === 'nest') {\n throw new Error(\n 'sentinel test: `isolate: false` is not available for a Nest suite. Nest registers its ' +\n 'metadata as an import side effect, so sharing a module registry lets one suite see what ' +\n 'another registered. Measured on `agency`, it would save 7% (43s to 40s) where a React ' +\n 'module saves 71%, so the trade is not worth making. Remove the option.',\n )\n }\n\n const bracketed: ViteUserConfig = {\n ...config,\n test: {\n ...config.test,\n /*\n * The boundary brackets the module's own setup files rather than joining them. The closing\n * entry has to run after the module's, because what it records as \"infrastructure\" is\n * whatever the setup files built, and `setupFiles` order is the only way to say \"last\".\n */\n setupFiles: [\n '@hublo/sentinel/test/setup/file-boundary',\n ...asSetupList(config.test?.setupFiles),\n '@hublo/sentinel/test/setup/file-boundary-close',\n ],\n },\n }\n\n return asIsolationSplit(bracketed, options.root)\n}\n\n/** `setupFiles` accepts a string or a list; the boundary needs a list either way. */\nfunction asSetupList(setupFiles: unknown): string[] {\n if (typeof setupFiles === 'string') return [setupFiles]\n if (Array.isArray(setupFiles)) return setupFiles as string[]\n return []\n}\n\n/** The base with the module's own overrides merged over it. */\nfunction mergedConfig(options: SharedTestOptions & { flavour: TestFlavour }): ViteUserConfig {\n const base = baseConfig(options)\n if (options.overrides === undefined) return base\n // Shallow by section rather than a deep merge helper: the three sections a module overrides in\n // practice are `test`, `resolve` and `plugins`, and a deep merge would silently concatenate\n // arrays a module meant to replace.\n return {\n ...base,\n ...options.overrides,\n plugins: [...(base.plugins ?? []), ...(options.overrides.plugins ?? [])],\n resolve: {\n ...base.resolve,\n ...options.overrides.resolve,\n /*\n * The module's own aliases come after sentinel's, and in a Vite alias ARRAY the FIRST match\n * wins. So sentinel's win, which is the opposite of what this comment used to claim.\n *\n * ⚠️ The order is corrected here rather than in the code, because changing it would flip a\n * behaviour that nothing exercises. Every alias sentinel adds is ANCHORED to one exact\n * specifier: `^axios$`, `^luxon$`, `^msw$`, `^jest-mock-extended$` and\n * `^@prisma/<x>/runtime/library$`. A module's own entry collides only by naming the identical\n * string, and measured across every `jest.config*` in the repo, none does: the closest are\n * `@front/type/axios` and `@front/api/msw-handlers` in `front-legacy`, different specifiers\n * in the one module this role refuses anyway.\n *\n * So today the order decides nothing, and all 97 migrations were measured with it this way\n * round. Reversing it on a hypothesis would be a silent behaviour change bought with nothing.\n * What was actually wrong was a comment promising a module it could win, which a reader would\n * have relied on.\n */\n alias: [\n ...asAliasArray(base.resolve?.alias),\n ...asAliasArray(options.overrides.resolve?.alias),\n ],\n },\n test: {\n ...base.test,\n ...options.overrides.test,\n /*\n * ⚠️ `server.deps.inline` is the one key inside `test` that must not be REPLACED.\n *\n * The shallow spread above is right for everything else: a module overriding `environment`\n * or `testTimeout` means to replace it. But the base uses this list to put sentinel's own\n * setup inside Vite's graph, and a module declares its own entries there for its workspace\n * packages. Dropping either half breaks something silently, and the halves have nothing to\n * do with each other, so they are concatenated.\n *\n * Measured on `libs/front/api`, which declares `['@front/runtime-config', '@segment/snippet']`\n * and would otherwise have discarded the base's entry and lost the timezone pin with it.\n */\n server: mergeServerDeps(base.test?.server, options.overrides.test?.server),\n },\n }\n}\n\n/** Two `server` blocks, with their `deps.inline` lists joined rather than one winning. */\nfunction mergeServerDeps(base: TestServer, override: TestServer): TestServer {\n if (base === undefined) return override\n if (override === undefined) return base\n\n const inline = [\n ...(asInlineArray(base.deps?.inline) ?? []),\n ...(asInlineArray(override.deps?.inline) ?? []),\n ]\n return {\n ...base,\n ...override,\n deps: { ...base.deps, ...override.deps, ...(inline.length > 0 ? { inline } : {}) },\n }\n}\n\n/** `inline` is a list or the literal `true`, and `true` cannot be concatenated with anything. */\nfunction asInlineArray(inline: unknown): (string | RegExp)[] | undefined {\n return Array.isArray(inline) ? (inline as (string | RegExp)[]) : undefined\n}\n","/**\n * Turning one config into two projects, so that most files can share a worker and the few that\n * cannot keep their isolation.\n *\n * ## Why a split rather than one setting\n *\n * Isolation is what makes a Vitest run expensive: every test file re-evaluates the whole import\n * graph. Measured on `front-components`, 230 files and 1418 tests, same machine, same session:\n *\n * jest, files in parallel 15.9 s\n * Vitest, isolated 44.6 s\n * Vitest, this split 11.0 s\n *\n * `./setup/file-boundary.js` gives back, by hand, everything a shared worker would otherwise leak.\n * One thing it cannot give back is a module that has already been evaluated. `vi.mock` has to be\n * in place BEFORE the module it replaces is imported, so a file declaring a mock cannot be handed\n * a registry another file already filled, and no hook can undo that after the fact.\n *\n * So those files, and only those, keep isolation. Measured on `front-components`, 13 of 230, which\n * is 6%. The two projects run in one command and report as one run.\n *\n * ## Why the split is read, not declared\n *\n * A list a module maintains by hand goes stale the first time someone adds a test, and it goes\n * stale silently: the file still runs, it just runs in the wrong project, and what it then breaks\n * lands on an unrelated file. Reading the sources costs one `readFileSync` per test file, measured\n * at a few milliseconds for 230, and it cannot drift.\n *\n * `jest.mock` counts as well as `vi.mock`: a module can be adopted before its codemod has run, and\n * `./setup/jest-global.js` makes the `jest` global work, so the call is real. `doMock` is spelled\n * out rather than folded into an optional `do` prefix, which is how the first version missed it:\n * the M is capital, and a case-insensitive flag would have matched a variable called `domock`.\n */\nimport { globSync, readFileSync } from 'node:fs'\nimport { join } from 'node:path'\n\nimport type { ViteUserConfig } from 'vitest/config'\n\n/**\n * The call that forces a file to keep its isolation.\n *\n * Deliberately not anchored to the start of a line: `vi.mock` is hoisted by Vitest wherever it\n * sits, including inside a `describe`. A match inside a comment or a string costs that one file\n * its share of the speed-up and nothing else, which is the right way round for a heuristic that\n * decides isolation.\n */\nconst DECLARES_A_MODULE_MOCK = /\\b(?:vi|jest)\\.(?:mock|doMock)\\s*\\(/\n\n/** The test files under `root` that `include` selects, relative to `root`, in a stable order. */\nexport function testFilesUnder(root: string, include: readonly string[]): string[] {\n return globSync([...include], { cwd: root, exclude: ['**/node_modules/**'] })\n .map(String)\n .sort()\n}\n\n/** Those of `files` that declare a module mock, and those that do not. */\nexport function splitByModuleMock(\n root: string,\n files: readonly string[],\n): { mocked: string[]; shared: string[] } {\n const mocked: string[] = []\n const shared: string[] = []\n\n for (const file of files) {\n const source = readSource(join(root, file))\n if (source !== undefined && DECLARES_A_MODULE_MOCK.test(source)) mocked.push(file)\n else shared.push(file)\n }\n\n return { mocked, shared }\n}\n\n/**\n * `config`, expressed as two projects: everything in a shared worker, the module-mocking files\n * isolated.\n *\n * The base is SPREAD into each project rather than reached through Vitest's `extends`. Measured:\n * `extends` merges with `mergeConfig`, which concatenates arrays, so each project's `include` came\n * back as the base's patterns plus its own and both projects ran all 230 files, reporting 460\n * files and 2836 tests for a module that has 230 and 1418. The count is what caught it; the run\n * was green and nearly the right duration.\n */\nexport function asIsolationSplit(config: ViteUserConfig, root: string): ViteUserConfig {\n const include = config.test?.include ?? []\n const files = testFilesUnder(root, include)\n\n // Nothing to split, and an empty `include` would make a project run the default patterns\n // instead of nothing at all.\n if (files.length === 0) return { ...config, test: { ...config.test, isolate: false } }\n\n const { mocked, shared } = splitByModuleMock(root, files)\n\n return {\n ...config,\n test: {\n // Kept at the root, where a project cannot answer for the whole run.\n coverage: config.test?.coverage,\n projects: [project(config, 'shared', shared, false), project(config, 'mocked', mocked, true)],\n },\n }\n}\n\nfunction project(\n config: ViteUserConfig,\n name: string,\n include: string[],\n isolate: boolean,\n): ViteUserConfig {\n return {\n ...config,\n test: {\n ...config.test,\n name,\n include,\n isolate,\n /*\n * A project with nothing to run is still cheaper than a conditional here: Vitest starts it,\n * finds no file and reports none. Leaving both projects in place keeps the shape of the\n * config the same for every module, which is what makes a generated config reviewable.\n */\n coverage: undefined,\n },\n }\n}\n\nfunction readSource(path: string): string | undefined {\n try {\n return readFileSync(path, 'utf8')\n } catch {\n // A file that vanished between the glob and the read is not this function's problem to report:\n // Vitest will say so itself, by name, when it fails to run it.\n return undefined\n }\n}\n","/**\n * Send every importer of the workspace's msw server to the one that actually listens.\n *\n * ## The gap an alias cannot close\n *\n * The generated config already aliases `@hublo/test/msw/server`, which covers a file naming the\n * server by its package specifier. It cannot cover a file that reaches the same module by a\n * RELATIVE path, because an alias matches the spelling of the import and `./server` is a spelling\n * thousands of unrelated files use.\n *\n * That gap is not hypothetical. `libs/nest/tests/src/msw/create-handlers.ts` exports\n * `createMswHandler`, whose body calls `server.use(...)` on `./server`. So a migrated module gets\n * its handlers registered on the WORKSPACE's server while sentinel's is the one listening, and the\n * request arrives unmatched.\n *\n * Measured on `apps/nest/microservices/institution`, its only consumer:\n *\n * jest 2958 passed\n * vitest, without this 2957 passed, 1 failed\n * the failure Worker Webservice transformTemporaryPersonToWorker\n * onUnhandledRequest, raised by sentinel's copy\n *\n * ## Why here rather than in that file\n *\n * One line in `create-handlers.ts` would also fix it. Héla, 2026-09-24: the fix belongs in what we\n * OWN. A config sentinel generates reaches every module that adopts, today and later, and asks\n * nothing of a team whose shared lib happens to sit in the path. Editing their file fixes one\n * import and leaves the next one to be discovered by whoever writes it.\n *\n * ## Why the RESOLVED path, which is narrower than it looks\n *\n * This matches what the import turned out to BE, not how it was written, so `./server`,\n * `../msw/server` and `@hublo/test/msw/server` are one case rather than three. The pattern is the\n * same convention the codemod already uses to repoint a server import: a file whose path ends in\n * `msw/server`. It is checked against the resolved id only, so a module's own `./server` that has\n * nothing to do with msw is never touched.\n *\n * ## What it does NOT do\n *\n * Redirect anything outside the workspace. A `msw/server` inside `node_modules` is a package's own\n * business, and sentinel's own server file is skipped so the redirect cannot point at itself.\n */\nimport type { Plugin } from 'vite'\n\n/** The convention: a module whose path ends in `msw/server`, whatever extension it carries. */\nconst WORKSPACE_MSW_SERVER = /(^|\\/)msw\\/server\\.[cm]?[jt]sx?$/\n\n/**\n * @param serverFile sentinel's own server, the one its setup starts listening on.\n * @param workspaceRoot the boundary: nothing outside it is redirected.\n */\nexport function mswServerRedirect(serverFile: string, workspaceRoot: string): Plugin {\n return {\n name: 'sentinel:msw-server-redirect',\n // Before the resolver settles on the workspace file, since the point is to replace it.\n enforce: 'pre',\n async resolveId(source, importer, options) {\n // Our own file, and anything already pointing at it: nothing to do, and no self-recursion.\n if (source === serverFile) return undefined\n\n const resolved = await this.resolve(source, importer, { ...options, skipSelf: true })\n if (resolved === null || resolved.external) return undefined\n\n const id = resolved.id\n if (!id.startsWith(workspaceRoot)) return undefined\n if (id.includes('/node_modules/')) return undefined\n if (!WORKSPACE_MSW_SERVER.test(id)) return undefined\n\n return serverFile\n },\n }\n}\n","/**\n * Resolve packages the way jest did, for a suite that was written against jest's resolution.\n *\n * ## What jest resolved, measured rather than remembered\n *\n * Every suite in this repo ran through `@nx/jest/preset`, whose\n * `testEnvironmentOptions.customExportConditions` is exactly `['node', 'require', 'default']`\n * (read off the installed preset, not off its documentation). `development` was never in that\n * list, so a package shipping a separate development build behind that condition loaded its\n * PRODUCTION file under jest.\n *\n * Vitest resolves with Vite's conditions instead, and its default carries the\n * `development|production` token. Measured on a bare config, the resolved list is\n * `['node', 'development|production']`. `@emotion/cache` then loads\n * `emotion-cache.development.cjs.js`, whose extra stylis plugin calls\n * `console.error(':first-child is potentially unsafe...')`.\n *\n * With `jest-fail-on-console` in the setup, that console call IS a failure: **100 tests over 26\n * files on `apps/front/front-legacy`**, every one of them green under jest. `libs/front/components`\n * hits the same root cause on one test.\n *\n * ## Why a plugin and not a config line\n *\n * Declaring `ssr.resolve.conditions: ['node']` does nothing: Vite merges config arrays by\n * CONCATENATION, so Vitest's default is appended straight back. The token has to come off the\n * RESOLVED config, which is what `configResolved` is for.\n *\n * This is the same shape as the other thing that cannot be declared: a module cannot reproduce\n * `customExportConditions` either, because the list it asks for is PREFIXED to Vite's defaults\n * rather than substituted for them.\n *\n * ## The three lists are one array\n *\n * Measured on Vite 8: `config.resolve.conditions`, `config.ssr.resolve.conditions` and\n * `config.environments.ssr.resolve.conditions` are the SAME array object, so stripping one strips\n * all three. All three are stripped anyway. Relying on an aliasing that nothing promises is how a\n * silent regression arrives on a Vite upgrade, and the cost of being explicit is two lines.\n *\n * ## ⚠️ What this deliberately hides, said out loud\n *\n * emotion's warning is REAL: `libs/front/components` is consumed by a Next app, and a\n * `:first-child` selector is genuinely unsafe when the markup is rendered server-side. Restoring\n * jest's resolution puts that warning back out of sight.\n *\n * It is hidden here on purpose all the same, because a migration that also turns 100 green tests\n * red cannot be told apart from a migration that broke something. The baseline gate compares test\n * names, and it has no way to know which reds are progress. The selector belongs to the module's\n * owners as its own piece of work, with its own ticket, not as a side effect of changing runner.\n */\nimport type { Plugin } from 'vite'\n\n/**\n * The tokens jest never had.\n *\n * Both spellings, because the resolved config carries `development|production` (Vite's own\n * placeholder, replaced per environment) while a config written by hand may carry `development`.\n */\nconst ABSENT_UNDER_JEST = new Set(['development', 'development|production'])\n\n/** Remove them in place, since the resolved config is what the resolver will read. */\nfunction stripInPlace(conditions: string[] | undefined): void {\n if (conditions === undefined) return\n\n for (let index = conditions.length - 1; index >= 0; index -= 1) {\n const condition = conditions[index]\n if (condition !== undefined && ABSENT_UNDER_JEST.has(condition)) conditions.splice(index, 1)\n }\n}\n\n/**\n * Add to `plugins` in a module whose suite was written against jest's resolution.\n *\n * Not applied by the preset for everyone: it is a MIGRATION aid, and a module that was always on\n * Vitest never had jest's resolution to go back to. A module adopts it the day it migrates, and\n * can drop it the day its suite no longer depends on the production build.\n */\nexport function jestExportConditions(): Plugin {\n return {\n name: 'sentinel:jest-export-conditions',\n configResolved(config) {\n const environments = config as unknown as {\n environments?: { ssr?: { resolve?: { conditions?: string[] } } }\n }\n\n stripInPlace(config.resolve?.conditions as string[] | undefined)\n stripInPlace(config.ssr?.resolve?.conditions as string[] | undefined)\n stripInPlace(environments.environments?.ssr?.resolve?.conditions)\n },\n }\n}\n"],"mappings":"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","names":["join","join"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/file-boundary-close.ts"],"sourcesContent":["/**\n * The second half of the file boundary, and the reason it is a second entry at all.\n *\n * Placed LAST in `setupFiles`, after the module's own, so that by the time it runs:\n *\n * - every mock a setup file built is already recorded as infrastructure, and anything built from\n * here on belongs to the test file;\n * - every global a setup file installed is already part of the baseline, and anything added from\n * here on belongs to the test file.\n *\n * Neither could be decided from the first entry. Vitest offers no hook between \"the last setup\n * file finished\" and \"the test file is imported\", and a microtask does not help because setup\n * files are awaited in turn. The order of `setupFiles` is the only way to say \"after the others\",\n * so the boundary is opened by `./file-boundary.js` and closed here.\n */\nimport { PHASE } from './file-boundary-state.js'\n\nconst state = globalThis as Record<symbol, unknown>\n\nstate[PHASE] = 'file'\n\n/*\n * ⚠️ NO global sweep here, and that is a retraction.\n *\n * An earlier version snapshotted `Reflect.ownKeys(globalThis)` per file and deleted whatever a\n * file added. It fixed three tests on `front-components` that read a `window.google` left by\n * another file, and it looked principled: a fresh environment had none of those keys either.\n *\n * It is not the same thing. The other four boundaries RESTORE what the runner used to reset. This\n * one DELETED state belonging to whoever created it, and a library that initialises once per\n * worker never gets it back. Measured on `agency`: `@prisma/*` sets `globalThis.DEBUG` with `??=`\n * when its runtime is first imported, the sweep removed it at the end of that file, and 41 suites\n * then died on `Cannot read properties of undefined (reading 'split')`. The same run is green with\n * isolation, so it was ours. Logging every deletion over a `front-components` run had already said\n * as much and I read it as acceptable: 220 deletions, of which 210 were msw's interceptor symbols.\n *\n * Removing it costs ONE test on `front-components`, and that number is measured, not predicted:\n * an earlier version of this comment claimed 1418 green before the run had happened, and the run\n * said 1417. The survivor is `useFeatureFlags`, and it is not a global a test wrote. It is msw:\n * the sweep was deleting `Symbol(setup-server)` and the three interceptor symbols beside it 210\n * times a run, which forced msw to rebuild per file and hid the fact that its registry carries\n * state from one file to the next. Narrowing the sweep to string keys loses the same single test,\n * which is what pointed at msw in the first place.\n *\n * So the msw leak is real and open, and it belongs to msw's lifecycle rather than to a blanket\n * sweep of the global object. A test that writes to `globalThis` and does not clean up stays the\n * module's to fix, and `--validate` names it.\n */\n"],"mappings":";;;;;AAiBA,IAAM,QAAQ;AAEd,MAAM,KAAK,IAAI;","names":[]}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CURRENT_FILE_MOCKS,
|
|
3
|
+
PHASE,
|
|
4
|
+
SHARED_WORKER
|
|
5
|
+
} from "../../../chunk-H3GSGAX2.js";
|
|
6
|
+
import {
|
|
7
|
+
alreadyInstalled
|
|
8
|
+
} from "../../../chunk-NUOQXAYR.js";
|
|
9
|
+
import {
|
|
10
|
+
fromModule
|
|
11
|
+
} from "../../../chunk-ELZHIN6E.js";
|
|
12
|
+
|
|
13
|
+
// src/roles/test/setup/file-boundary.ts
|
|
14
|
+
import { afterAll, afterEach, vi } from "vitest";
|
|
15
|
+
var state = globalThis;
|
|
16
|
+
state[SHARED_WORKER] = true;
|
|
17
|
+
var body = globalThis.document?.body;
|
|
18
|
+
if (body !== void 0) {
|
|
19
|
+
body.innerHTML = "";
|
|
20
|
+
body.removeAttribute("style");
|
|
21
|
+
}
|
|
22
|
+
var testingLibrary = fromModule("@testing-library/react");
|
|
23
|
+
if (typeof testingLibrary?.cleanup === "function") afterEach(testingLibrary.cleanup);
|
|
24
|
+
var mocksInThisFile = /* @__PURE__ */ new Set();
|
|
25
|
+
state[CURRENT_FILE_MOCKS] = mocksInThisFile;
|
|
26
|
+
state[PHASE] = "infrastructure";
|
|
27
|
+
var api = vi;
|
|
28
|
+
if (!alreadyInstalled(vi, "file-boundary")) {
|
|
29
|
+
for (const factory of ["fn", "spyOn"]) {
|
|
30
|
+
const create = api[factory].bind(api);
|
|
31
|
+
api[factory] = (...args) => {
|
|
32
|
+
const mock = create(...args);
|
|
33
|
+
register(mock);
|
|
34
|
+
return mock;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
for (const [name, method] of [
|
|
38
|
+
["clearAllMocks", "mockClear"],
|
|
39
|
+
["resetAllMocks", "mockReset"],
|
|
40
|
+
["restoreAllMocks", "mockRestore"]
|
|
41
|
+
]) {
|
|
42
|
+
api[name] = () => {
|
|
43
|
+
for (const mock of currentFileMocks()) mock[method]?.();
|
|
44
|
+
return api;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
afterAll(() => {
|
|
49
|
+
for (const mock of mocksInThisFile) mock.mockRestore?.();
|
|
50
|
+
mocksInThisFile.clear();
|
|
51
|
+
vi.useRealTimers();
|
|
52
|
+
vi.unstubAllGlobals();
|
|
53
|
+
vi.unstubAllEnvs();
|
|
54
|
+
});
|
|
55
|
+
function register(mock) {
|
|
56
|
+
if (state[PHASE] === "infrastructure") return;
|
|
57
|
+
state[CURRENT_FILE_MOCKS]?.add(mock);
|
|
58
|
+
}
|
|
59
|
+
function currentFileMocks() {
|
|
60
|
+
return state[CURRENT_FILE_MOCKS] ?? /* @__PURE__ */ new Set();
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=file-boundary.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/file-boundary.ts"],"sourcesContent":["/**\n * The end of a test file, given back its meaning when Vitest is told not to isolate.\n *\n * ## What isolation was paying for\n *\n * With `isolate: true`, which is Vitest's default, every test file gets its own module registry\n * and its own environment. A file that patches a prototype, leaves fake timers installed, writes\n * `window.google` or empties `document.body` cannot reach the next file, because the next file\n * does not share the things it touched. Nobody writes that down, and no test asserts it: it is\n * bought silently, and it is the single most expensive thing a Vitest run does. Measured on\n * `front-components`, 230 files and 1418 tests, on the same machine and in the same session:\n *\n * jest, files in parallel 15.9 s\n * Vitest, isolated (today) 44.6 s\n * Vitest, this boundary, not isolated 13.0 s\n *\n * So the runner is not slower than jest. Re-evaluating the import graph once per file is, and that\n * is exactly what isolation asks for.\n *\n * ## Why a setup file is the only place this can live\n *\n * Measured: Vitest re-executes each `setupFiles` entry for EVERY test file, even with isolation\n * off, while an ordinary imported module is evaluated once per worker. So a hook registered here\n * re-arms per file, and `afterAll` here IS the end-of-file boundary. A hook registered at the\n * import time of a normal module arms once and never again, which is the whole bug behind\n * Testing Library's cleanup below.\n *\n * ## The five things a file must give back\n *\n * Each line was found by bisecting a real failure on `front-components`, never by guessing:\n *\n * | given back | what leaked without it |\n * | ------------------------------ | ---------------------------------------------------------- |\n * | Testing Library's `cleanup` | it registers at IMPORT time, so it armed for one file only |\n * | an empty `document` | a test writing `document.body.innerHTML`, and focus with it |\n * | spies | `vi.spyOn(Element.prototype, 'scrollIntoView')`, left on |\n * | real timers | a file installing fake timers and never restoring them |\n * | the file's own mock register | `vi.resetAllMocks()` reaching a shared fixture |\n *\n * The global object is given back by `./file-boundary-close.js`, which also closes the\n * infrastructure phase this file opens. The two are one mechanism in two entries because the\n * closing one has to run AFTER the module's own setup files, and `setupFiles` order is the only\n * way to say that.\n *\n * ## What this deliberately does NOT do\n *\n * Not a blanket `vi.restoreAllMocks()` at the boundary. Measured: it also resets the\n * `vi.fn().mockImplementation(...)` that a shared fixture exports at module scope, and 30 tests\n * then fail with `observe is not a function`. Isolation RE-CREATED that fixture, it did not\n * restore it, and those are different actions.\n *\n * Not a substitute for isolating a file that calls `vi.mock`. A module already evaluated in the\n * worker cannot be un-evaluated, so a mock declared by a later file cannot reach it. Those files\n * are routed to an isolated project instead; measured on `front-components`, 13 of 230.\n */\nimport { afterAll, afterEach, vi } from 'vitest'\n\nimport { CURRENT_FILE_MOCKS, PHASE, SHARED_WORKER } from './file-boundary-state.js'\nimport { alreadyInstalled } from './install-once.js'\nimport { fromModule } from './module-copy.js'\n\nconst state = globalThis as Record<symbol, unknown>\n\n// Anything whose lifecycle is the worker's rather than the file's reads this.\nstate[SHARED_WORKER] = true\n\n/* ── A document each file starts from ──────────────────────────────────────────────────────────\n *\n * Testing Library's cleanup removes the containers Testing Library rendered, and nothing else. A\n * test that writes `document.body.innerHTML` itself, or focuses an element, used to be forgiven by\n * the next file getting a new document.\n *\n * Reset at the START of the file rather than at its end, on purpose: a file that crashes never\n * runs its `afterAll`, and the next file should not inherit the wreckage of a file that failed.\n */\n// Reached through `globalThis` rather than the `document` global: sentinel's build emits types\n// without the DOM lib, and this same file is loaded by node-environment suites that have no\n// document at all.\nconst body = (globalThis as { document?: { body?: DocumentBody } }).document?.body\nif (body !== undefined) {\n body.innerHTML = ''\n body.removeAttribute('style')\n}\n\n/* ── Testing Library's cleanup, re-armed ───────────────────────────────────────────────────────\n *\n * `@testing-library/react` registers `afterEach(cleanup)` when it is imported, guarded by\n * `RTL_SKIP_AUTO_CLEANUP`. Without isolation that import happens once per worker, so the hook\n * belongs to whichever file pulled the module in first and every later file renders into a\n * document nobody clears. Measured: 342 of 1418 tests fail, almost all of them\n * `Found multiple elements`.\n *\n * Loaded defensively, the way `./workspace.js` loads `dotenv-flow`: a module that does not render\n * React has no reason to have Testing Library installed, and asking for it would make this file\n * unusable there.\n */\nconst testingLibrary = fromModule('@testing-library/react') as { cleanup?: () => void } | undefined\nif (typeof testingLibrary?.cleanup === 'function') afterEach(testingLibrary.cleanup)\n\n/* ── The mock register, given back its file ────────────────────────────────────────────────────\n *\n * Measured on plain Vitest 4, with no sentinel in the picture, against a\n * `vi.fn().mockImplementation(...)`:\n *\n * vi.clearAllMocks() implementation kept\n * vi.resetAllMocks() implementation DESTROYED\n * vi.restoreAllMocks() implementation kept\n *\n * Under jest that destruction could not outlive the file, because the next file re-evaluated the\n * module and rebuilt the mock. Without isolation it reaches every later file in the worker, so one\n * `vi.resetAllMocks()` in one hook leaves a shared fixture returning `undefined` for the rest of\n * the run. Bisected on `front-components`: `useDebounce.test.ts` does exactly that, and ten tests\n * in `Tabs.test.tsx` die of it.\n *\n * Two registers, therefore. What a setup file builds is infrastructure and is never reset; what\n * the test file builds is the file's own and behaves exactly as it always did. The phase is opened\n * here and closed by `./file-boundary-close.js`, last in `setupFiles`.\n */\nconst mocksInThisFile = new Set<MockLike>()\n\nstate[CURRENT_FILE_MOCKS] = mocksInThisFile\nstate[PHASE] = 'infrastructure'\n\nconst api = vi as unknown as MockApi\n\nif (!alreadyInstalled(vi, 'file-boundary')) {\n for (const factory of ['fn', 'spyOn'] as const) {\n const create = api[factory].bind(api)\n api[factory] = (...args: never[]) => {\n const mock = create(...args)\n register(mock)\n return mock\n }\n }\n\n for (const [name, method] of [\n ['clearAllMocks', 'mockClear'],\n ['resetAllMocks', 'mockReset'],\n ['restoreAllMocks', 'mockRestore'],\n ] as const) {\n api[name] = () => {\n for (const mock of currentFileMocks()) mock[method]?.()\n return api\n }\n }\n}\n\nafterAll(() => {\n /*\n * A spy patches something SHARED: a prototype, a global, another module's export. Isolation undid\n * that by throwing the registry away, so restoring at the boundary is the transposition, not an\n * improvement. Restoring the file's plain `vi.fn()` mocks too is harmless, since the file that\n * made them is over.\n */\n for (const mock of mocksInThisFile) mock.mockRestore?.()\n mocksInThisFile.clear()\n\n vi.useRealTimers()\n vi.unstubAllGlobals()\n vi.unstubAllEnvs()\n})\n\n/** Only what this file does to the document. The DOM lib is not in sentinel's build. */\ninterface DocumentBody {\n innerHTML: string\n removeAttribute: (name: string) => void\n}\n\n/** The part of `vi` this file replaces. Vitest's own overloaded types are not needed to say it. */\ninterface MockApi {\n fn: (...args: never[]) => MockLike\n spyOn: (...args: never[]) => MockLike\n clearAllMocks: () => unknown\n resetAllMocks: () => unknown\n restoreAllMocks: () => unknown\n}\n\n/** Only the part of a mock this file touches; Vitest's own generic types are not needed to say it. */\ninterface MockLike {\n mockClear?: () => unknown\n mockReset?: () => unknown\n mockRestore?: () => unknown\n}\n\n/**\n * Remembers a mock only while the TEST FILE is the one building them.\n *\n * ⚠️ An earlier version kept a second set for the mocks a setup file builds, so the global reset\n * APIs could skip them. It retained them instead: a setup file is re-executed per test file, so a\n * `vi.fn()` written at its module scope is a NEW mock every time, and `front-components` has\n * several (`matchMedia` among them). 230 files later the set held hundreds of mocks, each holding\n * its `mock.calls`, and through them whatever was passed in, DOM nodes included.\n *\n * Nothing ever read that set. Skipping the registration says the same thing and keeps nothing.\n */\nfunction register(mock: MockLike): void {\n if (state[PHASE] === 'infrastructure') return\n ;(state[CURRENT_FILE_MOCKS] as Set<MockLike> | undefined)?.add(mock)\n}\n\nfunction currentFileMocks(): Set<MockLike> {\n return (state[CURRENT_FILE_MOCKS] as Set<MockLike> | undefined) ?? new Set()\n}\n"],"mappings":";;;;;;;;;;;;;AAuDA,SAAS,UAAU,WAAW,UAAU;AAMxC,IAAM,QAAQ;AAGd,MAAM,aAAa,IAAI;AAcvB,IAAM,OAAQ,WAAsD,UAAU;AAC9E,IAAI,SAAS,QAAW;AACtB,OAAK,YAAY;AACjB,OAAK,gBAAgB,OAAO;AAC9B;AAcA,IAAM,iBAAiB,WAAW,wBAAwB;AAC1D,IAAI,OAAO,gBAAgB,YAAY,WAAY,WAAU,eAAe,OAAO;AAqBnF,IAAM,kBAAkB,oBAAI,IAAc;AAE1C,MAAM,kBAAkB,IAAI;AAC5B,MAAM,KAAK,IAAI;AAEf,IAAM,MAAM;AAEZ,IAAI,CAAC,iBAAiB,IAAI,eAAe,GAAG;AAC1C,aAAW,WAAW,CAAC,MAAM,OAAO,GAAY;AAC9C,UAAM,SAAS,IAAI,OAAO,EAAE,KAAK,GAAG;AACpC,QAAI,OAAO,IAAI,IAAI,SAAkB;AACnC,YAAM,OAAO,OAAO,GAAG,IAAI;AAC3B,eAAS,IAAI;AACb,aAAO;AAAA,IACT;AAAA,EACF;AAEA,aAAW,CAAC,MAAM,MAAM,KAAK;AAAA,IAC3B,CAAC,iBAAiB,WAAW;AAAA,IAC7B,CAAC,iBAAiB,WAAW;AAAA,IAC7B,CAAC,mBAAmB,aAAa;AAAA,EACnC,GAAY;AACV,QAAI,IAAI,IAAI,MAAM;AAChB,iBAAW,QAAQ,iBAAiB,EAAG,MAAK,MAAM,IAAI;AACtD,aAAO;AAAA,IACT;AAAA,EACF;AACF;AAEA,SAAS,MAAM;AAOb,aAAW,QAAQ,gBAAiB,MAAK,cAAc;AACvD,kBAAgB,MAAM;AAEtB,KAAG,cAAc;AACjB,KAAG,iBAAiB;AACpB,KAAG,cAAc;AACnB,CAAC;AAmCD,SAAS,SAAS,MAAsB;AACtC,MAAI,MAAM,KAAK,MAAM,iBAAkB;AACtC,EAAC,MAAM,kBAAkB,GAAiC,IAAI,IAAI;AACrE;AAEA,SAAS,mBAAkC;AACzC,SAAQ,MAAM,kBAAkB,KAAmC,oBAAI,IAAI;AAC7E;","names":[]}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import {
|
|
2
|
+
alreadyInstalled
|
|
3
|
+
} from "../../../chunk-NUOQXAYR.js";
|
|
1
4
|
import {
|
|
2
5
|
installWorkspaceSetup
|
|
3
|
-
} from "../../../chunk-
|
|
6
|
+
} from "../../../chunk-DWVRVPMN.js";
|
|
7
|
+
import "../../../chunk-ELZHIN6E.js";
|
|
4
8
|
import {
|
|
5
9
|
clearDeepMocks,
|
|
6
10
|
resetDeepMocks
|
|
@@ -21,6 +25,7 @@ function asConstructable(implementation) {
|
|
|
21
25
|
};
|
|
22
26
|
}
|
|
23
27
|
function installJestConstructorSemantics(vi2) {
|
|
28
|
+
if (alreadyInstalled(vi2, "jest-constructor-semantics")) return;
|
|
24
29
|
const patch = (mock) => {
|
|
25
30
|
const { mockImplementation, mockImplementationOnce } = mock;
|
|
26
31
|
mock.mockImplementation = function(implementation) {
|
|
@@ -48,6 +53,7 @@ function installJestConstructorSemantics(vi2) {
|
|
|
48
53
|
|
|
49
54
|
// src/roles/test/setup/deep-mock-reset.ts
|
|
50
55
|
function installDeepMockReset(vi2) {
|
|
56
|
+
if (alreadyInstalled(vi2, "deep-mock-reset")) return;
|
|
51
57
|
const { resetAllMocks, clearAllMocks } = vi2;
|
|
52
58
|
vi2.resetAllMocks = function extended() {
|
|
53
59
|
const answer = resetAllMocks.call(this);
|
|
@@ -67,6 +73,7 @@ function errorsCompareByMessage(left, right) {
|
|
|
67
73
|
return void 0;
|
|
68
74
|
}
|
|
69
75
|
function installJestErrorEquality(expect2) {
|
|
76
|
+
if (alreadyInstalled(expect2, "jest-error-equality")) return;
|
|
70
77
|
expect2.addEqualityTesters([errorsCompareByMessage]);
|
|
71
78
|
}
|
|
72
79
|
|
|
@@ -90,6 +97,7 @@ function withoutJestOnlyOptions(options) {
|
|
|
90
97
|
return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) };
|
|
91
98
|
}
|
|
92
99
|
function installJestFakeTimerOptions(vi2) {
|
|
100
|
+
if (alreadyInstalled(vi2, "jest-fake-timer-options")) return;
|
|
93
101
|
const inherited = vi2.useFakeTimers.bind(vi2);
|
|
94
102
|
vi2.useFakeTimers = (options) => inherited(withoutJestOnlyOptions(options));
|
|
95
103
|
}
|
|
@@ -115,6 +123,7 @@ function resetLikeJest(mock) {
|
|
|
115
123
|
return mock;
|
|
116
124
|
}
|
|
117
125
|
function installJestMockReset(vi2) {
|
|
126
|
+
if (alreadyInstalled(vi2, "jest-mock-reset")) return;
|
|
118
127
|
for (const name of ["fn", "spyOn"]) {
|
|
119
128
|
const factory = vi2[name].bind(vi2);
|
|
120
129
|
vi2[name] = ((...args) => resetLikeJest(factory(...args)));
|
|
@@ -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\n/** The mocking surface this touches. Vitest's own type is not needed to say it. */\ninterface MockLike {\n mockImplementation(implementation: (...args: unknown[]) => unknown): unknown\n mockImplementationOnce(implementation: (...args: unknown[]) => unknown): unknown\n mockReturnValue(value: unknown): unknown\n mockReturnValueOnce(value: unknown): unknown\n}\n\ninterface ViLike {\n fn(implementation?: (...args: unknown[]) => unknown): MockLike\n /**\n * ⚠️ The REST, not `(target, key)`.\n *\n * Vitest's third argument is the access type, `'get'` or `'set'`, and the first version of the\n * wrapper below forwarded two arguments and dropped it. `vi.spyOn(el, 'scrollWidth', 'get')`\n * then became a spy on the VALUE of an accessor that only exists on a prototype, and jsdom\n * answered `'get scrollWidth' called on an object that is not a valid instance of Element`.\n *\n * Measured on `libs/front/components`, 2 tests, and invisible to every nest module because none\n * of them spies on a DOM accessor.\n */\n spyOn(target: object, ...rest: unknown[]): MockLike\n}\n\n/**\n * Can this function be used with `new`?\n *\n * Asked of the `prototype` property rather than of the source text: an arrow function, a shorthand\n * method and a bound function all lack it, and all three are exactly the cases that throw. A\n * class and a plain `function` have it.\n */\nfunction constructable(value: unknown): boolean {\n return (\n typeof value === 'function' && Object.getOwnPropertyDescriptor(value, 'prototype') !== undefined\n )\n}\n\n/**\n * The same implementation, reachable through `new`.\n *\n * A plain `function` that forwards the call and RETURNS the result. JavaScript's own `new` then\n * hands that object back, which is what jest did, so nothing here imitates jest by hand: it\n * restores the one property the arrow was missing and lets the language do the rest.\n */\nfunction asConstructable(\n implementation: (...args: unknown[]) => unknown,\n): (...args: unknown[]) => unknown {\n if (constructable(implementation)) return implementation\n\n return function forwarded(this: unknown, ...args: unknown[]): unknown {\n return implementation.apply(this, args)\n }\n}\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so every mock they produce accepts `new` the way jest's did.\n *\n * Wrapped at the factory, like `installJestMockReset`, because the behaviour belongs to every mock\n * a suite makes and a suite should not have to ask for it.\n */\nexport function installJestConstructorSemantics(vi: ViLike): void {\n const patch = (mock: MockLike): MockLike => {\n const { mockImplementation, mockImplementationOnce } = mock\n\n mock.mockImplementation = function (implementation) {\n return mockImplementation.call(this, asConstructable(implementation))\n }\n mock.mockImplementationOnce = function (implementation) {\n return mockImplementationOnce.call(this, asConstructable(implementation))\n }\n\n /*\n * Routed through the implementation rather than given a mechanism of its own: the two are the\n * same statement, and one of them already accepts `new`.\n */\n mock.mockReturnValue = function (value) {\n return this.mockImplementation(() => value)\n }\n mock.mockReturnValueOnce = function (value) {\n return this.mockImplementationOnce(() => value)\n }\n return mock\n }\n\n const { fn, spyOn } = vi\n\n vi.fn = function (implementation) {\n return patch(fn.call(this, implementation && asConstructable(implementation)))\n }\n vi.spyOn = function (target, ...rest) {\n return patch(spyOn.call(this, target, ...rest))\n }\n}\n","/**\n * `vi.resetAllMocks()` reaching the deep mocks, the way jest's registry did.\n *\n * ## The divergence, and why it is invisible\n *\n * jest built `jest-mock-extended`'s mocks with `jest.fn()`, so they sat in jest's own registry and\n * `jest.resetAllMocks()` cleared them with everything else. `vitest-mock-extended` builds them its\n * own way, so `vi.resetAllMocks()` walks past them and their call history survives into the next\n * test.\n *\n * Nothing announces it. The suite still runs, and an assertion fails several tests later with a\n * count that is off by exactly what its neighbour did.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does what jest expected:\n *\n * beforeEach(() => mocked.findEvents.mockResolvedValue([]))\n * afterEach(() => vi.resetAllMocks())\n *\n * Eleven tests asserting `toHaveBeenCalledTimes(0)` saw the call left by the one before them. Each\n * PASSES on its own and fails as soon as its neighbour runs first, which is the signature of\n * leakage rather than of a wrong assertion.\n *\n * ## What this installs, and what it leaves alone\n *\n * `resetAllMocks` and `clearAllMocks` do what they did, then extend to the deep mocks: `reset`\n * drops implementations as well as calls, `clear` drops only calls, which is the same distinction\n * the two names already carry.\n *\n * `restoreAllMocks` is NOT extended. It restores spies to their originals, and a deep mock has no\n * original to go back to: it was invented. Extending it would mean deciding what \"restore\" means\n * for something that never existed, which is a claim, not a translation.\n */\nimport { clearDeepMocks, resetDeepMocks } from './mock-extended.js'\n\n/** The part of `vi` this touches. Vitest's own type is not needed to say it. */\ninterface ViLike {\n resetAllMocks(): unknown\n clearAllMocks(): unknown\n}\n\nexport function installDeepMockReset(vi: ViLike): void {\n const { resetAllMocks, clearAllMocks } = vi\n\n vi.resetAllMocks = function extended(this: unknown): unknown {\n const answer = resetAllMocks.call(this)\n resetDeepMocks()\n return answer\n }\n\n vi.clearAllMocks = function extended(this: unknown): unknown {\n const answer = clearAllMocks.call(this)\n clearDeepMocks()\n return answer\n }\n}\n","/**\n * How two `Error` values compare, which the two runners disagree about.\n *\n * A suite that asserts on a thrown or captured error usually writes the error it expects by hand:\n *\n * expect(save).toHaveBeenCalledWith({ error: new AxiosError('Request failed with status code 500'), ... })\n *\n * Under jest that passes whatever else the real error carries. Under Vitest it fails, and the\n * report is 6600 lines of an axios error's `config`, `request` and `response`, which reads like a\n * broken test rather than a runner difference.\n *\n * ## What each runner actually does, measured on the same four cases\n *\n * | two errors | jest 29 | Vitest 4 |\n * | --------------------------------- | -------- | ----------- |\n * | same message, same type | equal | equal |\n * | same message, DIFFERENT types | equal | not equal |\n * | same message, extra properties | equal | not equal |\n * | different messages | not equal| not equal |\n *\n * jest compares errors by their MESSAGE and nothing else: a `TypeError` and a `RangeError` with the\n * same text are equal to it. Vitest compares the type and the own properties too.\n *\n * ## Why the looser rule is the one restored\n *\n * Because it is the one 3481 test files were written against. Tightening it here would turn green\n * tests red during a migration whose whole promise is that the suite means the same thing\n * afterwards, and a baseline gate cannot tell that kind of loss from a real one.\n *\n * The question is reported rather than settled: comparing the type as well would be a better rule,\n * and it may cost nothing on this corpus. That is a measurement to run and a change to make on its\n * own, once the suites no longer move. Measured need so far: `libs/cloud/shared`, whose last\n * missing test was exactly this.\n */\nimport type { expect as ExpectApi } from 'vitest'\n\n/**\n * Restore jest's rule: two errors are equal when their messages are.\n *\n * Returning `undefined` for anything else hands the pair back to the default comparison, which is\n * what an equality tester is expected to do for values it has no opinion about.\n */\nexport function errorsCompareByMessage(left: unknown, right: unknown): boolean | undefined {\n if (left instanceof Error && right instanceof Error) return left.message === right.message\n return undefined\n}\n\nexport function installJestErrorEquality(expect: typeof ExpectApi): void {\n expect.addEqualityTesters([errorsCompareByMessage])\n}\n","/**\n * `useFakeTimers({ doNotFake: [...] })`, which Vitest accepts and ignores.\n *\n * jest names what to LEAVE ALONE, Vitest names what to FAKE. The option Vitest does not know is\n * dropped in silence, so a suite that carefully kept `setTimeout` real gets it faked, and anything\n * awaiting a timer never resolves.\n *\n * Measured on both runners with the same source:\n *\n * useFakeTimers({ doNotFake: ['setTimeout'] }) jest: setTimeout real Vitest: setTimeout FAKED\n * useFakeTimers({ toFake: ['Date'] }) Vitest: setTimeout real\n *\n * Found on `libs/cloud/events-notifications`: 4 tests in one file died on `Test timed out in\n * 5000ms` with nothing else to show, because the code under test awaits a real timer. The repo has\n * 2 files using `doNotFake`, the other in `apps/nest/microservices/institution`.\n *\n * ## The translation, and what it inherits\n *\n * `doNotFake: [a, b]` becomes `toFake: <everything the runner fakes by default> minus [a, b]`. The\n * default set is Vitest's, measured rather than assumed, and NOT jest's, which is wider: jest also\n * fakes `nextTick`, `queueMicrotask` and the animation-frame pair. Subtracting from Vitest's own\n * default is what every other `useFakeTimers()` call in the corpus already gets, so this keeps one\n * behaviour for the whole migration instead of two.\n */\n\n/**\n * What `vi.useFakeTimers()` replaces when told nothing, measured on Vitest 4 by comparing each\n * global before and after the call.\n */\nconst FAKED_BY_DEFAULT = [\n 'setTimeout',\n 'clearTimeout',\n 'setInterval',\n 'clearInterval',\n 'setImmediate',\n 'clearImmediate',\n 'Date',\n 'performance',\n 'hrtime',\n] as const\n\n/** The options both runners take, plus the one only jest knows. */\ninterface TimerOptions {\n toFake?: string[]\n doNotFake?: string[]\n}\n\n/**\n * Turn \"leave these alone\" into \"fake those\", leaving anything else untouched.\n *\n * Exported for its own test: the translation is the whole rule, and asserting it directly says more\n * than asserting that a wrapper was installed.\n */\nexport function withoutJestOnlyOptions<T>(options: T): T {\n const given = options as TimerOptions | undefined\n if (given?.doNotFake === undefined) return options\n\n const { doNotFake, ...rest } = given\n const base = rest.toFake ?? [...FAKED_BY_DEFAULT]\n\n return { ...rest, toFake: base.filter((timer) => !doNotFake.includes(timer)) } as T\n}\n\n/**\n * The one function this touches, named by its shape rather than by Vitest's type.\n *\n * `Options` is the caller's own parameter type: the wrapper hands back exactly what it was given,\n * minus the option Vitest does not know, so it must not narrow what the runner accepts.\n */\ninterface FakeTimerApi<Options> {\n useFakeTimers: (options?: Options) => unknown\n}\n\n/** Wrap `vi.useFakeTimers` so a jest-shaped options object still means what it said. */\nexport function installJestFakeTimerOptions<Options>(vi: FakeTimerApi<Options>): void {\n const inherited = vi.useFakeTimers.bind(vi)\n vi.useFakeTimers = (options?: Options) => inherited(withoutJestOnlyOptions(options))\n}\n","/**\n * The `jest` global, kept alive for helpers that a migrating module is not allowed to edit.\n *\n * ## Why a module cannot solve this for itself\n *\n * The codemod rewrites a module's own test files. It does not rewrite files in OTHER projects, and\n * it must not: a shared helper is imported by modules still on jest, so migrating it would break\n * them, and leaving it breaks the migrated one. That is the constraint the whole per-module plan\n * rests on.\n *\n * But those helpers call the jest API at MODULE scope. `libs/front/tests/src/mocks/**` does\n * `jest.fn()` when it is imported, before any test runs, so a migrated module dies on\n * `ReferenceError: jest is not defined` the moment it imports one.\n *\n * Measured repo-wide, excluding documentation: **50 files use the jest API without being test\n * files**, in `jest.setup.js`, `*.mock.ts`, `*.test-helper.ts`, `*.test-wrapper.ts`. Three\n * independent hand migrations reached this same line without knowing about each other:\n * `libs/front/components` (8 shared helpers, 16 sites), `apps/nest/microservices/mission` and\n * `apps/nest/backends-for-frontends/admin`.\n *\n * ## It is `vi`, not a fake jest\n *\n * The global IS Vitest's `vi`, so anything Vitest does not have keeps failing loudly:\n * `jest.requireActual` and `jest.isolateModules` are still errors, and a module relying on them\n * still has to be migrated properly. Handing over a hand-written imitation would turn those into\n * silent wrong behaviour, which is the opposite of the point.\n *\n * ## ⚠️ What it does NOT cover, and this bound is measured\n *\n * `jest.mock()`. Vitest hoists mock registrations above the imports by scanning the source\n * STATICALLY, and that scan only recognises the receivers `vi` and `vitest` (`@vitest/mocker`,\n * `hoistMocksPlugin`). A `jest.mock()` left in place is therefore NOT hoisted: it runs after the\n * imports it was meant to intercept and does nothing at all, in silence. Measured on\n * `apps/front/front-legacy`, where 216 of 427 files call it.\n *\n * So this covers a helper that CALLS the jest API. It does not make an unmigrated test file work,\n * and the codemod's rename stays load-bearing rather than cosmetic.\n */\n\n/** The part of `vi` this installs. Vitest's own type is not needed to say it. */\ntype JestLike = object\n\n/**\n * Put `vi` on `globalThis` under the name `jest`.\n *\n * Assigned rather than defined with a getter: a helper may well write to it (`jest.fn = ...` in a\n * test double), and a getter-only property would throw where jest allowed it.\n */\nexport function installJestGlobal(vi: JestLike): void {\n ;(globalThis as Record<string, unknown>).jest = vi\n}\n","/**\n * What `mockReset()` leaves behind, which is where the two runners disagree most dangerously.\n *\n * jest REMOVES the implementation: a reset spy returns `undefined` and the real function is not\n * called. Vitest puts the ORIGINAL implementation back: a reset spy calls the real function again.\n *\n * Measured on both runners with the same source:\n *\n * after resetAllMocks() on a spy jest: undefined Vitest: the real function\n * after mockReset() on a spy jest: undefined Vitest: the real function\n * after mockReset() on fn(impl) jest: undefined Vitest: impl\n *\n * The shape this breaks is ordinary and common: a suite spies on a provider in `beforeAll` and\n * resets its mocks in `beforeEach`. Under jest the provider stayed neutralised for every test.\n * Under Vitest the first `beforeEach` hands the real provider back, and every test after it runs\n * the real code. Measured on `libs/cloud/events-notifications`, that meant real HTTP: 19 tests\n * failed on `captured a request without a matching request handler` for the hermes API and 23 more\n * timed out waiting on it. Nothing in any report named a reset.\n *\n * 240 files in this repo both spy and reset, in every family: 111 under `apps/nest`, 77 under\n * `libs/cloud`, 31 under `apps/front`.\n *\n * ## Why here and not in the 240 files\n *\n * Because a codemod would have to decide, per spy, whether the suite wanted the real function\n * back, and the answer is in the test's intent rather than in its text. The runner-level rule is\n * the one that was true for all 240 while they were written, so restoring it is the transcription\n * and rewriting them would be the guess.\n *\n * Vitest routes `vi.resetAllMocks()` through each mock's own `mockReset`, measured, so overriding\n * that method covers the bulk form as well as the direct one. `mockRestore` is untouched: it puts\n * the original back under both runners, which is what it is for.\n */\n\n/** The part of a mock this file touches. Vitest's own types are not needed to say it. */\ninterface ResettableMock {\n mockReset: () => unknown\n mockImplementation: (fn: (...args: unknown[]) => unknown) => unknown\n}\n\nfunction isResettable(value: unknown): value is ResettableMock {\n return (\n typeof value === 'function' &&\n typeof (value as Partial<ResettableMock>).mockReset === 'function' &&\n typeof (value as Partial<ResettableMock>).mockImplementation === 'function'\n )\n}\n\n/**\n * Make one mock forget its implementation on reset, as jest's did.\n *\n * The override is installed on the instance rather than on a prototype: mocks are functions with\n * their own properties, and there is no shared prototype to reach.\n */\nfunction resetLikeJest<T>(mock: T): T {\n if (!isResettable(mock)) return mock\n\n const inherited = mock.mockReset.bind(mock)\n mock.mockReset = () => {\n inherited()\n mock.mockImplementation(() => undefined)\n return mock\n }\n return mock\n}\n\n/** The two factories a suite gets its mocks from. */\ntype MockFactories = { fn: (...args: never[]) => unknown; spyOn: (...args: never[]) => unknown }\n\n/**\n * Wrap `vi.fn` and `vi.spyOn` so everything they hand out resets the way jest's did.\n *\n * Called with the `vi` a setup file imports, so nothing here reaches for a global.\n */\nexport function installJestMockReset(vi: MockFactories): void {\n for (const name of ['fn', 'spyOn'] as const) {\n const factory = vi[name].bind(vi) as (...args: never[]) => unknown\n vi[name] = ((...args: never[]) => resetLikeJest(factory(...args))) as MockFactories[typeof name]\n }\n}\n","/**\n * A rejected value that is a FUNCTION, which jest calls and Vitest does not.\n *\n * ## The divergence, measured rather than reasoned\n *\n * jest's `toThrow` decides what was thrown like this: under `.rejects` it uses the rejection\n * reason ONLY when that reason is an Error. Otherwise it falls through to the ordinary branch,\n * sees a function, and CALLS it, asserting on whatever that call throws.\n *\n * Probed on this repo under jest 29, with controls:\n *\n * reject(() => { throw new Error('some error') })\n * await expect(…).rejects.toThrow(new Error('some error')) -> passes\n * await expect(…).rejects.toThrow(new Error('other text')) -> fails\n * await expect(…).rejects.toThrow('other text') -> fails\n *\n * The two controls are what make the first line mean something: jest is not passing everything,\n * it really is comparing the message of the error the CALL produced.\n *\n * Vitest treats the rejection reason as the thrown value, so the assertion is made against a\n * function. A function has no `message`, and the failure reads\n * `Cannot read properties of undefined (reading 'indexOf')`, which names nothing near the cause.\n *\n * ## Where it shows, and what it is worth\n *\n * Measured across the nest campaign: 4 tests in 3 modules.\n * `apps/nest/microservices/worker` (1), `apps/nest/microservices/institution` (2) and\n * `apps/nest/microservices/mission` (1). All four write the same shape, a mock rejecting with a\n * thunk that throws, or a `throw <a function>`.\n *\n * ## What this changes, and what it cannot\n *\n * Only a case that CANNOT work today: under `.rejects`, a reason that is a function and not an\n * Error. Vitest has no useful behaviour there, so nothing that passes today changes shape. An\n * Error reason, a string, an object, a rejected value of any other kind, and every assertion\n * outside `.rejects` all reach Vitest's own matcher untouched.\n *\n * ⚠️ It is jest's behaviour, not a good one. A suite reaching it is asserting on a function it\n * never meant to hand over, and it passed by accident of the runner. Reproducing it is what keeps\n * the migration honest: the gate promises the suite means the same thing afterwards, and a test\n * that was green cannot be turned red by us and called a finding. The teams own the cleanup.\n */\nimport { chai } from 'vitest'\n\n/** The part of a chai assertion this touches. Chai's own types are not needed to say it. */\ninterface AssertionLike {\n _obj: unknown\n}\n\ntype Matcher = (this: AssertionLike, ...args: unknown[]) => unknown\n\n/**\n * What calling the function throws, or the function itself when it throws nothing.\n *\n * Returning it unchanged matters: a function that completes is not \"nothing was thrown\", and\n * handing Vitest the same value it had leaves the report exactly as it would have been.\n */\nfunction thrownByCalling(candidate: () => unknown): unknown {\n try {\n candidate()\n } catch (thrown) {\n return thrown\n }\n return candidate\n}\n\nexport function installJestRejectedFunction(): void {\n const { Assertion, util } = chai as unknown as {\n Assertion: { prototype: Record<string, unknown> }\n util: { flag(object: unknown, key: string): unknown }\n }\n\n for (const name of ['toThrow', 'toThrowError']) {\n const original = Assertion.prototype[name] as Matcher | undefined\n if (typeof original !== 'function') continue\n\n Assertion.prototype[name] = function patched(this: AssertionLike, ...args: unknown[]): unknown {\n const reason = this._obj\n const rejected = util.flag(this, 'promise') === 'rejects'\n\n if (rejected && typeof reason === 'function' && !(reason instanceof Error)) {\n this._obj = thrownByCalling(reason as () => unknown)\n }\n\n return original.apply(this, args)\n } as unknown as Matcher\n }\n}\n"],"mappings":"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","names":["vi","vi","expect","vi","vi","vi"]}
|
|
1
|
+
{"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 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/setup/msw-lifecycle.ts"],"sourcesContent":["/**\n * The msw lifecycle, as its OWN setup file, loaded only by a module that uses msw.\n *\n * ## Why it moved out of the shared setup\n *\n * It used to run for every adopted module, and that was wrong in a way only a real module could\n * show. Installing msw means patching `http` and `https` and refusing any request that matches no\n * handler. A module that never asked for msw gets that refusal anyway, and its own traffic is what\n * pays.\n *\n * Measured on `libs/nest/starter`, whose suite talks to a Fastify server it starts itself:\n *\n * its own config 11 of 11\n * the same config plus the shared setup 10 of 11, `TypeError: Invalid URL`\n * inside @mswjs/interceptors' fetch interceptor\n *\n * That module has no msw handler anywhere. The failing test does\n * `new URL(`${prefix}/events`, await app.getUrl())` and fetches its own server, and the\n * interceptor cannot read that URL.\n *\n * So the rule is the one asked for at the start: intelligent per module, decided from what the\n * module's files actually contain, not applied to everybody because it is convenient.\n *\n * ## What it does NOT change\n *\n * The lifecycle itself is untouched: `listen` at evaluation time AND in `beforeAll`,\n * `onUnhandledRequest: 'error'` carried across, no `resetHandlers`. `./msw.js` carries the\n * measurement for each of those.\n */\nimport { afterAll, afterEach, beforeAll } from 'vitest'\n\nimport { installMswLifecycle, server } from './msw.js'\n\ninstallMswLifecycle({ beforeAll, afterEach, afterAll })\n\n/**\n * Re-exported so a suite can add its own handlers, which is how all 600 files that touch msw here\n * already work: `server.use(...)` inside a test.\n */\nexport { server }\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/msw-lifecycle.ts"],"sourcesContent":["/**\n * The msw lifecycle, as its OWN setup file, loaded only by a module that uses msw.\n *\n * ## Why it moved out of the shared setup\n *\n * It used to run for every adopted module, and that was wrong in a way only a real module could\n * show. Installing msw means patching `http` and `https` and refusing any request that matches no\n * handler. A module that never asked for msw gets that refusal anyway, and its own traffic is what\n * pays.\n *\n * Measured on `libs/nest/starter`, whose suite talks to a Fastify server it starts itself:\n *\n * its own config 11 of 11\n * the same config plus the shared setup 10 of 11, `TypeError: Invalid URL`\n * inside @mswjs/interceptors' fetch interceptor\n *\n * That module has no msw handler anywhere. The failing test does\n * `new URL(`${prefix}/events`, await app.getUrl())` and fetches its own server, and the\n * interceptor cannot read that URL.\n *\n * So the rule is the one asked for at the start: intelligent per module, decided from what the\n * module's files actually contain, not applied to everybody because it is convenient.\n *\n * ## What it does NOT change\n *\n * The lifecycle itself is untouched: `listen` at evaluation time AND in `beforeAll`,\n * `onUnhandledRequest: 'error'` carried across, no `resetHandlers`. `./msw.js` carries the\n * measurement for each of those.\n */\nimport { afterAll, afterEach, beforeAll } from 'vitest'\n\nimport { installMswLifecycle, server } from './msw.js'\n\ninstallMswLifecycle({ beforeAll, afterEach, afterAll })\n\n/**\n * Re-exported so a suite can add its own handlers, which is how all 600 files that touch msw here\n * already work: `server.use(...)` inside a test.\n */\nexport { server }\n"],"mappings":";;;;;;;AA6BA,SAAS,UAAU,WAAW,iBAAiB;AAI/C,oBAAoB,EAAE,WAAW,WAAW,SAAS,CAAC;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/setup/msw-server.ts"],"sourcesContent":["/**\n * The msw server a migrated suite shares with its setup.\n *\n * ## Why this entry exists at all\n *\n * The setup beside it starts a server and the tests add handlers to one. If those are two different\n * instances, the one that listens has no handlers and every request is unhandled, which under\n * `onUnhandledRequest: 'error'` fails every test that touches HTTP.\n *\n * That is not hypothetical. Migrating `apps/cloud/agency` and running its suite produced exactly\n * it: 22 green tests under jest became 10 failures, with `[MSW] Cannot bypass a request when using\n * the \"error\" strategy`. The setup had been absorbed into sentinel while the 599 files that import\n * the server still pointed at the workspace's own instance.\n *\n * So the server is exported under its own name, and the codemod repoints those imports here. One\n * instance, listened to by the setup and used by the tests.\n */\nexport { server } from './msw.js'\n\n/**\n * msw's own API, re-exported, so a module gets its handlers from the SAME physical copy that\n * intercepts.\n *\n * sentinel ships msw as a dependency on purpose: a module must stand alone, and the workspace root\n * that used to provide it is going away. But a test that keeps `import { rest } from 'msw'` builds\n * its handlers with the WORKSPACE's copy while the server that listens is built with sentinel's.\n * Same version, two physical instances, one interceptor: the handler is never matched.\n *\n * Measured on `libs/cloud/shared`: the msw handler is never called and the test fails on\n * `expected \"vi.fn()\" to be called 1 times, but got 0 times`. Same family as the axios duplicate,\n * where `resolve.dedupe` changed nothing and only pointing at one physical path did.\n *\n * `export *` rather than a list: `rest` covers 669 of the 671 importing files here, but the\n * surface a test may need (`graphql`, `ctx`, the handler types) belongs to msw, not to a list\n * sentinel would have to keep in step. `setupServer` is not part of it: it lives in `msw/node`,\n * and the server is sentinel's to create.\n */\nexport * from 'msw'\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/msw-server.ts"],"sourcesContent":["/**\n * The msw server a migrated suite shares with its setup.\n *\n * ## Why this entry exists at all\n *\n * The setup beside it starts a server and the tests add handlers to one. If those are two different\n * instances, the one that listens has no handlers and every request is unhandled, which under\n * `onUnhandledRequest: 'error'` fails every test that touches HTTP.\n *\n * That is not hypothetical. Migrating `apps/cloud/agency` and running its suite produced exactly\n * it: 22 green tests under jest became 10 failures, with `[MSW] Cannot bypass a request when using\n * the \"error\" strategy`. The setup had been absorbed into sentinel while the 599 files that import\n * the server still pointed at the workspace's own instance.\n *\n * So the server is exported under its own name, and the codemod repoints those imports here. One\n * instance, listened to by the setup and used by the tests.\n */\nexport { server } from './msw.js'\n\n/**\n * msw's own API, re-exported, so a module gets its handlers from the SAME physical copy that\n * intercepts.\n *\n * sentinel ships msw as a dependency on purpose: a module must stand alone, and the workspace root\n * that used to provide it is going away. But a test that keeps `import { rest } from 'msw'` builds\n * its handlers with the WORKSPACE's copy while the server that listens is built with sentinel's.\n * Same version, two physical instances, one interceptor: the handler is never matched.\n *\n * Measured on `libs/cloud/shared`: the msw handler is never called and the test fails on\n * `expected \"vi.fn()\" to be called 1 times, but got 0 times`. Same family as the axios duplicate,\n * where `resolve.dedupe` changed nothing and only pointing at one physical path did.\n *\n * `export *` rather than a list: `rest` covers 669 of the 671 importing files here, but the\n * surface a test may need (`graphql`, `ctx`, the handler types) belongs to msw, not to a list\n * sentinel would have to keep in step. `setupServer` is not part of it: it lives in `msw/node`,\n * and the server is sentinel's to create.\n */\nexport * from 'msw'\n"],"mappings":";;;;;;AAqCA,cAAc;","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../../src/roles/test/setup/workspace-entry.ts"],"sourcesContent":["/**\n * What the WORKSPACE's shared jest setup did, for the modules that actually loaded it.\n *\n * Separate from `setup/jest-parity` on purpose, and the split is the whole point. `setup/jest-parity` carries the\n * runner shims: `jest.*` semantics restored under Vitest, which every migrated module needs whatever\n * its config said. This entry carries the ZONE that `jest.setup.after.env.js` pinned at the workspace root, which\n * only a module whose jest config NAMED that file ever had:\n *\n * require('dotenv-flow').config(...) the .env cascade\n * Settings.defaultZone = 'utc' luxon's default zone\n *\n * Measured on this repo: 97 modules name the root setup, 7 name a setup of their own instead. For\n * those 7 the pin was never applied, and applying it in the migration changes what the suite does\n * while claiming to move it.\n *\n * `libs/front/logic` is the one that said so out loud. It has its own setup, uses luxon, and its\n * test is literally called \"formats them in the local zone\":\n *\n * expect(formatTimeOfDay(new Date(2024, 2, 1, 8, 5))).toBe('08:05')\n * // pinned to utc: '07:05'\n *\n * One test, and it would have been one silent hour of offset in any suite that did not assert it.\n */\nimport { pinWorkspaceTimezone } from './workspace.js'\n\n/*\n * Awaited at the top level, so the environment is loaded before the first test file is imported.\n * Deferring it to a `beforeAll` would be too late: a module read at import time would already have\n * captured an unset variable.\n */\nawait pinWorkspaceTimezone()\n"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../../src/roles/test/setup/workspace-entry.ts"],"sourcesContent":["/**\n * What the WORKSPACE's shared jest setup did, for the modules that actually loaded it.\n *\n * Separate from `setup/jest-parity` on purpose, and the split is the whole point. `setup/jest-parity` carries the\n * runner shims: `jest.*` semantics restored under Vitest, which every migrated module needs whatever\n * its config said. This entry carries the ZONE that `jest.setup.after.env.js` pinned at the workspace root, which\n * only a module whose jest config NAMED that file ever had:\n *\n * require('dotenv-flow').config(...) the .env cascade\n * Settings.defaultZone = 'utc' luxon's default zone\n *\n * Measured on this repo: 97 modules name the root setup, 7 name a setup of their own instead. For\n * those 7 the pin was never applied, and applying it in the migration changes what the suite does\n * while claiming to move it.\n *\n * `libs/front/logic` is the one that said so out loud. It has its own setup, uses luxon, and its\n * test is literally called \"formats them in the local zone\":\n *\n * expect(formatTimeOfDay(new Date(2024, 2, 1, 8, 5))).toBe('08:05')\n * // pinned to utc: '07:05'\n *\n * One test, and it would have been one silent hour of offset in any suite that did not assert it.\n */\nimport { pinWorkspaceTimezone } from './workspace.js'\n\n/*\n * Awaited at the top level, so the environment is loaded before the first test file is imported.\n * Deferring it to a `beforeAll` would be too late: a module read at import time would already have\n * captured an unset variable.\n */\nawait pinWorkspaceTimezone()\n"],"mappings":";;;;;;;AA8BA,MAAM,qBAAqB;","names":[]}
|
|
@@ -21,6 +21,19 @@ interface SharedTestOptions {
|
|
|
21
21
|
* plugin for all of them took the suite from 98s to 249s.
|
|
22
22
|
*/
|
|
23
23
|
lowerDecoratorsWithTypeScript?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Let the files of this suite share a worker instead of each getting its own registry.
|
|
26
|
+
*
|
|
27
|
+
* Off by default, which is Vitest's own default and jest's semantics. Turning it off buys the
|
|
28
|
+
* whole of `./setup/file-boundary.js` and `./isolation-split.js`: measured on
|
|
29
|
+
* `front-components`, 44.6s isolated against 11.0s split, with all 1418 tests green and the
|
|
30
|
+
* same result across ten shuffled file orders.
|
|
31
|
+
*
|
|
32
|
+
* A module takes it deliberately, and proves it with `--validate`, because the switch is the one
|
|
33
|
+
* thing in this preset that changes what a suite MEANS: a file can see what an earlier file left
|
|
34
|
+
* behind, and only running the suite can say whether any of them does.
|
|
35
|
+
*/
|
|
36
|
+
isolate?: boolean;
|
|
24
37
|
/** Merged over the base. For what a module genuinely needs to differ on, nothing else. */
|
|
25
38
|
overrides?: ViteUserConfig;
|
|
26
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hublo/sentinel",
|
|
3
|
-
"version": "1.4.0-alpha.
|
|
3
|
+
"version": "1.4.0-alpha.36",
|
|
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",
|
|
@@ -58,6 +58,14 @@
|
|
|
58
58
|
"types": "./dist/roles/test/setup/jest-parity.d.ts",
|
|
59
59
|
"import": "./dist/roles/test/setup/jest-parity.js"
|
|
60
60
|
},
|
|
61
|
+
"./test/setup/file-boundary": {
|
|
62
|
+
"types": "./dist/roles/test/setup/file-boundary.d.ts",
|
|
63
|
+
"import": "./dist/roles/test/setup/file-boundary.js"
|
|
64
|
+
},
|
|
65
|
+
"./test/setup/file-boundary-close": {
|
|
66
|
+
"types": "./dist/roles/test/setup/file-boundary-close.d.ts",
|
|
67
|
+
"import": "./dist/roles/test/setup/file-boundary-close.js"
|
|
68
|
+
},
|
|
61
69
|
"./test/msw": {
|
|
62
70
|
"types": "./dist/roles/test/setup/msw-server.d.ts",
|
|
63
71
|
"import": "./dist/roles/test/setup/msw-server.js"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/roles/test/shared-test-config.ts","../src/roles/test/msw-server-redirect.ts","../src/roles/test/react/jest-export-conditions.ts"],"sourcesContent":["/**\n * The Vitest config a Nest module gets, as one call instead of forty lines copied 133 times.\n *\n * The React family needed none of this: those modules already had a Vitest config, so adoption\n * moved its imports and changed nothing else. The Jest family has no config at all, so one has to\n * be WRITTEN, and writing the same four decisions into 133 files is how they drift. A module that\n * genuinely differs still can: `overrides` is merged over the base, the same shape `nestService`\n * uses on the build side.\n *\n * Every line below was established by running a real suite, not by reading a guide. `mission` is\n * the reference: 387 specs, the module the build role refused for having its own webpack config,\n * and the one with the only `isolateModules` in the repo.\n */\nimport { existsSync } from 'node:fs'\nimport { createRequire } from 'node:module'\nimport { dirname, join } from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nimport type { Alias as ViteAlias, AliasOptions } from 'vite'\nimport type { ViteUserConfig } from 'vitest/config'\n\nimport { decoratorMetadata } from '../build/nest/decorator-metadata.js'\nimport { tsconfigAliases } from '../build/nest/tsconfig-aliases.js'\nimport type { Alias } from '../build/nest/tsconfig-aliases.js'\n\nimport { mswServerRedirect } from './msw-server-redirect.js'\nimport { jestExportConditions } from './react/jest-export-conditions.js'\n\n/** Which family this config is for: it decides the three things that are not shared. */\nexport type TestFlavour = 'nest' | 'react'\n\nexport interface SharedTestOptions {\n /** Which family this config is for. It decides the three things that are not shared. */\n flavour?: TestFlavour\n /** The module's own directory: where its specs live and what its config is relative to. */\n root: string\n /** The workspace root, which is where `tsconfig.base.json` and its path aliases are. */\n workspaceRoot: string\n /**\n * Lower decorators with TypeScript before oxc sees them, for a module that needs it.\n *\n * ⚠️ Written by `--init --test` from the module's own sources, never by hand, because the\n * condition is not a preference: it is whether a decorator sits on an `abstract` class member,\n * the one construct oxc does not reproduce. See `generate-config.ts` for the measurement.\n *\n * Off by default, and that default is the measured one: on\n * `apps/nest/microservices/mission`, 715 of 716 decorated files need nothing, and running the\n * plugin for all of them took the suite from 98s to 249s.\n */\n lowerDecoratorsWithTypeScript?: boolean\n /** Merged over the base. For what a module genuinely needs to differ on, nothing else. */\n overrides?: ViteUserConfig\n}\n\n/** A file shipped beside this module in the build, or nothing when running from source. */\nfunction siblingFile(relative: string): string | undefined {\n /*\n * ⚠️ Several candidates, because the EMITTED layout is not ours to predict. This file is imported\n * by two toolchains, so the bundler extracts it into a shared chunk at the package root, and it\n * does so even when it is declared as its own entry (the entry then only re-exports the chunk).\n * A single `import.meta.url`-relative path therefore resolves differently depending on where the\n * bundler decided to put the code.\n *\n * That is not theoretical: it silently broke the jest-mock-extended and msw-server adapters, and\n * `apps/cloud/shift-offer` went from 83/83 to 56/83 with \"The property X is not defined on the\n * object\" — a mockDeep shape error that names nothing near the cause. The alias had quietly\n * fallen back to the raw fork.\n *\n * So the file is LOOKED FOR rather than computed, and the first one that exists wins.\n */\n const candidates = [\n relative,\n `roles/test/${relative.replace(/^\\.\\//, '')}`,\n `../${relative.replace(/^\\.\\//, '')}`,\n ]\n for (const candidate of candidates) {\n try {\n const sibling = fileURLToPath(new URL(candidate, import.meta.url))\n if (existsSync(sibling)) return sibling\n } catch {\n continue\n }\n }\n return undefined\n}\n\n/** A package resolved from SENTINEL, so the module gets the copy the setup uses. */\nfunction resolveFrom(specifier: string): string | undefined {\n try {\n return fileURLToPath(import.meta.resolve(specifier))\n } catch {\n try {\n return createRequire(import.meta.url).resolve(specifier)\n } catch {\n return undefined\n }\n }\n}\n\n/**\n * sentinel's own adapter around the fork, which the alias points at when it has been built.\n *\n * The adapter only makes a deep mock answer `in` and `getOwnPropertyDescriptor`, which Vitest's\n * `spyOn` asks and jest's did not; `mock-extended.ts` carries the measurement. Resolved as a\n * sibling file rather than by specifier, because it is not a package.\n *\n * Falls back to the fork itself when there is no build beside this module, which is the case while\n * sentinel's own suite runs from `src`. The alias then behaves exactly as it did before the\n * adapter existed, so a missing build degrades instead of throwing.\n */\nfunction mockExtendedAdapterPath(): string | undefined {\n return siblingFile('./setup/mock-extended.js')\n}\n\n/**\n * The ABSOLUTE path of the Vitest fork, resolved from sentinel rather than named.\n *\n * A bare specifier in an alias is resolved by Vite against the IMPORTER, which here is a spec file\n * inside the adopted module. `vitest-mock-extended` is sentinel's dependency, not the module's, so\n * under pnpm's isolated layout the module cannot see it and the alias points at nothing.\n *\n * Measured on `apps/cloud/agency`: three suites died with \"Cannot find package\n * 'jest-mock-extended'\" while the fork sat in the store, installed and unreachable. Same class of\n * defect as the axios alias below, and the same fix: resolve it where it actually is.\n *\n * Falls back to the bare name, which keeps the previous behaviour rather than throwing while a\n * config is being built.\n */\nfunction vitestMockExtendedPath(): string {\n /*\n * `import.meta.resolve` and not `createRequire().resolve`: the latter follows the `require`\n * condition and hands back the package's CJS entry, whose first line requires `vitest`. Vitest\n * refuses that outright (\"Vitest cannot be imported in a CommonJS module using require()\"), so\n * the alias resolved correctly and then failed one step later. Measured on `apps/cloud/agency`.\n */\n try {\n return fileURLToPath(import.meta.resolve('vitest-mock-extended'))\n } catch {\n try {\n return createRequire(import.meta.url).resolve('vitest-mock-extended')\n } catch {\n return 'vitest-mock-extended'\n }\n }\n}\n\n/**\n * The ONE msw copy a module's tests share with the server that listens.\n *\n * sentinel ships msw so that a module stands alone, and the codemod repoints the SPEC files to it.\n * Helpers are not spec files, so they keep `import { rest } from 'msw'` and build their handlers\n * with the WORKSPACE's copy while the server that listens was built with sentinel's. Same version,\n * two physical instances, and a handler registered on one is invisible to the other.\n *\n * Measured on `apps/cloud/shift-offer`, whose `setup-mocks.ts` registers every handler its suites\n * rely on: 19 tests failed on `captured a request without a matching request handler` for a URL the\n * helper had a handler for. The repo has 159 non-spec files importing msw, and 2 importing the\n * workspace's server directly, so this is not one module's habit.\n *\n * Aliased rather than codemodded, for the same reason as `jest-mock-extended` above: it lets an\n * UNMIGRATED shared helper keep working inside a migrated module, which is what makes the\n * per-module plan possible at all. `^msw$` only: `msw/node` stays the package's own, and it is\n * sentinel's setup that imports it.\n */\n/** sentinel's own server file, the one its setup starts listening on. */\nfunction mswServerFile(): string | undefined {\n return siblingFile('./setup/msw-server.js')\n}\n\nfunction mswAliases(): Alias[] {\n const server = mswServerFile()\n const own = resolveFrom('msw')\n return [\n ...(own === undefined ? [] : [{ find: /^msw$/, replacement: own }]),\n ...(server === undefined ? [] : [{ find: /^@hublo\\/test\\/msw\\/server$/, replacement: server }]),\n ]\n}\n\n/**\n * One PHYSICAL axios, resolved from the MODULE and not from sentinel.\n *\n * pnpm installs axios twice here, same 1.17.0, differing only by a peer. The service client\n * resolves one copy and a provider's `import { AxiosError } from 'axios'` the other, so\n * `if (!(error instanceof AxiosError)) throw error` answers false on a genuine axios error and\n * rethrows it raw. The test then fails on the WRAPPER being absent, naming nothing near the cause.\n *\n * `resolve.dedupe` does NOT fix this, and neither does `deps.inline`: dedupe settles bare\n * specifiers, not two physical paths. The alias has to name the path.\n *\n * The module's OWN `node_modules`, checked directly rather than resolved.\n *\n * `createRequire(...).resolve()` was the first version and it is wrong twice. It walks UP the\n * tree, so a module without axios would be aliased to a PARENT's copy, which is the opposite of\n * pinning its own. And because it consults ambient resolution, the answer depends on where the\n * process runs: it correctly found nothing from a plain CLI run and found something under Vitest,\n * which is how a test of it became a test of its environment.\n *\n * pnpm links a module's direct dependencies into `<module>/node_modules`, so that path is both the\n * right answer and a deterministic one.\n *\n * Measured on `mission`: 378 passing suites to 381 of 387.\n *\n * ## ⚠️ And the workspace root when the module has none of its own\n *\n * The first version stopped there and returned nothing for a module without its own axios, on the\n * reasoning that pinning it to a PARENT's copy is the opposite of pinning its own. That reasoning\n * has an blind spot: a module with no copy of its own does not thereby have no axios PROBLEM. It\n * receives one through its dependencies, and it is still importing it two ways.\n *\n * Measured in `apps/nest/backends-for-frontends/backoffice`, which declares no axios, under this\n * very preset with no alias:\n *\n * the ESM AxiosError === the CJS AxiosError false\n * a CJS error instanceof the CJS class true\n * a CJS error instanceof the ESM class FALSE\n *\n * `@nestjs/axios` is externalised, so Node loads it and it throws the CJS class; the provider does\n * `import { AxiosError } from 'axios'` through Vite and gets the ESM one. Its\n * `if (error instanceof AxiosError) throw new RemoteBackendException(...)` therefore answers false\n * on a genuine axios error and rethrows it raw, and the test fails on the WRAPPER being absent,\n * naming nothing near the cause. Four modules reported it: `admin`, `backoffice`, `mission`,\n * `institution`.\n *\n * So: the module's own copy first, the workspace root second, nearest wins. That is the same shape\n * as `luxonAlias` above, and for the same reason: when a module has nothing of its own, the copy\n * it actually uses is the one to pin. Absent axios everywhere is still not an error.\n */\n/**\n * ONE physical luxon, for the same reason as axios: `Settings` is per-copy state.\n *\n * The setup pins `Settings.defaultZone = 'utc'`, which the root jest setup did for every suite in\n * the repo. It was reaching the copy `require` resolves, the CJS build, while the tests import the\n * one Vite resolves, the ESM build. Two instances, two `Settings`, and the pin landed on the one\n * nobody read.\n *\n * Measured on `libs/cloud/events-notifications`: the suite ran in `Europe/Paris`, so every date\n * came out two hours off and 12 assertions compared timestamps that differed by exactly that.\n *\n * luxon sits at the workspace root here rather than in each module, so both places are checked,\n * nearest first. Absent luxon is not an error: most modules have none.\n */\nfunction luxonAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'luxon', 'package.json')\n if (existsSync(manifest)) return [{ find: /^luxon$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\nfunction axiosAlias(root: string, workspaceRoot: string): Alias[] {\n for (const base of [root, workspaceRoot]) {\n const manifest = join(base, 'node_modules', 'axios', 'package.json')\n if (existsSync(manifest)) return [{ find: /^axios$/, replacement: dirname(manifest) }]\n }\n return []\n}\n\n/**\n * A generated Prisma client's runtime, resolved to the file the package actually ships.\n *\n * The generated client reaches its own runtime by package name, and the package's `exports` map\n * answers differently depending on which condition asks:\n *\n * \"./runtime/library\": { \"require\": \"./runtime/library.js\", // shipped\n * \"import\": \"./runtime/library.mjs\" } // NOT shipped\n *\n * jest asked as CJS and got the file. Vitest resolves the same specifier under `import`, is sent\n * to a `.mjs` that does not exist, and the spec file does not load at all, so its tests go missing\n * rather than failing. Node itself spells the answer out: \"Did you mean to import\n * .../runtime/library.js?\".\n *\n * Measured on `apps/nest/microservices/client-management` (1 file) and\n * `apps/nest/microservices/institution` (3 files), on two different generated clients. This repo\n * generates 31 of them, all with the same manifest, so this is a property of the generator rather\n * than of a module.\n *\n * Written as a pattern with a back-reference, not one entry per client: the module does not know\n * which clients its dependencies pull in, and enumerating 31 names would go stale the day a\n * thirty-second schema is added.\n *\n * ⚠️ Not a workaround for a mistake of ours. The package declares a target it does not ship, and\n * pointing at the shipped file is what `require` already did. Nothing else changes: the `.js` IS\n * the runtime, in the same package, at the version installed.\n */\nfunction prismaRuntimeAlias(workspaceRoot: string): Alias[] {\n const prisma = join(workspaceRoot, 'node_modules', '@prisma')\n if (!existsSync(prisma)) return []\n\n return [\n {\n find: /^@prisma\\/([^/]+)\\/runtime\\/library$/,\n replacement: join(prisma, '$1', 'runtime', 'library.js'),\n },\n ]\n}\n\n/**\n * The base every Nest module gets. Four decisions, each with its measurement.\n */\nfunction baseConfig(options: SharedTestOptions): ViteUserConfig {\n const { root, workspaceRoot } = options\n\n return {\n /*\n * NO decorator-metadata plugin, and that is a measured removal rather than an omission.\n *\n * Nest reads constructor parameter types from `emitDecoratorMetadata`, so this preset used to\n * run the BUILD role's SWC plugin to emit it, on the premise that the bundler does not. That\n * premise was true of esbuild and is false of Vite 8, which transforms with oxc: oxc lowers the\n * decorators itself and emits the metadata, provided a tsconfig with `experimentalDecorators`\n * applies to the file.\n *\n * Measured five times before removing it. On `apps/nest/microservices/mission`, with and\n * without the plugin, the output is strictly identical for `@Injectable()` constructors\n * (including a service caught in a two-file import CYCLE, all 8 parameters resolved), for route\n * handler parameters and return types, and for DTO properties; the suite then passes in 98s\n * instead of 249s. Confirmed by a direct `Reflect.getMetadata` probe on\n * `apps/nest/backends-for-frontends/admin` and on `libs/nest/starter`, and by two A/B runs\n * through this very preset: `agency-notification` (100 tests) identical with and without, and\n * `grid-leave` (3335 tests) identical without.\n *\n * One caveat that the same measurement produced, and which belongs to the CONSUMER rather than\n * here: a property typed by an interface or by a type-alias imported with `import type` emits\n * `Object`. That is `emitDecoratorMetadata`'s own behaviour, identical with and without the\n * plugin, not a Vite regression.\n *\n * The BUILD role keeps its plugin. Its context differs, it feeds a shipped artefact, and\n * removing it there needs its own proof.\n *\n * ⚠️ ONE construct escapes oxc, and the exception is why this line is a condition rather than\n * an empty array: a decorator on an `abstract` class member. swc emitted it, oxc erases the\n * member and the decorator with it, silently. There is exactly one such member in this repo,\n * so `lowerDecoratorsWithTypeScript` buys the plugin back for that module alone.\n */\n /*\n * `jestExportConditions` is UNCONDITIONAL, and it is the one plugin every migrated module gets.\n *\n * jest resolved with `['node', 'require', 'default']`, read off the installed `@nx/jest/preset`\n * rather than off its documentation. `development` was never in that list, so a package\n * shipping a separate development build loaded its PRODUCTION file. Vite's own conditions carry\n * `development|production`, so `@emotion/cache` loads `emotion-cache.development.cjs.js`, whose\n * extra stylis plugin calls `console.error(':first-child is potentially unsafe...')`, and with\n * `jest-fail-on-console` in the setup that console call IS a failure.\n *\n * Measured on `libs/front/components`: 2 of its 3 remaining failures, both green again with\n * this. `apps/front/front-legacy` has 100 over 26 files from the same cause.\n *\n * Unconditional rather than reserved for a jsdom module, because it is not a statement about\n * the DOM: it is what this migration is for, running the suite the way the runner it was\n * written for ran it. It changes nothing for a module with no dual-published dependency, which\n * is why nest and cloud were measured identical without it.\n */\n plugins: [\n jestExportConditions(),\n /*\n * The alias beside it covers a file naming the workspace's msw server by its package\n * specifier. This covers the one that reaches the same module RELATIVELY, which an alias\n * cannot: `./server` is a spelling thousands of unrelated files use, so the redirect is\n * decided on the resolved path instead. See `msw-server-redirect.ts` for the measurement.\n */\n ...(mswServerFile() === undefined\n ? []\n : [mswServerRedirect(mswServerFile() as string, workspaceRoot)]),\n ...(options.lowerDecoratorsWithTypeScript ? [decoratorMetadata({ root })] : []),\n ],\n\n /*\n * The proviso in the paragraph above, made unconditional.\n *\n * oxc lowers decorators from the tsconfig that applies to the file, so a file belonging to NO\n * tsconfig `include` is lowered as if it used the STANDARD decorators, and comes out of Vite as\n * invalid JavaScript. Not merely without metadata: the file fails to parse, and every file that\n * imports it disappears with it.\n *\n * Measured on `apps/nest/microservices/mission`, where ONE uncovered helper,\n * `src/app/test/mission.test-wrapper.ts`, took down 158 of 416 test files. Reproduced on a\n * three-file case: covered file fine, uncovered file `SyntaxError: Invalid or unexpected\n * token`, and this option alone turns it into the same output the covered file gets, metadata\n * included.\n *\n * Declared here rather than left to each module's tsconfig `include`, because the alternative\n * is asking 107 teams to find which of their files no tsconfig covers, which is the work this\n * role exists to do for them.\n *\n * Two things the same measurement established, both deliberate:\n *\n * - it OVERRIDES the tsconfig, it is not a default the tsconfig refines. A file under a\n * tsconfig saying `emitDecoratorMetadata: false` gets metadata anyway. Acceptable because\n * this is the NEST preset and a Nest module is legacy decorators by definition: no tsconfig\n * under `apps/nest`, `libs/nest`, `apps/cloud` or `libs/cloud` sets either option to false.\n * - it needs no polyfill. Without `reflect-metadata` loaded, nothing throws, the metadata is\n * simply unreadable, exactly as before.\n */\n oxc: { decorator: { legacy: true, emitDecoratorMetadata: true } },\n\n resolve: {\n alias: [\n ...axiosAlias(root, workspaceRoot),\n ...luxonAlias(root, workspaceRoot),\n /*\n * Nest only, and measured: 1186 files under `apps/nest` and `libs/nest` mention `@prisma/`,\n * and ZERO under `apps/front` and `libs/front`. A React module paying for an alias to a\n * client it never generates is noise in a file someone has to read.\n */\n ...(options.flavour === 'nest' ? prismaRuntimeAlias(workspaceRoot) : []),\n ...mswAliases(),\n /*\n * `jest-mock-extended` loads `@jest/globals`, which refuses to run outside jest. 2491\n * files import it, 104 of them under `libs/` as SHARED helpers, so migrating those helpers\n * breaks every module still on jest and leaving them breaks every module moved to Vitest.\n * Old and new therefore do not cohabit on shared helpers, which would have killed the\n * per-module plan.\n *\n * This one line removes the constraint: an unmigrated helper resolves to the Vitest fork\n * inside an adopted module and keeps resolving to the jest one everywhere else.\n * `vitest-mock-extended@5.1.1` is a fork of the same package and exports the same names.\n *\n * Measured on `mission`: failing suites went from 159 to 10.\n */\n {\n find: /^jest-mock-extended$/,\n replacement: mockExtendedAdapterPath() ?? vitestMockExtendedPath(),\n },\n ...tsconfigAliases(workspaceRoot),\n ],\n },\n\n test: {\n globals: true,\n environment: 'node',\n root,\n /*\n * What the repo's jest preset actually matched, copied rather than approximated:\n * `**\\/?(*.)+(spec|test).[jt]s?(x)`.\n *\n * Both NAMES, because jest ran both: `**\\/*.spec.ts` alone read green while missing three\n * `.test.ts` files and 23 tests, with nothing saying so. And all four EXTENSIONS, for the\n * same reason one notch further out. Measured over the repo's 7063 test files:\n *\n * .ts 5844 .tsx 1108 .js 99 .mjs/.cjs 12\n *\n * The `.ts`-only form cost nothing on nest and cloud, which have none of the others, and it\n * cost `libs/front/api` three files and 13 tests on the first front module it met. Half the\n * front's test files are `.tsx`.\n *\n * `.mjs` and `.cjs` are deliberately OUT: jest's `[jt]s?(x)` does not match them either, and\n * Vitest's own default include does. Running a file the reference never ran is as wrong as\n * skipping one it did.\n */\n include: ['**/*.spec.[jt]s?(x)', '**/*.test.[jt]s?(x)'],\n /*\n * Kept, and it is not a performance knob. Nest registers metadata as an import SIDE EFFECT:\n * a decorator writes into a catalog when its module loads. Sharing a module registry across\n * files lets one suite see what another registered, and the failure appears in whichever\n * file happens to run second.\n */\n /*\n * Nest only. A Nest module registers metadata as an import SIDE EFFECT, so sharing a module\n * registry across files lets one suite see what another registered. A React module has no\n * such catalog, and isolation is not free.\n */\n ...(options.flavour === 'nest' ? { isolate: true } : {}),\n /*\n * Concurrency, transposed from what this repo does today rather than chosen.\n *\n * Every jest target inherits `configurations.ci = { ci: true, runInBand: true }` from the\n * `@nx/jest:jest` key in `nx.json`, and CI invokes every test target with\n * `--configuration=ci`. So on CI every suite in this repo runs ONE FILE AT A TIME today. That\n * key belongs to the jest executor and cannot be touched, because the workspace is mixed: it\n * still serves the modules that have not moved.\n *\n * ⚠️ And it is CI-ONLY. A local run omits `--configuration=ci`, so jest runs files in\n * PARALLEL on a developer's machine. A flat `fileParallelism: false` here would make local\n * runs slower than jest, which loses something the module had. Hence the condition rather\n * than the constant: parallel locally, one file at a time on CI, which is jest on both sides.\n *\n * Measured on three files that each hold the clock for 400ms and record their interval:\n *\n * default files overlap, 402ms\n * fileParallelism: false no overlap, 1442ms\n * maxWorkers: 1 no overlap, 1423ms\n *\n * Both candidates give the property that matters. `fileParallelism` is the one that says what\n * the module MEANS (\"do not run my files at the same time\"); `maxWorkers` is a pool size, and\n * it is what a module asking for a CAP gets instead (three BFFs ask for 4).\n *\n * A module that declared its own concurrency overrides this, in both environments, exactly as\n * it does today.\n */\n /*\n * ⚠️ PARALLEL on CI too, since 26/09, and that reverses what the note above decided.\n *\n * The transposition was faithful and expensive. Measured on `libs/front/components`, 1418\n * tests green on every line, on the repo's own 4-core agents:\n *\n * jest, as CI runs it 158.7 s\n * vitest serial, the transposition 644.5 s 4.1x slower\n * vitest, 3 workers 324.7 s 2.05x slower\n *\n * Serialising costs vitest far more than it costs jest, because vitest re-evaluates the\n * module graph per file through Vite's SSR runner: 555 ms of `import` and 478 ms of\n * `environment` per file against 139 ms of actual tests. Parallelism does not remove that\n * cost, it pays it on several cores at once.\n *\n * Every other documented lever was measured and does nothing or breaks the suite:\n * `deps.optimizer.ssr`, `NODE_COMPILE_CACHE` and `fsModuleCache` all cache TRANSFORMATION\n * where the cost is EVALUATION; `pool: threads` is noise; `vmThreads` collapses into\n * `no-isolate` when serial and fails 473 tests; `--no-isolate` fails 351.\n *\n * ⚠️ And NO worker cap, which is deliberate after reading vitest's own sizing:\n *\n * getDefaultThreadsCount: config.watch ? max(numCpus / 2, 1) : max(numCpus - 1, 1)\n *\n * `cores - 1` is 3 on this repo's 4-core agents, which is exactly the pool the 324.7 s above\n * was measured with, and 9 on a 10-core laptop. Pinning a number would slow every developer\n * to serve the agent, and a percentage would only re-describe a default that already adapts.\n * So one variable changes here and the tool keeps sizing its own pool.\n *\n * Memory was the reason to hesitate and it was measured rather than assumed: a full jsdom\n * run holds 1.1 GB at three workers and 1.9 GB at six, on agents with 32 GB. The OOM this\n * repo suffered was three nx tasks near 7.45 GB each, which is not what a test worker costs.\n *\n * A module that declares its own concurrency still overrides this, as before.\n */\n fileParallelism: true,\n\n /*\n * ⚠️ Sentinel's own setup files go through VITE, not round it, and that is load-bearing.\n *\n * Vitest externalizes anything under `node_modules`, so an installed `@hublo/sentinel` is\n * loaded by Node while the module's own code is served by Vite. The two then hold DIFFERENT\n * instances of the same package, and a setup whose whole job is to mutate per-copy state\n * mutates a copy nobody reads.\n *\n * Measured on `apps/nest/microservices/agency`, with the published 1.4.0-alpha.4:\n *\n * zone of the copy sentinel pinned UTC\n * zone of the copy the test imports Europe/Paris\n * same object false\n *\n * Six tests that passed under jest failed, all comparing `DateTime.now().toISO()` against a\n * date the provider had serialised: identical instants, one rendered `+02:00` and the other\n * `Z`. The root jest setup pinned `Settings.defaultZone = 'utc'` and 97 modules named it, so\n * this is not one module's edge case.\n *\n * ⚠️ It hid for a whole campaign because the harness SYMLINKS sentinel to its source tree.\n * The resolved path then holds no `node_modules`, Vitest inlines it, and the pin lands on the\n * right copy: 96 modules reported IDENTIQUE while the real install was red. An instrument\n * that installs the package differently from a team does not measure what a team will see.\n *\n * The narrow form `/@hublo\\/sentinel\\/dist\\/roles\\//` was tried and does NOT work: the pin\n * itself lives in a shared chunk at the dist root, which stays external. It has to be the\n * whole package.\n */\n server: { deps: { inline: [/@hublo\\/sentinel/] } },\n },\n }\n}\n\n/**\n * Vite accepts `alias` as an ARRAY or as an object map, and the two do not combine.\n *\n * The array form is the one that matters here: only it takes a RegExp `find`, which both the axios\n * and the `jest-mock-extended` aliases need. A module overriding with the object form gets its\n * entries converted rather than dropped, because silently losing an override is worse than a shape\n * the caller did not expect.\n */\nfunction asAliasArray(alias: AliasOptions | undefined): ViteAlias[] {\n if (alias === undefined) return []\n if (Array.isArray(alias)) return alias as ViteAlias[]\n return Object.entries(alias as Record<string, string>).map(([find, replacement]) => ({\n find,\n replacement,\n }))\n}\n\n/** A `test.server` block, named off Vitest's own type so the merge cannot drift from it. */\ntype TestServer = NonNullable<ViteUserConfig['test']>['server']\n\n/** The config, with the module's own overrides merged over it. */\nexport function sharedTestConfig(\n options: SharedTestOptions & { flavour: TestFlavour },\n): ViteUserConfig {\n const base = baseConfig(options)\n if (options.overrides === undefined) return base\n // Shallow by section rather than a deep merge helper: the three sections a module overrides in\n // practice are `test`, `resolve` and `plugins`, and a deep merge would silently concatenate\n // arrays a module meant to replace.\n return {\n ...base,\n ...options.overrides,\n plugins: [...(base.plugins ?? []), ...(options.overrides.plugins ?? [])],\n resolve: {\n ...base.resolve,\n ...options.overrides.resolve,\n /*\n * The module's own aliases come after sentinel's, and in a Vite alias ARRAY the FIRST match\n * wins. So sentinel's win, which is the opposite of what this comment used to claim.\n *\n * ⚠️ The order is corrected here rather than in the code, because changing it would flip a\n * behaviour that nothing exercises. Every alias sentinel adds is ANCHORED to one exact\n * specifier: `^axios$`, `^luxon$`, `^msw$`, `^jest-mock-extended$` and\n * `^@prisma/<x>/runtime/library$`. A module's own entry collides only by naming the identical\n * string, and measured across every `jest.config*` in the repo, none does: the closest are\n * `@front/type/axios` and `@front/api/msw-handlers` in `front-legacy`, different specifiers\n * in the one module this role refuses anyway.\n *\n * So today the order decides nothing, and all 97 migrations were measured with it this way\n * round. Reversing it on a hypothesis would be a silent behaviour change bought with nothing.\n * What was actually wrong was a comment promising a module it could win, which a reader would\n * have relied on.\n */\n alias: [\n ...asAliasArray(base.resolve?.alias),\n ...asAliasArray(options.overrides.resolve?.alias),\n ],\n },\n test: {\n ...base.test,\n ...options.overrides.test,\n /*\n * ⚠️ `server.deps.inline` is the one key inside `test` that must not be REPLACED.\n *\n * The shallow spread above is right for everything else: a module overriding `environment`\n * or `testTimeout` means to replace it. But the base uses this list to put sentinel's own\n * setup inside Vite's graph, and a module declares its own entries there for its workspace\n * packages. Dropping either half breaks something silently, and the halves have nothing to\n * do with each other, so they are concatenated.\n *\n * Measured on `libs/front/api`, which declares `['@front/runtime-config', '@segment/snippet']`\n * and would otherwise have discarded the base's entry and lost the timezone pin with it.\n */\n server: mergeServerDeps(base.test?.server, options.overrides.test?.server),\n },\n }\n}\n\n/** Two `server` blocks, with their `deps.inline` lists joined rather than one winning. */\nfunction mergeServerDeps(base: TestServer, override: TestServer): TestServer {\n if (base === undefined) return override\n if (override === undefined) return base\n\n const inline = [\n ...(asInlineArray(base.deps?.inline) ?? []),\n ...(asInlineArray(override.deps?.inline) ?? []),\n ]\n return {\n ...base,\n ...override,\n deps: { ...base.deps, ...override.deps, ...(inline.length > 0 ? { inline } : {}) },\n }\n}\n\n/** `inline` is a list or the literal `true`, and `true` cannot be concatenated with anything. */\nfunction asInlineArray(inline: unknown): (string | RegExp)[] | undefined {\n return Array.isArray(inline) ? (inline as (string | RegExp)[]) : undefined\n}\n","/**\n * Send every importer of the workspace's msw server to the one that actually listens.\n *\n * ## The gap an alias cannot close\n *\n * The generated config already aliases `@hublo/test/msw/server`, which covers a file naming the\n * server by its package specifier. It cannot cover a file that reaches the same module by a\n * RELATIVE path, because an alias matches the spelling of the import and `./server` is a spelling\n * thousands of unrelated files use.\n *\n * That gap is not hypothetical. `libs/nest/tests/src/msw/create-handlers.ts` exports\n * `createMswHandler`, whose body calls `server.use(...)` on `./server`. So a migrated module gets\n * its handlers registered on the WORKSPACE's server while sentinel's is the one listening, and the\n * request arrives unmatched.\n *\n * Measured on `apps/nest/microservices/institution`, its only consumer:\n *\n * jest 2958 passed\n * vitest, without this 2957 passed, 1 failed\n * the failure Worker Webservice transformTemporaryPersonToWorker\n * onUnhandledRequest, raised by sentinel's copy\n *\n * ## Why here rather than in that file\n *\n * One line in `create-handlers.ts` would also fix it. Héla, 2026-09-24: the fix belongs in what we\n * OWN. A config sentinel generates reaches every module that adopts, today and later, and asks\n * nothing of a team whose shared lib happens to sit in the path. Editing their file fixes one\n * import and leaves the next one to be discovered by whoever writes it.\n *\n * ## Why the RESOLVED path, which is narrower than it looks\n *\n * This matches what the import turned out to BE, not how it was written, so `./server`,\n * `../msw/server` and `@hublo/test/msw/server` are one case rather than three. The pattern is the\n * same convention the codemod already uses to repoint a server import: a file whose path ends in\n * `msw/server`. It is checked against the resolved id only, so a module's own `./server` that has\n * nothing to do with msw is never touched.\n *\n * ## What it does NOT do\n *\n * Redirect anything outside the workspace. A `msw/server` inside `node_modules` is a package's own\n * business, and sentinel's own server file is skipped so the redirect cannot point at itself.\n */\nimport type { Plugin } from 'vite'\n\n/** The convention: a module whose path ends in `msw/server`, whatever extension it carries. */\nconst WORKSPACE_MSW_SERVER = /(^|\\/)msw\\/server\\.[cm]?[jt]sx?$/\n\n/**\n * @param serverFile sentinel's own server, the one its setup starts listening on.\n * @param workspaceRoot the boundary: nothing outside it is redirected.\n */\nexport function mswServerRedirect(serverFile: string, workspaceRoot: string): Plugin {\n return {\n name: 'sentinel:msw-server-redirect',\n // Before the resolver settles on the workspace file, since the point is to replace it.\n enforce: 'pre',\n async resolveId(source, importer, options) {\n // Our own file, and anything already pointing at it: nothing to do, and no self-recursion.\n if (source === serverFile) return undefined\n\n const resolved = await this.resolve(source, importer, { ...options, skipSelf: true })\n if (resolved === null || resolved.external) return undefined\n\n const id = resolved.id\n if (!id.startsWith(workspaceRoot)) return undefined\n if (id.includes('/node_modules/')) return undefined\n if (!WORKSPACE_MSW_SERVER.test(id)) return undefined\n\n return serverFile\n },\n }\n}\n","/**\n * Resolve packages the way jest did, for a suite that was written against jest's resolution.\n *\n * ## What jest resolved, measured rather than remembered\n *\n * Every suite in this repo ran through `@nx/jest/preset`, whose\n * `testEnvironmentOptions.customExportConditions` is exactly `['node', 'require', 'default']`\n * (read off the installed preset, not off its documentation). `development` was never in that\n * list, so a package shipping a separate development build behind that condition loaded its\n * PRODUCTION file under jest.\n *\n * Vitest resolves with Vite's conditions instead, and its default carries the\n * `development|production` token. Measured on a bare config, the resolved list is\n * `['node', 'development|production']`. `@emotion/cache` then loads\n * `emotion-cache.development.cjs.js`, whose extra stylis plugin calls\n * `console.error(':first-child is potentially unsafe...')`.\n *\n * With `jest-fail-on-console` in the setup, that console call IS a failure: **100 tests over 26\n * files on `apps/front/front-legacy`**, every one of them green under jest. `libs/front/components`\n * hits the same root cause on one test.\n *\n * ## Why a plugin and not a config line\n *\n * Declaring `ssr.resolve.conditions: ['node']` does nothing: Vite merges config arrays by\n * CONCATENATION, so Vitest's default is appended straight back. The token has to come off the\n * RESOLVED config, which is what `configResolved` is for.\n *\n * This is the same shape as the other thing that cannot be declared: a module cannot reproduce\n * `customExportConditions` either, because the list it asks for is PREFIXED to Vite's defaults\n * rather than substituted for them.\n *\n * ## The three lists are one array\n *\n * Measured on Vite 8: `config.resolve.conditions`, `config.ssr.resolve.conditions` and\n * `config.environments.ssr.resolve.conditions` are the SAME array object, so stripping one strips\n * all three. All three are stripped anyway. Relying on an aliasing that nothing promises is how a\n * silent regression arrives on a Vite upgrade, and the cost of being explicit is two lines.\n *\n * ## ⚠️ What this deliberately hides, said out loud\n *\n * emotion's warning is REAL: `libs/front/components` is consumed by a Next app, and a\n * `:first-child` selector is genuinely unsafe when the markup is rendered server-side. Restoring\n * jest's resolution puts that warning back out of sight.\n *\n * It is hidden here on purpose all the same, because a migration that also turns 100 green tests\n * red cannot be told apart from a migration that broke something. The baseline gate compares test\n * names, and it has no way to know which reds are progress. The selector belongs to the module's\n * owners as its own piece of work, with its own ticket, not as a side effect of changing runner.\n */\nimport type { Plugin } from 'vite'\n\n/**\n * The tokens jest never had.\n *\n * Both spellings, because the resolved config carries `development|production` (Vite's own\n * placeholder, replaced per environment) while a config written by hand may carry `development`.\n */\nconst ABSENT_UNDER_JEST = new Set(['development', 'development|production'])\n\n/** Remove them in place, since the resolved config is what the resolver will read. */\nfunction stripInPlace(conditions: string[] | undefined): void {\n if (conditions === undefined) return\n\n for (let index = conditions.length - 1; index >= 0; index -= 1) {\n const condition = conditions[index]\n if (condition !== undefined && ABSENT_UNDER_JEST.has(condition)) conditions.splice(index, 1)\n }\n}\n\n/**\n * Add to `plugins` in a module whose suite was written against jest's resolution.\n *\n * Not applied by the preset for everyone: it is a MIGRATION aid, and a module that was always on\n * Vitest never had jest's resolution to go back to. A module adopts it the day it migrates, and\n * can drop it the day its suite no longer depends on the production build.\n */\nexport function jestExportConditions(): Plugin {\n return {\n name: 'sentinel:jest-export-conditions',\n configResolved(config) {\n const environments = config as unknown as {\n environments?: { ssr?: { resolve?: { conditions?: string[] } } }\n }\n\n stripInPlace(config.resolve?.conditions as string[] | undefined)\n stripInPlace(config.ssr?.resolve?.conditions as string[] | undefined)\n stripInPlace(environments.environments?.ssr?.resolve?.conditions)\n },\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/roles/test/setup/msw.ts"],"sourcesContent":["/**\n * The HTTP interception a test suite runs against, owned here rather than by each module.\n *\n * ## Why sentinel ships msw at all\n *\n * The goal is that a future migration is ONE version bump of sentinel. Today's migration is\n * painful for the opposite reason: the runner and everything around it are declared in 133 places,\n * so moving them means moving 133 modules. A tool that sits between the runner and the tests, as\n * msw does, has to travel with the runner or it becomes the next thing nobody can move.\n *\n * It also puts the compatibility question where someone can answer it. When Vitest moves, msw\n * either still works with it or does not, and that is a question for the tooling team with one\n * test suite, not for thirty teams discovering it separately.\n *\n * ## Why version 1.3.3, which is not the newest\n *\n * Measured across the repo: 523 test files use the msw 1 API (`rest.get`), and NOT ONE uses the\n * msw 2 API (`http.get`). Shipping msw 2 would break all 523 on the day they migrate, turning one\n * migration into two. Shipping 1.3.3 keeps them working, and the eventual 1 to 2 move becomes\n * OURS: studied once, then automated or documented, which is the whole point of owning it.\n *\n * The one module on msw 2 is `apps/front/hr-management`, which is Svelte, and the test role defers\n * Svelte for its own reasons.\n */\nimport { setupServer } from 'msw/node'\n\n/**\n * No default handlers, deliberately.\n *\n * The repo's own server had `defaultHandlers = []` too, so nothing is lost. A handler here would\n * be a Hublo endpoint inside a package meant to be usable by anyone, which is the line between\n * what sentinel owns (the config) and what an app owns (what it mocks).\n *\n * A suite adds its own with `server.use(...)`, which is how all 600 files that touch this already\n * work.\n */\nexport const server = setupServer()\n\n/** The options the repo's own setup passed, kept in one place since two calls now use them. */\nconst LISTEN_OPTIONS = { onUnhandledRequest: 'error' } as const\n\n/**\n * The lifecycle, as the root jest setup had it, plus ONE deviation the runners forced.\n *\n * `onUnhandledRequest: 'error'` is carried over rather than softened: a test that reaches a real\n * network is a test whose result depends on something outside the repo, and it should say so\n * loudly. The repo already ran this way, so nothing changes for a migrating suite.\n *\n * Registered through the globals Vitest exposes, because the preset sets `globals: true` and a\n * setup file that imported them would need them resolvable from wherever it is loaded.\n *\n * ## Why `listen()` also runs here, at module scope\n *\n * msw intercepts by REPLACING `request` and `get` on the `http` and `https` modules. Under jest,\n * `import * as https from 'https'` compiled to `require('https')`, so the module object was read\n * at call time and a patch applied later was seen. Under Vitest the same line is a real ESM\n * namespace import of a CJS builtin, and Node builds that namespace ONCE, from the exports as they\n * stand at first import. A patch applied afterwards never reaches it.\n *\n * `beforeAll` runs after the test file and everything it imports have been evaluated, so the\n * namespace is already frozen by then: the provider keeps calling the ORIGINAL `https.request` and\n * the request leaves the machine. Measured on `libs/cloud/hermes`: `send-push` reached the REAL\n * OneSignal API, which answered `400 Failed to parse app_id from request`, a message that exists\n * nowhere in this repo. msw never saw the call, so `onUnhandledRequest: 'error'` never fired\n * either, and the only visible symptom was `expected \"vi.fn()\" to be called 50 times, but got 0`.\n *\n * A setup file is evaluated BEFORE the test module graph, so patching here happens before the\n * namespace is built. Same file, same suite, 0 of 7 to 7 of 7.\n *\n * The `beforeAll` call is kept as well: it is what restores interception for a suite that closed\n * the server itself, and msw's own `listen()` is safe to call twice.\n */\nexport function installMswLifecycle(hooks: {\n beforeAll: (fn: () => void) => void\n afterEach: (fn: () => void) => void\n afterAll: (fn: () => void) => void\n}): void {\n server.listen(LISTEN_OPTIONS)\n hooks.beforeAll(() => server.listen(LISTEN_OPTIONS))\n hooks.afterAll(() => server.close())\n\n /*\n * `server.resetHandlers()` is NOT called here, and that is a transcription and not a judgement.\n *\n * The root jest setup has the line commented out. Enabling it would be an improvement in the\n * abstract — a handler one test adds leaks into the next, and the failure then appears in\n * whichever test happens to run after, which is the hardest kind to trace. But some suite may\n * depend on that persistence today, and a migration is the wrong moment to find out.\n *\n * So the behaviour is carried across exactly, and the question is reported rather than settled\n * here: it belongs to whoever owns those suites, as its own reviewed change.\n */\n}\n"],"mappings":";AAwBA,SAAS,mBAAmB;AAYrB,IAAM,SAAS,YAAY;AAGlC,IAAM,iBAAiB,EAAE,oBAAoB,QAAQ;AAiC9C,SAAS,oBAAoB,OAI3B;AACP,SAAO,OAAO,cAAc;AAC5B,QAAM,UAAU,MAAM,OAAO,OAAO,cAAc,CAAC;AACnD,QAAM,SAAS,MAAM,OAAO,MAAM,CAAC;AAarC;","names":[]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/roles/test/setup/workspace.ts"],"sourcesContent":["/**\n * The rest of what a migrated suite needs before its tests, loaded DEFENSIVELY.\n *\n * ## Why this is in sentinel and not in the repo\n *\n * Because it is config, and an app must depend on its own config and nothing outside it. The first\n * version of the generated config referenced `'../../../../vitest.setup.mts'`, a relative path\n * climbing four levels to the workspace root — which is exactly the external link this whole\n * migration exists to remove. A module that reaches outside itself cannot move on its own.\n *\n * ## Why loading is conditional, and why that is the point rather than a precaution\n *\n * Unlike the msw lifecycle beside it, nothing here reacts to the RUNNER. `dotenv-flow` and luxon's\n * zone would behave identically under jest, Vitest or `node:test`; what they react to is the REPO,\n * which chose those two packages. So the conditional load is the statement of ownership: sentinel\n * offers the behaviour, and the repo that installed the package decides whether it happens.\n *\n * That is also what keeps sentinel installable elsewhere. Neither package can be a hard dependency,\n * so each is loaded only if present and skipped in silence if not. Elsewhere this file does nothing;\n * here both are present and the behaviour is the one the jest setup had.\n *\n * Silence is the right answer for an absent package and the wrong one for a broken package, so a\n * load that fails for any other reason is re-thrown rather than swallowed.\n */\nimport { createRequire } from 'node:module'\nimport { join } from 'node:path'\n\nimport { findWorkspaceRoot } from '../../../core/workspace-prep.js'\n\n/** Was this module absent, as opposed to present and broken? */\nfunction isMissingModule(error: unknown): boolean {\n return (error as { code?: string } | null)?.code === 'ERR_MODULE_NOT_FOUND'\n}\n\n/**\n * Optional by construction: `import()` inside a try, so a missing package is a no-op rather than a\n * failure at load time. A static import would make every one of them a hard dependency.\n */\nasync function optional(specifier: string, use: (module: unknown) => void): Promise<void> {\n try {\n use(await import(/* @vite-ignore */ specifier))\n } catch (error) {\n if (!isMissingModule(error)) throw error\n }\n}\n\n/**\n * The MODULE's copy of a package, not sentinel's.\n *\n * Under pnpm, an import written here resolves from sentinel's own directory, so it sees only\n * sentinel's dependencies. That is right for anything whose effect is GLOBAL, and wrong for\n * anything whose effect is per-copy: pinning `Settings.defaultZone` on sentinel's luxon leaves\n * the module's luxon in the machine's local zone, and the tests drift by an hour twice a year\n * with nothing saying why.\n *\n * Measured 2026-09-21: neither `luxon` nor `dotenv-flow` resolves from sentinel, while both\n * resolve from the module under test. The silent `optional` above therefore skipped BOTH, so a\n * migrated suite lost the `.env` cascade and the UTC pin at once, without a word.\n */\nfunction fromModule(specifier: string): unknown | undefined {\n try {\n const require = createRequire(join(process.cwd(), 'noop.cjs'))\n return require(specifier)\n } catch {\n return undefined\n }\n}\n\n/**\n * `dotenv-flow`, which the root jest setup loaded first.\n *\n * Vite's own `.env` handling does NOT replace it, which was checked before writing this. Measured on\n * Vitest 4 against a fixture holding `.env` and `.env.test`: the cascade is read and `MODE` is\n * `test`, but only `VITE_`-prefixed values are exposed, and only on `import.meta.env`. `process.env`\n * comes back untouched, and the code under test reads unprefixed `process.env`.\n *\n * `purge_dotenv` is carried across: it clears variables a previous load left behind, and without it\n * a value from one run leaks into the next. Not a detail in a suite that asserts on URLs.\n */\nasync function loadEnvironment(): Promise<void> {\n // A plain dependency of sentinel now: it mutates `process.env`, which is global, so WHICH copy\n // runs does not matter. It is the other half of replacing the root setup, and the root is going\n // away (Héla, 2026-09-21: \"mon but est de supprimer la config root\").\n await optional('dotenv-flow', (module) => {\n const flow = module as {\n config?: (options: Record<string, unknown>) => void\n default?: unknown\n }\n const config =\n flow.config ?? (flow.default as { config?: (o: Record<string, unknown>) => void })?.config\n /*\n * From the WORKSPACE ROOT, not from the module.\n *\n * dotenv-flow reads its cascade from the current directory, and the two runners do not share\n * one: jest ran from the repo root, where `.env`, `.env.test` and `.env.token` live, while\n * `sentinel --run --test` runs from the module, where there is nothing to read. Measured on\n * `libs/cloud/events-notifications`: from the module, a service call waits for an endpoint its\n * env never named and the test dies on a 5 s timeout; from the root, it answers in 200 ms.\n *\n * The root is found rather than configured: the setup cannot be handed a value, since a module\n * that overrides `test.env` would replace whatever the preset injected there.\n */\n const workspaceRoot = findWorkspaceRoot(process.cwd())\n config?.({\n silent: true,\n purge_dotenv: true,\n ...(workspaceRoot === undefined ? {} : { path: workspaceRoot }),\n })\n })\n}\n\n/**\n * Luxon's default zone, carried as luxon's own setting and NOT translated to `process.env.TZ`.\n *\n * They are different instructions: one configures luxon, the other the whole process, `Date` and\n * `Intl` included. Swapping them would change what a suite does while claiming to migrate it, and\n * timezone is not a detail here — a machine's zone accounted for a large part of 345 local failures\n * on `host-admin` that did not exist in CI.\n */\nasync function pinLuxonZone(): Promise<void> {\n // The module's copy, deliberately: `Settings` is per-copy state, so sentinel's own luxon is the\n // one package this setup must NOT configure.\n /*\n * Through the RUNNER's resolver first, which is what the tests use. `fromModule` goes through\n * `require`, and a package shipping both builds hands it the CJS one while Vite hands the tests\n * the ESM one. Two copies, two `Settings`, and the pin lands on the one nobody reads: measured on\n * `libs/cloud/events-notifications`, whose suite ran in `Europe/Paris` and compared timestamps\n * two hours apart. The preset aliases luxon to one path so both routes agree.\n */\n let pinned = false\n await optional('luxon', (module) => {\n const settings = (module as { Settings?: { defaultZone: string } }).Settings\n if (settings === undefined) return\n settings.defaultZone = 'utc'\n pinned = true\n })\n if (pinned) return\n\n const own = fromModule('luxon') as { Settings?: { defaultZone: string } } | undefined\n if (own?.Settings !== undefined) {\n own.Settings.defaultZone = 'utc'\n return\n }\n\n // Said out loud rather than skipped: a suite whose dates silently run in the machine's zone is\n // the kind of failure that gets blamed on the migration months later.\n process.stderr.write(\n 'sentinel (test): luxon was not found in this module, so the UTC default zone is NOT pinned. ' +\n 'Dates will follow the machine zone, as they did not under jest.\\n',\n )\n}\n\n/**\n * Everything the workspace needs beyond the msw lifecycle.\n *\n * NOT included: the `jest.mock('dynamoose')` and `jest.mock('@opentelemetry/exporter-metrics-otlp-grpc')`\n * calls the jest setup made. A module mock is a per-suite decision that `vi.mock` must make in the\n * file that needs it, and hoisting it into a shared setup is what makes a test pass for a reason\n * nobody can see. The migration reports them instead, so the module that relies on one declares it.\n */\nexport async function installWorkspaceSetup(): Promise<void> {\n await loadEnvironment()\n}\n\n/**\n * Luxon's default zone, split out because it is not in the same class as the rest.\n *\n * ⚠️ These two were one function until 23/09 and moving them together broke three modules.\n *\n * The `.env` cascade is needed by any suite whose code reads `process.env`, whatever setup its\n * jest config named: under jest those modules got it from a `globalSetup` running in the main\n * process, and Vitest's workers do not inherit it the same way. Removing it cost\n * `libs/front/logic` 23 tests and `libs/front/api` 44, all with `missing env var\n * NEXT_PUBLIC_MONOREPO_BASE_URL`.\n *\n * The UTC pin is a decision ONE file at the workspace root made, and only the modules naming that\n * file ever had it. So it is conditional and the cascade is not.\n */\nexport async function pinWorkspaceTimezone(): Promise<void> {\n await pinLuxonZone()\n}\n"],"mappings":";;;;;AAwBA,SAAS,qBAAqB;AAC9B,SAAS,YAAY;AAKrB,SAAS,gBAAgB,OAAyB;AAChD,SAAQ,OAAoC,SAAS;AACvD;AAMA,eAAe,SAAS,WAAmB,KAA+C;AACxF,MAAI;AACF,QAAI,MAAM;AAAA;AAAA,MAA0B;AAAA,KAAU;AAAA,EAChD,SAAS,OAAO;AACd,QAAI,CAAC,gBAAgB,KAAK,EAAG,OAAM;AAAA,EACrC;AACF;AAeA,SAAS,WAAW,WAAwC;AAC1D,MAAI;AACF,UAAMA,WAAU,cAAc,KAAK,QAAQ,IAAI,GAAG,UAAU,CAAC;AAC7D,WAAOA,SAAQ,SAAS;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAaA,eAAe,kBAAiC;AAI9C,QAAM,SAAS,eAAe,CAAC,WAAW;AACxC,UAAM,OAAO;AAIb,UAAM,SACJ,KAAK,UAAW,KAAK,SAA+D;AAatF,UAAM,gBAAgB,kBAAkB,QAAQ,IAAI,CAAC;AACrD,aAAS;AAAA,MACP,QAAQ;AAAA,MACR,cAAc;AAAA,MACd,GAAI,kBAAkB,SAAY,CAAC,IAAI,EAAE,MAAM,cAAc;AAAA,IAC/D,CAAC;AAAA,EACH,CAAC;AACH;AAUA,eAAe,eAA8B;AAU3C,MAAI,SAAS;AACb,QAAM,SAAS,SAAS,CAAC,WAAW;AAClC,UAAM,WAAY,OAAkD;AACpE,QAAI,aAAa,OAAW;AAC5B,aAAS,cAAc;AACvB,aAAS;AAAA,EACX,CAAC;AACD,MAAI,OAAQ;AAEZ,QAAM,MAAM,WAAW,OAAO;AAC9B,MAAI,KAAK,aAAa,QAAW;AAC/B,QAAI,SAAS,cAAc;AAC3B;AAAA,EACF;AAIA,UAAQ,OAAO;AAAA,IACb;AAAA,EAEF;AACF;AAUA,eAAsB,wBAAuC;AAC3D,QAAM,gBAAgB;AACxB;AAgBA,eAAsB,uBAAsC;AAC1D,QAAM,aAAa;AACrB;","names":["require"]}
|