fix(ci): run allocation gates on Node 26

This commit is contained in:
BreezeDelegate committed 2026-10-25 03:15:11 +01:00
1 parent 2dca762e4a
commit bbfb23a765
8 files changed
+46 -48

No files matched your search

@@ -0,0 +1 @@
- Run the existing GC-bracketed allocation regression gates on Node 26 as well as Node 24. Keep the current byte budgets so CI catches allocation regressions on both supported runtimes (#39).
+8 -9
View File
@@ -3148,9 +3148,9 @@ down | auth-failed` (plus the unchanged `missing-key`/`unsupported`) with
> style-switch persistence state. Browser/GPU performance comparison remains > style-switch persistence state. Browser/GPU performance comparison remains
> operator-side; the final accounting records only comparable population > operator-side; the final accounting records only comparable population
> reductions and deterministic Node allocation/solve measurements. Those > reductions and deterministic Node allocation/solve measurements. Those
> allocation gates are calibrated on Node.js 24.14.x; `package.json` permits > allocation gates are calibrated on both supported Node majors (24 and 26);
> supported product runtimes on Node 24 or 26, while the allocation runner > `package.json` permits those same product runtimes, and the existing CI matrix
> separately enforces Node 24 for these two calibrated probes. Each probe > executes the two calibrated probes on both entries. Each probe
> compiles synchronously and discards explicit-GC > compiles synchronously and discards explicit-GC
> transition chunks before applying the unchanged byte ceilings. The unit > transition chunks before applying the unchanged byte ceilings. The unit
> runner executes ordinary test files with Node's default parallelism, then > runner executes ordinary test files with Node's default parallelism, then
@@ -4987,12 +4987,11 @@ and unreachable upstream (502/504) separately from road geometry.
> **Reading `npm test` totals:** the count depends on the Node major. The two > **Reading `npm test` totals:** the count depends on the Node major. The two
> GC-bracketed allocation microbenchmarks (`src/data/focusAllocations.test.mjs` > GC-bracketed allocation microbenchmarks (`src/data/focusAllocations.test.mjs`
> = 1 test, `src/overlays/worldOverlayAllocation.test.mjs` = 13) only RUN on the > = 1 test, `src/overlays/worldOverlayAllocation.test.mjs` = 13) RUN on both
> calibrated Node 24 runtime; on any other major the runner skips both files and > supported Node 24 and Node 26. Unsupported majors may still skip those files
> their 14 tests are absent from the total. A branch total quoted without its > unless `GEV_REQUIRE_ALLOCATION_GATE=1` is set; every current supported CI
> Node version is therefore not reproducible. As of the fly_route cinematic > matrix entry is calibrated and therefore executes them. Historical totals recorded from unsupported Node 25
> branch: **2,281 on Node 25.6.1** (allocation suites skipped) = 2,255 on > runs can therefore remain lower because their 14 allocation tests were skipped.
> `main` + 26 route pins; the same tree on Node 24 reports 2,295.
- **Camera verbs** (`src/cameraVerbs.js`) — one motion slot, driven per clock tick. `move_camera` orbits/pans/tilts/rotates; `fly_route` is a cinematic dolly along an existing route annotation. `rotate_globe` rotates the world-space camera rigidly around Earth's center using its screen-up axis, preserving geocentric range and framing. Left/right describe visible globe motion, while `rotate` turns the camera in place. Omitted globe direction/mode default to right/continuous; explicit values prevail. Other camera motions keep their once default. Admission requires 3D, no Cockpit/tracking owner and the whole globe inside the existing keyhole circle, using the shared keyhole/projection geometry and vertical camera FOV with a rotation-local bounding-sphere cone for off-axis containment. Canvas-edge clipping alone does not block a rigid rotation or its unchanged Globe View preset. There is no altitude cutoff. An already smaller, fully framed globe rotates directly; repeated nudges, direction and speed changes recheck current framing without demanding a reset merely because ellipsoid altitude varied. A close-up or overlap refuses without moving the camera. When the unchanged Globe View preset fits, voice says “Globe rotation isn’t available from this view. You can say ‘Reset to Globe View and rotate the globe.’” without asking a permission question or automatically resetting. Explicit Reset/Show Globe View commands remain direct; an explicit reset-and-rotate request waits for reset success before requesting rotation. If OpenAI dispatches both tools together, rotation waits for earlier same-response resets and is skipped if any fails or is cancelled. OpenAI rejects later tool calls in the same response after a globe refusal, including calls arriving while globe admission is pending, and preserves response-scoped `tool_choice: none` for the refusal follow-up; Gemini rejects further tool execution, including queued sibling calls, until fresh typed input or admitted speech. Model interruption, advisory captions and microphone toggles do not release that guard. There is no pending Yes/No offer. Outside the refused response, explicit-command interpretation and avoiding repeated resets remain model-guided; a distinct model-issued reset call is not an app-enforced proof of user intent. A failed standalone reset can still be retried by the model, and a reset result does not guarantee the camera has fully settled. Before suggesting a reset, a cloned camera checks the unchanged Globe View destination against the same keyhole/depth rules without moving the live camera. If that preset cannot fit, `globe-reset-unavailable` asks for further zoom and the refusal instructs further zoom instead of a reset; this also covers ultrawide views. Unsupported view geometry or non-WGS84 projection assumptions fail closed without the closer-zoom instruction. Supported camera inputs/navigation intents cancel rotation before their action. Finite radius greater than the ellipsoid's maximum radius proves whole-arc clearance at constant centre-distance; no low-altitude swept solver is needed. The unchanged Globe View reset preset remains 18,000,000 m. Terrain and buildings remain outside the guarantee. - **Camera verbs** (`src/cameraVerbs.js`) — one motion slot, driven per clock tick. `move_camera` orbits/pans/tilts/rotates; `fly_route` is a cinematic dolly along an existing route annotation. `rotate_globe` rotates the world-space camera rigidly around Earth's center using its screen-up axis, preserving geocentric range and framing. Left/right describe visible globe motion, while `rotate` turns the camera in place. Omitted globe direction/mode default to right/continuous; explicit values prevail. Other camera motions keep their once default. Admission requires 3D, no Cockpit/tracking owner and the whole globe inside the existing keyhole circle, using the shared keyhole/projection geometry and vertical camera FOV with a rotation-local bounding-sphere cone for off-axis containment. Canvas-edge clipping alone does not block a rigid rotation or its unchanged Globe View preset. There is no altitude cutoff. An already smaller, fully framed globe rotates directly; repeated nudges, direction and speed changes recheck current framing without demanding a reset merely because ellipsoid altitude varied. A close-up or overlap refuses without moving the camera. When the unchanged Globe View preset fits, voice says “Globe rotation isn’t available from this view. You can say ‘Reset to Globe View and rotate the globe.’” without asking a permission question or automatically resetting. Explicit Reset/Show Globe View commands remain direct; an explicit reset-and-rotate request waits for reset success before requesting rotation. If OpenAI dispatches both tools together, rotation waits for earlier same-response resets and is skipped if any fails or is cancelled. OpenAI rejects later tool calls in the same response after a globe refusal, including calls arriving while globe admission is pending, and preserves response-scoped `tool_choice: none` for the refusal follow-up; Gemini rejects further tool execution, including queued sibling calls, until fresh typed input or admitted speech. Model interruption, advisory captions and microphone toggles do not release that guard. There is no pending Yes/No offer. Outside the refused response, explicit-command interpretation and avoiding repeated resets remain model-guided; a distinct model-issued reset call is not an app-enforced proof of user intent. A failed standalone reset can still be retried by the model, and a reset result does not guarantee the camera has fully settled. Before suggesting a reset, a cloned camera checks the unchanged Globe View destination against the same keyhole/depth rules without moving the live camera. If that preset cannot fit, `globe-reset-unavailable` asks for further zoom and the refusal instructs further zoom instead of a reset; this also covers ultrawide views. Unsupported view geometry or non-WGS84 projection assumptions fail closed without the closer-zoom instruction. Supported camera inputs/navigation intents cancel rotation before their action. Finite radius greater than the ellipsoid's maximum radius proves whole-arc clearance at constant centre-distance; no low-altitude swept solver is needed. The unchanged Globe View reset preset remains 18,000,000 m. Terrain and buildings remain outside the guarantee.
- **The route dolly is shaped, not linear** (2026-08-20). A trapezoid speed profile (smoothstep up, cruise, smoothstep down, distance taken as the closed-form integral) eases both ends without changing the pace — duration is still `totalM / ROUTE_M_S[speed]`, with one exception: a 0.5 s minimum keeps a degenerate route from being an instant teleport, so routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly SLOWER than the speed word, never faster. Turns bank up to **10°** (a 90° street corner settles near 7.5°), measured as a triangular pulse over a 4 s window centred on the camera so the roll leads in and unwinds after. Altitude breathes ±20 m around the 260 m mean and lifts up to 26 m into turns. Pitch is LOCKED at −32°; heading comes from a gaze that leads the path by 6.5 s of travel. `prefers-reduced-motion` zeroes the roll and the altitude shaping and keeps the easing. - **The route dolly is shaped, not linear** (2026-08-20). A trapezoid speed profile (smoothstep up, cruise, smoothstep down, distance taken as the closed-form integral) eases both ends without changing the pace — duration is still `totalM / ROUTE_M_S[speed]`, with one exception: a 0.5 s minimum keeps a degenerate route from being an instant teleport, so routes under 10 m (slow) / 20 m (normal) / 45 m (fast) fly SLOWER than the speed word, never faster. Turns bank up to **10°** (a 90° street corner settles near 7.5°), measured as a triangular pulse over a 4 s window centred on the camera so the roll leads in and unwinds after. Altitude breathes ±20 m around the 260 m mean and lifts up to 26 m into turns. Pitch is LOCKED at −32°; heading comes from a gaze that leads the path by 6.5 s of travel. `prefers-reduced-motion` zeroes the roll and the altitude shaping and keeps the easing.
+12 -12
View File
@@ -239,11 +239,11 @@ function satisfiesEngines(version, range) {
* re-resolve the interpreter from PATH and hand the gate a different Node than * re-resolve the interpreter from PATH and hand the gate a different Node than
* the one the check believes it selected. * the one the check believes it selected.
* @param {string} version e.g. "v24.19.0" * @param {string} version e.g. "v24.19.0"
* @returns {boolean} true only for a parseable Node 24. * @returns {boolean} true for a parseable supported Node 24 or 26.
*/ */
function isCalibratedAllocationRuntime(version) { function isCalibratedAllocationRuntime(version) {
const [maj] = String(version || '').trim().replace(/^v/, '').split('.').map((n) => Number.parseInt(n, 10)); const [maj] = String(version || '').trim().replace(/^v/, '').split('.').map((n) => Number.parseInt(n, 10));
return maj === 24; return maj === 24 || maj === 26;
} }
/** /**
@@ -546,7 +546,7 @@ const check = (spec) => { CHECKS.push(spec); };
// ─── A · REPO GATES ─────────────────────────────────────────────────────── // ─── A · REPO GATES ───────────────────────────────────────────────────────
check({ check({
id: 'A1', group: 'A', desc: 'Node runtime satisfies package.json engines (allocation budgets are pinned to Node 24)', id: 'A1', group: 'A', desc: 'Node runtime satisfies package.json engines (allocation budgets cover supported Node majors)',
run: async () => { run: async () => {
const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8')); const pkg = JSON.parse(readFileSync(resolve(REPO_ROOT, 'package.json'), 'utf8'));
const range = pkg.engines?.node || '(unset)'; const range = pkg.engines?.node || '(unset)';
@@ -557,7 +557,7 @@ check({
if (satisfied) return pass(`node ${process.versions.node} satisfies "${range}"`); if (satisfied) return pass(`node ${process.versions.node} satisfies "${range}"`);
// An off-range runtime is an environment problem, not a product defect — // An off-range runtime is an environment problem, not a product defect —
// but it silently disables the allocation gate, so say so loudly. A3 then // but it silently disables the allocation gate, so say so loudly. A3 then
// runs that gate under a discovered Node 24. // runs that gate under a discovered calibrated Node 24 fallback.
return env.node24 return env.node24
? skip(`node ${process.versions.node} is OUTSIDE "${range}"; A3 runs the allocation gate under ${env.node24.label}, but prefer running the whole L9 pass on Node 24`, 'ENV') ? skip(`node ${process.versions.node} is OUTSIDE "${range}"; A3 runs the allocation gate under ${env.node24.label}, but prefer running the whole L9 pass on Node 24`, 'ENV')
: fail(`node ${process.versions.node} is OUTSIDE "${range}" and no Node 24 runtime was found — the allocation gate cannot run at all`); : fail(`node ${process.versions.node} is OUTSIDE "${range}" and no Node 24 runtime was found — the allocation gate cannot run at all`);
@@ -579,13 +579,13 @@ check({
}); });
check({ check({
id: 'A3', group: 'A', desc: 'Allocation microbenchmarks actually EXECUTE (they silently skip off Node 24)', id: 'A3', group: 'A', desc: 'Allocation microbenchmarks actually EXECUTE on supported Node majors',
run: async () => { run: async () => {
// npm test prints "[unit] SKIPPED n allocation microbenchmarks" and still // npm test prints "[unit] SKIPPED n allocation microbenchmarks" and still
// exits 0 when the runtime is not Node 24. A green suite is therefore NOT // exits 0 when the runtime is outside the calibrated supported majors. A green
// proof the gate ran. Force it, under Node 24 when one is discoverable. // suite is therefore NOT proof the gate ran. Force it under a calibrated runtime.
// //
// ALWAYS invoke the Node 24 BINARY directly — never `npm test`. npm // ALWAYS invoke the calibrated Node binary directly — never `npm test`. npm
// re-resolves the interpreter from PATH and can land back on the system // re-resolves the interpreter from PATH and can land back on the system
// Node even when THIS process is already 24: running the whole matrix under // Node even when THIS process is already 24: running the whole matrix under
// `mise exec [email protected] --`, the npm shell-out still re-execed system // `mise exec [email protected] --`, the npm shell-out still re-execed system
@@ -597,10 +597,10 @@ check({
const v = await sh(bin, ['--version'], { timeoutMs: 60000 }); const v = await sh(bin, ['--version'], { timeoutMs: 60000 });
const version = (v.out || '').trim(); const version = (v.out || '').trim();
if (!isCalibratedAllocationRuntime(version)) { if (!isCalibratedAllocationRuntime(version)) {
// run-unit-tests.mjs refuses to measure calibrated budgets off Node 24, // run-unit-tests.mjs refuses to measure calibrated budgets off supported Node majors,
// so a wrong binary means the gate never ran. That is this check failing // so a wrong binary means the gate never ran. That is this check failing
// to establish its own claim — not evidence about the product. // to establish its own claim — not evidence about the product.
return crash(`the runtime selected for the allocation gate (${label}) reports ${version || 'no parseable version'}, not Node 24 — the gate would refuse or silently skip, so this check verified nothing`); return crash(`the runtime selected for the allocation gate (${label}) reports ${version || 'no parseable version'}, not Node 24 or 26 — the gate would refuse or silently skip, so this check verified nothing`);
} }
const r = await sh(bin, [resolve(REPO_ROOT, 'scripts/run-unit-tests.mjs')], { const r = await sh(bin, [resolve(REPO_ROOT, 'scripts/run-unit-tests.mjs')], {
timeoutMs: 600000, env: { GEV_REQUIRE_ALLOCATION_GATE: '1' }, timeoutMs: 600000, env: { GEV_REQUIRE_ALLOCATION_GATE: '1' },
@@ -611,9 +611,9 @@ check({
}; };
const [maj] = process.versions.node.split('.').map(Number); const [maj] = process.versions.node.split('.').map(Number);
if (maj === 24) return runGate(process.execPath, 'this runtime'); if (maj === 24 || maj === 26) return runGate(process.execPath, 'this runtime');
if (env.node24) return runGate(env.node24.bin, env.node24.label); if (env.node24) return runGate(env.node24.bin, env.node24.label);
return skip(`no Node 24 runtime found (running ${process.versions.node}); the gate SKIPS silently — install Node 24 and re-run`, 'OWNER-RUN'); return skip(`no calibrated Node 24/26 runtime found (running ${process.versions.node}); the gate SKIPS silently — install a supported Node runtime and re-run`, 'OWNER-RUN');
}, },
}); });
+11 -11
View File
@@ -8,15 +8,16 @@ export const ALLOCATION_TEST_FILES = Object.freeze([
'src/overlays/worldOverlayAllocation.test.mjs', 'src/overlays/worldOverlayAllocation.test.mjs',
]); ]);
/** Whether this runtime matches the one the allocation budgets were calibrated on. */ /** Whether this runtime matches a supported major the allocation budgets are calibrated on. */
export function isCalibratedAllocationRuntime(version = process.versions.node) { export function isCalibratedAllocationRuntime(version = process.versions.node) {
return Number.parseInt(String(version).split('.')[0], 10) === 24; const major = Number.parseInt(String(version).split('.')[0], 10);
return major === 24 || major === 26;
} }
/** Require the runtime on which allocation budgets were calibrated. */ /** Require a runtime on which allocation budgets are calibrated. */
export function assertNode24AllocationRuntime(version = process.versions.node) { export function assertCalibratedAllocationRuntime(version = process.versions.node) {
if (!isCalibratedAllocationRuntime(version)) { if (!isCalibratedAllocationRuntime(version)) {
throw new Error(`Allocation budgets require the calibrated Node 24 runtime; received ${version}`); throw new Error(`Allocation budgets require calibrated Node 24 or 26; received ${version}`);
} }
return version; return version;
} }
@@ -86,19 +87,18 @@ export function runUnitTests() {
const parallelStatus = runTests(['--test', ...plan.parallel]); const parallelStatus = runTests(['--test', ...plan.parallel]);
if (parallelStatus !== 0) return parallelStatus; if (parallelStatus !== 0) return parallelStatus;
// The GC-bracketed budgets are calibrated on Node 24 and are meaningless on // The GC-bracketed budgets are calibrated on every supported Node major.
// other allocators. A contributor's suite must stay green on any supported // Unsupported runtimes may still run ordinary tests, so uncalibrated runtimes skip the
// engine (package.json permits >=24), so uncalibrated runtimes skip the
// probes with a warning. Set GEV_REQUIRE_ALLOCATION_GATE=1 (pinned CI / // probes with a warning. Set GEV_REQUIRE_ALLOCATION_GATE=1 (pinned CI /
// release batteries) to make an uncalibrated runtime a hard failure. // release batteries) to make an uncalibrated runtime a hard failure.
if (!isCalibratedAllocationRuntime()) { if (!isCalibratedAllocationRuntime()) {
if (process.env.GEV_REQUIRE_ALLOCATION_GATE === '1') { if (process.env.GEV_REQUIRE_ALLOCATION_GATE === '1') {
assertNode24AllocationRuntime(); assertCalibratedAllocationRuntime();
} }
console.warn( console.warn(
`[unit] SKIPPED ${ALLOCATION_TEST_FILES.length} allocation microbenchmarks: ` `[unit] SKIPPED ${ALLOCATION_TEST_FILES.length} allocation microbenchmarks: `
+ `budgets are calibrated for Node 24, running ${process.versions.node}. ` + `budgets are calibrated for Node 24 and 26, running ${process.versions.node}. `
+ 'Run under Node 24 (or set GEV_REQUIRE_ALLOCATION_GATE=1 to fail instead).', + 'Run under Node 24 or 26 (or set GEV_REQUIRE_ALLOCATION_GATE=1 to fail instead).',
); );
return 0; return 0;
} }
+1 -1
View File
@@ -13,7 +13,7 @@ import { isCalibratedAllocationRuntime } from '../../scripts/run-unit-tests.mjs'
*/ */
test('converged focus treatment stays within the GC-bracketed allocation budget', (t) => { test('converged focus treatment stays within the GC-bracketed allocation budget', (t) => {
if (!isCalibratedAllocationRuntime()) { if (!isCalibratedAllocationRuntime()) {
return t.skip(`allocation budgets are calibrated for Node 24; running ${process.versions.node}`); return t.skip(`allocation budgets are calibrated for Node 24 or 26; running ${process.versions.node}`);
} }
const result = spawnSync( const result = spawnSync(
process.execPath, process.execPath,
+1 -1
View File
@@ -333,7 +333,7 @@ function runAllocationProbe(entryCount, profile = 'generic') {
for (const workload of WORKLOADS) { for (const workload of WORKLOADS) {
test(`steady moving-source frames stay in budget ${workload.name}`, (t) => { test(`steady moving-source frames stay in budget ${workload.name}`, (t) => {
if (!CALIBRATED_ALLOCATION_RUNTIME) { if (!CALIBRATED_ALLOCATION_RUNTIME) {
return t.skip(`allocation budgets are calibrated for Node 24; running ${process.versions.node}`); return t.skip(`allocation budgets are calibrated for Node 24 or 26; running ${process.versions.node}`);
} }
const payload = runAllocationProbe(workload.entries, workload.profile); const payload = runAllocationProbe(workload.entries, workload.profile);
+2 -1
View File
@@ -127,9 +127,10 @@ test('an unevaluable engines range reports unknown rather than guessing', () =>
// A full pass run under `mise exec node@24 --` still re-execed system Node 25 // A full pass run under `mise exec node@24 --` still re-execed system Node 25
// through npm; the forced gate refused ("received 25.6.1") and A3 reported it // through npm; the forced gate refused ("received 25.6.1") and A3 reported it
// as a product failure. A3 now invokes the binary directly and checks it. // as a product failure. A3 now invokes the binary directly and checks it.
test('only a parseable Node 24 counts as the calibrated allocation runtime', () => { test('every supported Node major counts as a calibrated allocation runtime', () => {
assert.equal(isCalibratedAllocationRuntime('v24.19.0'), true); assert.equal(isCalibratedAllocationRuntime('v24.19.0'), true);
assert.equal(isCalibratedAllocationRuntime('24.19.0'), true, 'the v prefix is optional'); assert.equal(isCalibratedAllocationRuntime('24.19.0'), true, 'the v prefix is optional');
assert.equal(isCalibratedAllocationRuntime('v26.3.0'), true);
assert.equal(isCalibratedAllocationRuntime('v25.6.1'), false, 'the npm re-resolution case'); assert.equal(isCalibratedAllocationRuntime('v25.6.1'), false, 'the npm re-resolution case');
assert.equal(isCalibratedAllocationRuntime('v22.21.1'), false); assert.equal(isCalibratedAllocationRuntime('v22.21.1'), false);
for (const junk of ['', ' ', 'node', undefined, null]) { for (const junk of ['', ' ', 'node', undefined, null]) {
+10 -13
View File
@@ -7,7 +7,7 @@ import {
ALLOCATION_TEST_FILES, ALLOCATION_TEST_FILES,
UNIT_TEST_ROOTS, UNIT_TEST_ROOTS,
allocationTestArgs, allocationTestArgs,
assertNode24AllocationRuntime, assertCalibratedAllocationRuntime,
buildUnitTestPlan, buildUnitTestPlan,
discoverUnitTestFiles, discoverUnitTestFiles,
isCalibratedAllocationRuntime, isCalibratedAllocationRuntime,
@@ -87,30 +87,27 @@ test('the runner discovers every test file in the repository', () => {
} }
}); });
test('allocation runtime calibration is explicit and pinned to Node 24', () => { test('allocation runtime calibration covers both supported Node majors', () => {
assert.equal(assertNode24AllocationRuntime('24.19.0'), '24.19.0'); assert.equal(assertCalibratedAllocationRuntime('24.19.0'), '24.19.0');
assert.throws( assert.throws(
() => assertNode24AllocationRuntime('22.23.1'), () => assertCalibratedAllocationRuntime('22.23.1'),
/calibrated Node 24 runtime/, /calibrated Node 24 or 26/,
);
assert.throws(
() => assertNode24AllocationRuntime('26.0.0'),
/calibrated Node 24 runtime/,
); );
assert.equal(assertCalibratedAllocationRuntime('26.0.0'), '26.0.0');
assert.equal(isCalibratedAllocationRuntime('24.19.0'), true); assert.equal(isCalibratedAllocationRuntime('24.19.0'), true);
assert.equal(isCalibratedAllocationRuntime('22.23.1'), false); assert.equal(isCalibratedAllocationRuntime('22.23.1'), false);
assert.equal(isCalibratedAllocationRuntime('26.3.0'), false); assert.equal(isCalibratedAllocationRuntime('26.3.0'), true);
}); });
test('npm test stays green on every supported engine, not only the calibrated one', () => { test('npm test calibrates both supported engines while retaining off-range warnings', () => {
// package.json wiring: `npm test` must invoke this runner, and the engines // package.json wiring: `npm test` must invoke this runner, and the engines
// range it advertises must not be narrower than what the runner tolerates. // range it advertises must not be narrower than what the runner tolerates.
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')); const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
assert.equal(pkg.scripts.test, 'node scripts/run-unit-tests.mjs'); assert.equal(pkg.scripts.test, 'node scripts/run-unit-tests.mjs');
const enginesNode = String(pkg.engines?.node || ''); const enginesNode = String(pkg.engines?.node || '');
assert.ok(enginesNode, 'engines.node must be declared'); assert.ok(enginesNode, 'engines.node must be declared');
// The runner throws for uncalibrated runtimes ONLY behind the explicit // Unsupported runtimes only skip probes without the explicit opt-in hard gate.
// opt-in env; by default it skips, so a supported non-24 engine cannot fail. // Node 26 is calibrated and runs those probes with the same budgets.
const runner = readFileSync(new URL('../scripts/run-unit-tests.mjs', import.meta.url), 'utf8'); const runner = readFileSync(new URL('../scripts/run-unit-tests.mjs', import.meta.url), 'utf8');
assert.match(runner, /GEV_REQUIRE_ALLOCATION_GATE/); assert.match(runner, /GEV_REQUIRE_ALLOCATION_GATE/);
assert.match(runner, /SKIPPED .*allocation microbenchmarks/); assert.match(runner, /SKIPPED .*allocation microbenchmarks/);