@hublo/sentinel 1.4.0-alpha.4 → 1.4.0-alpha.41
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/README.md +1 -0
- package/dist/bin/sentinel.d.ts +1 -0
- package/dist/bin/sentinel.js +45 -102
- package/dist/{chunk-ASBZQAXV.js → chunk-2UU57Y2T.js} +221 -4339
- package/dist/chunk-2XLX6PFR.js.map +1 -0
- package/dist/{chunk-CPCUPK4J.js → chunk-DWVRVPMN.js} +5 -12
- 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-I3FX32E7.js +5644 -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-WLFE5RUU.js → chunk-NW6UHNYX.js} +6 -6
- package/dist/chunk-NW6UHNYX.js.map +1 -0
- package/dist/chunk-O7REVMOC.js +38 -0
- package/dist/chunk-O7REVMOC.js.map +1 -0
- package/dist/chunk-QXFCZON7.js +16 -0
- package/dist/chunk-QXFCZON7.js.map +1 -0
- package/dist/{chunk-MT7VQXRA.js → chunk-SOCLCHJK.js} +228 -13
- package/dist/chunk-SOCLCHJK.js.map +1 -0
- package/dist/{chunk-3TDUIKVQ.js → chunk-SWWQ7X7B.js} +11 -8
- package/dist/chunk-SWWQ7X7B.js.map +1 -0
- package/dist/index.d.ts +28 -5
- package/dist/index.js +7 -4
- package/dist/roles/build/nest/toolchain.js +11 -33
- package/dist/roles/build/nest/toolchain.js.map +1 -1
- package/dist/roles/test/nest/toolchain.js +3 -2
- package/dist/roles/test/nest/toolchain.js.map +1 -1
- package/dist/roles/test/react/toolchain.js +3 -2
- package/dist/roles/test/react/toolchain.js.map +1 -1
- package/dist/roles/test/setup/a11y.d.ts +45 -0
- package/dist/roles/test/setup/a11y.js +72 -0
- package/dist/roles/test/setup/a11y.js.map +1 -0
- 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 +19 -2
- 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/w3c.d.ts +75 -0
- package/dist/roles/test/setup/w3c.js +49 -0
- package/dist/roles/test/setup/w3c.js.map +1 -0
- package/dist/roles/test/setup/workspace-entry.js +3 -2
- 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 +3 -2
- package/dist/roles/test/tools/msw.d.ts +1 -0
- package/dist/roles/test/tools/msw.js +3 -0
- package/dist/roles/test/tools/msw.js.map +1 -0
- package/dist/validate-ANM7RQ3C.js +169 -0
- package/docs/test-adoption.md +282 -5
- package/docs/using-sentinel.md +17 -1
- package/docs/validating-a-change.md +35 -2
- package/package.json +29 -4
- package/types/jest-global.d.ts +85 -0
- package/types/mock-extended.d.ts +52 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/roles/test/setup/mock-extended.ts"],"sourcesContent":["/**\n * `jest-mock-extended`'s deep mocks, made answerable to the question Vitest's `spyOn` asks.\n *\n * A deep mock is a Proxy that invents a property the first time something READS it. Nothing exists\n * until then, so the object reports itself empty:\n *\n * const provider = mockDeep<InstitutionProvider>()\n * 'getAdminFirstAndLastNames' in provider // false\n * Object.getOwnPropertyDescriptor(provider, 'getAdmin...') // undefined\n * typeof provider.getAdminFirstAndLastNames // 'function', and now it exists\n *\n * jest's `spyOn` reads the property, so the Proxy created it and the spy worked. Vitest's asks the\n * object whether it HAS the property first, gets no for both questions, and throws\n * `The property \"getAdminFirstAndLastNames\" is not defined on the object.` The file then reports no\n * test at all, so its names simply go missing rather than failing.\n *\n * Measured on `apps/cloud/shift`: one such spy took 37 of its 108 tests away. The repo has 128 of\n * them across 29 files, mostly in `apps/nest/microservices` and\n * `apps/nest/backends-for-frontends`.\n *\n * ## Why it is fixed here and not in the 128 test files\n *\n * Because it is a difference between two runners, not something 29 suites each got wrong. A\n * codemod rewriting every site would put a migration artefact in front of every reader of those\n * files forever, and teams would carry it. One adapter keeps the test files exactly as they are.\n *\n * The two traps answer by doing what jest's `spyOn` did: read the property once, then answer. The\n * read is the Proxy's own documented way of materialising it, so nothing here reimplements the\n * mock, it only asks the question in the form the underlying object understands.\n *\n * `then` is never materialised. A deep mock that suddenly HAS a `then` is a thenable, and awaiting\n * it, or returning it from an async function, would hang on a promise nothing resolves.\n */\nimport * as mockExtended from 'vitest-mock-extended'\n\n/**\n * Keys that must stay absent however they are asked for.\n *\n * Not an arbitrary list: every one of them is a PROTOCOL PROBE, a property a framework reads to\n * decide what KIND of value it is holding. A deep mock invents whatever it is asked for, so it\n * answers yes to every probe, and each yes is a lie about what it is.\n *\n * then a thenable. Awaiting the mock, or returning it from an async function,\n * hangs on a promise nothing resolves.\n * asymmetricMatch an asymmetric matcher. Measured: `expect(p).rejects.toThrow(deepMock)`\n * answers `expected error to match asymmetric matcher` instead of comparing\n * against the class, so the assertion stops testing what it says.\n * $$typeof a React element. Serializers branch on it and read fields that are not\n * there.\n * nodeType a DOM node, which testing-library and serializers both branch on.\n *\n * Symbols never reach here: the materialiser returns early for a non-string key, so `Symbol.\n * iterator` and friends are already safe.\n */\nconst NEVER_MATERIALISED = new Set(['then', 'asymmetricMatch', '$$typeof', 'nodeType'])\n\nfunction materialise(target: object, key: PropertyKey): void {\n if (typeof key !== 'string' || NEVER_MATERIALISED.has(key)) return\n // The read IS the creation: that is how the deep mock's own Proxy works.\n void (target as Record<string, unknown>)[key]\n}\n\n/**\n * Wrap a deep mock so that `in` and `getOwnPropertyDescriptor` see what a read would create.\n *\n * Only the object handed back is wrapped, not what it returns: a nested spy would need its own\n * wrapper, and the corpus has none. Wrapping every read would also mean a new wrapper per access\n * unless they were cached, and an identity that changes between two reads is a worse bug than the\n * one being fixed.\n */\n/**\n * One proxy per underlying mock, so identity is stable.\n *\n * A suite holds on to `db.timeSlot` and compares it later, or passes it to `toHaveBeenCalledWith`.\n * Handing back a fresh proxy on every read would make the same mock unequal to itself.\n */\nconst wrappers = new WeakMap<object, object>()\n\nfunction answeringProxy<T extends object>(mock: T): T {\n return new Proxy(mock, {\n /*\n * A NESTED mock has to answer the same questions as the top-level one.\n *\n * The adapter used to wrap only what `mockDeep()` returned, and the level below it was a raw\n * mock again. Measured: `'deleteMany' in db.timeSlot` answered false and its descriptor was\n * absent, so `vi.spyOn(db.timeSlot, 'deleteMany')` failed with\n * `The property \"deleteMany\" is not defined on the function` — the exact error\n * `apps/nest/microservices/hublo-pool` reports, and the exact failure this adapter was written\n * to remove, one level down.\n *\n * Read from the TARGET, never with this proxy as the receiver: forwarding the receiver changes\n * what the deep mock hands back.\n */\n get(target, key) {\n if (typeof key === 'string' && NEVER_MATERIALISED.has(key)) return undefined\n\n const value = Reflect.get(target, key) as unknown\n if (value === null) return value\n if (typeof value !== 'object' && typeof value !== 'function') return value\n\n /*\n * A Proxy invariant, not a preference: for a property that is non-configurable AND\n * non-writable, `get` MUST hand back the target's own value. Wrapping one throws\n * `'get' on proxy: property 'mock' is a read-only and non-configurable data property on the\n * proxy target but the proxy did not return its actual value`, which is what a mock's own\n * `mock` record is.\n */\n const descriptor = Reflect.getOwnPropertyDescriptor(target, key)\n if (\n descriptor !== undefined &&\n descriptor.configurable === false &&\n descriptor.writable === false\n ) {\n return value\n }\n\n const existing = wrappers.get(value as object)\n if (existing !== undefined) return existing\n\n const wrapper = answeringProxy(value as object)\n wrappers.set(value as object, wrapper)\n return wrapper\n },\n has(target, key) {\n if (Reflect.has(target, key)) return true\n materialise(target, key)\n return Reflect.has(target, key)\n },\n getOwnPropertyDescriptor(target, key) {\n const existing = Reflect.getOwnPropertyDescriptor(target, key)\n if (existing !== undefined) return existing\n materialise(target, key)\n return Reflect.getOwnPropertyDescriptor(target, key)\n },\n })\n}\n\n/*\n * Everything the fork exports, so a module importing `jest-mock-extended` still gets `mockReset`,\n * `mockClear`, `anyString`, the matchers and the types. The two names below are declared after it:\n * an explicit export wins over a star export, which is what lets this file override exactly two\n * functions and pass the rest through untouched.\n */\nexport * from 'vitest-mock-extended'\n\n/*\n * And the same names again, one by one.\n *\n * `export *` from a CJS package forwards only what a static read of that package can SEE.\n * `vitest-mock-extended` ships CommonJS and assembles part of its exports at runtime, so the star\n * carries the types and loses several of the functions. A spec importing one of them then reads\n * `undefined` and dies at the call, naming the binding and nothing else. Measured on\n * `apps/nest/microservices/mission`:\n *\n * TypeError: (0 , __vite_ssr_import_3__.mockReset) is not a function\n *\n * It is the same shape as the automock namespace built from a static export list, which\n * `server.deps.inline` exists to answer; here the file doing the re-export is ours, so it can\n * simply say the names out loud. A name that ever disappears from the fork breaks the build\n * instead of a suite, which is the better end of that trade.\n */\nexport {\n any,\n anyArray,\n anyBoolean,\n anyFunction,\n anyMap,\n anyNumber,\n anyObject,\n anySet,\n anyString,\n anySymbol,\n arrayIncludes,\n calledWithFn,\n captor,\n isA,\n isMockObject,\n mapHas,\n matches,\n mockClear,\n mocked,\n mockedFn,\n mockFn,\n mockReset,\n notEmpty,\n notNull,\n notUndefined,\n objectContainsKey,\n} from 'vitest-mock-extended'\n\n/**\n * Every deep mock handed out, so `vi.resetAllMocks()` can reach them.\n *\n * ⚠️ Kept on `globalThis`, and that is not laziness. This file is reached by TWO specifiers: the\n * setup imports it by its path, and a spec imports `jest-mock-extended`, which the preset aliases\n * here. Vite resolves those to two ids and instantiates the module twice, so a registry held in\n * module scope would be filled by one instance and read by the other, and the reset would find\n * nothing. It is the same dual-identity trap the axios alias exists to close, one layer up.\n *\n * ⚠️ It does not reach them on its own, and that is a silent divergence. jest's\n * `jest.resetAllMocks()` reset these because `jest-mock-extended` built them with `jest.fn()`, so\n * they sat in jest's own registry. `vitest-mock-extended` builds them its own way, so\n * `vi.resetAllMocks()` walks past them and their call history survives into the next test.\n *\n * Measured on `apps/nest/microservices/activity`, whose suite does exactly 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 test before them.\n * Each one PASSES on its own and fails right after its neighbour, which is the signature of\n * leakage rather than of a broken assertion.\n *\n * A plain Set: a test file gets its own module instance, so the registry lives and dies with it.\n */\nconst REGISTRY = Symbol.for('@hublo/sentinel/deep-mocks')\nconst handedOut: Set<object> = ((globalThis as Record<symbol, unknown>)[REGISTRY] ??=\n new Set<object>()) as Set<object>\n\n/** Reset every deep mock this adapter created, the way jest's registry did. */\nexport function resetDeepMocks(): void {\n for (const created of handedOut) mockExtended.mockReset(created as never)\n}\n\n/** Clear their calls without touching their implementations. */\nexport function clearDeepMocks(): void {\n for (const created of handedOut) mockExtended.mockClear(created as never)\n}\n\nexport const mock = ((...args: unknown[]) => {\n const created = (mockExtended.mock as (...rest: unknown[]) => object)(...args)\n handedOut.add(created)\n return answeringProxy(created)\n}) as unknown as typeof mockExtended.mock\n\nexport const mockDeep = ((...args: unknown[]) => {\n const created = (mockExtended.mockDeep as (...rest: unknown[]) => object)(...args)\n handedOut.add(created)\n return answeringProxy(created)\n}) as unknown as typeof mockExtended.mockDeep\n"],"mappings":";AAiCA,YAAY,kBAAkB;AA8G9B,cAAc;AAkBd;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAAA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAAC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAtIP,IAAM,qBAAqB,oBAAI,IAAI,CAAC,QAAQ,mBAAmB,YAAY,UAAU,CAAC;AAEtF,SAAS,YAAY,QAAgB,KAAwB;AAC3D,MAAI,OAAO,QAAQ,YAAY,mBAAmB,IAAI,GAAG,EAAG;AAE5D,OAAM,OAAmC,GAAG;AAC9C;AAgBA,IAAM,WAAW,oBAAI,QAAwB;AAE7C,SAAS,eAAiCC,OAAY;AACpD,SAAO,IAAI,MAAMA,OAAM;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAcrB,IAAI,QAAQ,KAAK;AACf,UAAI,OAAO,QAAQ,YAAY,mBAAmB,IAAI,GAAG,EAAG,QAAO;AAEnE,YAAM,QAAQ,QAAQ,IAAI,QAAQ,GAAG;AACrC,UAAI,UAAU,KAAM,QAAO;AAC3B,UAAI,OAAO,UAAU,YAAY,OAAO,UAAU,WAAY,QAAO;AASrE,YAAM,aAAa,QAAQ,yBAAyB,QAAQ,GAAG;AAC/D,UACE,eAAe,UACf,WAAW,iBAAiB,SAC5B,WAAW,aAAa,OACxB;AACA,eAAO;AAAA,MACT;AAEA,YAAM,WAAW,SAAS,IAAI,KAAe;AAC7C,UAAI,aAAa,OAAW,QAAO;AAEnC,YAAM,UAAU,eAAe,KAAe;AAC9C,eAAS,IAAI,OAAiB,OAAO;AACrC,aAAO;AAAA,IACT;AAAA,IACA,IAAI,QAAQ,KAAK;AACf,UAAI,QAAQ,IAAI,QAAQ,GAAG,EAAG,QAAO;AACrC,kBAAY,QAAQ,GAAG;AACvB,aAAO,QAAQ,IAAI,QAAQ,GAAG;AAAA,IAChC;AAAA,IACA,yBAAyB,QAAQ,KAAK;AACpC,YAAM,WAAW,QAAQ,yBAAyB,QAAQ,GAAG;AAC7D,UAAI,aAAa,OAAW,QAAO;AACnC,kBAAY,QAAQ,GAAG;AACvB,aAAO,QAAQ,yBAAyB,QAAQ,GAAG;AAAA,IACrD;AAAA,EACF,CAAC;AACH;AAgFA,IAAM,WAAW,uBAAO,IAAI,4BAA4B;AACxD,IAAM,YAA2B,WAAuC,QAAQ,MAC9E,oBAAI,IAAY;AAGX,SAAS,iBAAuB;AACrC,aAAW,WAAW,UAAW,CAAa,uBAAU,OAAgB;AAC1E;AAGO,SAAS,iBAAuB;AACrC,aAAW,WAAW,UAAW,CAAa,uBAAU,OAAgB;AAC1E;AAEO,IAAMA,SAAQ,IAAI,SAAoB;AAC3C,QAAM,UAAwB,kBAAwC,GAAG,IAAI;AAC7E,YAAU,IAAI,OAAO;AACrB,SAAO,eAAe,OAAO;AAC/B;AAEO,IAAMC,aAAY,IAAI,SAAoB;AAC/C,QAAM,UAAwB,sBAA4C,GAAG,IAAI;AACjF,YAAU,IAAI,OAAO;AACrB,SAAO,eAAe,OAAO;AAC/B;","names":["mockClear","mockReset","mock","mockDeep"]}
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
fromModule
|
|
3
|
+
} from "./chunk-ELZHIN6E.js";
|
|
1
4
|
import {
|
|
2
5
|
findWorkspaceRoot
|
|
3
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-NW6UHNYX.js";
|
|
4
7
|
|
|
5
8
|
// src/roles/test/setup/workspace.ts
|
|
6
|
-
import { createRequire } from "module";
|
|
7
|
-
import { join } from "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":[]}
|