Skip to content

Commit 9df3a01

Browse files
committed
fix(scripts): measure the entry-guard probe's noise floor instead of asserting empty stderr
`check-required-contexts.mjs --verify-required-set` exits 2 = NOT VERIFIED without `--use-env-proxy` and prescribes exactly that flag. Both gates are derived for the same change surface, so they run in one batch under one NODE_OPTIONS -- and under that flag node opens every child with the UNDICI-EHPA experimental warning plus its `--trace-warnings` hint. The entry-guard case asserted a literally empty child stderr, so obeying the other gate turned it red on a tree the seat had just changed. Neither assertion was wrong, so neither is weakened. The probe now spawns an identical child that imports nothing, and compares against what that child prints: anything beyond the measured floor came from the import. Node's pid is the only normalisation. Not a shape filter: node's hint line carries no `[CODE] Warning:`, so a filter written to that shape leaves it behind -- and would swallow a module line wearing the same shape. Four controls pin this, including a module line disguised as a node warning, and the battery floor rises 2 -> 6 to hold them. Closes #15234 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
1 parent ec739e6 commit 9df3a01

1 file changed

Lines changed: 105 additions & 5 deletions

File tree

‎scripts/check-cross-package-test-inputs.mjs‎

Lines changed: 105 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,10 @@ const SELF_TEST_BATTERIES = Object.freeze({
262262
'the INTERPOLATING TEMPLATE argument (#11487)': 4,
263263
'the INTERPOLATING TEMPLATE argument, `NEW_URL_LITERAL` sibling (#12085) ─': 7,
264264
'the RESOLVER half (#10452)': 43,
265-
'the entry guard, driven for real': 2,
265+
// Raised 2 -> 6: the four controls added beside the two original cases are
266+
// what keep "prints nothing of its own" from being satisfied by a probe that
267+
// looks at nothing. Floored, they cannot be dropped back out quietly.
268+
'the entry guard, driven for real': 6,
266269
'the SPLIT test:repo task (#16466)': 16,
267270
'the node_modules REACH rule (#16555)': 18,
268271
});
@@ -2793,20 +2796,117 @@ function selfTest() {
27932796
}
27942797

27952798
battery('the entry guard, driven for real');
2796-
const importProbe = spawnSync(
2799+
//
2800+
// ── Why the noise floor is MEASURED and not pattern-matched ──────────────
2801+
//
2802+
// This probe inherits the batch's `NODE_OPTIONS`, and under some values the
2803+
// RUNTIME writes to the child's stderr before any user code runs. The
2804+
// measured case: `check-required-contexts.mjs --verify-required-set` exits
2805+
// 2 = NOT VERIFIED without `--use-env-proxy` and prescribes exactly that
2806+
// flag, citing #9642 for why the inference it prevents matters. Follow that
2807+
// correct advice and node opens EVERY child with two lines:
2808+
//
2809+
// (node:NNN) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, ...
2810+
// (Use `node --trace-warnings ...` to show where the warning was created)
2811+
//
2812+
// Both gates are derived for the same change surface, so they run in one
2813+
// batch under one `NODE_OPTIONS`. Asserting a LITERALLY empty stderr here
2814+
// therefore turned this case red for obeying the other gate -- a true
2815+
// assertion reporting a regression that does not exist, on a tree the seat
2816+
// had just changed. The other direction is worse and silent: a seat that
2817+
// reads gate A's exit 2 as noise never runs the required-set sweep at all.
2818+
//
2819+
// ⛔ The remedy is NOT to ignore this child's output -- that trades a true
2820+
// assertion for silence. ⛔ It is also not to match node's warning SHAPE:
2821+
// the hint line above carries no `[CODE] Warning:` at all, so a filter
2822+
// written to that description leaves it behind and the case stays red; and
2823+
// such a filter would swallow a line the MODULE wrote in that same shape.
2824+
//
2825+
// So the noise floor is measured. An identical child that imports NOTHING
2826+
// runs first, with the same argv shape and the same inherited env, and
2827+
// whatever IT prints is what this runtime prints unprompted. Anything the
2828+
// real probe prints BEYOND that came from the import. No pattern describes
2829+
// the noise, so this cannot rot when node changes its warning text, and any
2830+
// line the module writes still reds -- including one disguised as a node
2831+
// warning, which the positive controls below drive.
2832+
//
2833+
// The single normalisation is node's pid, which differs between the two
2834+
// children by construction. Nothing else about the text is touched.
2835+
const spawnImportProbe = (preamble) => spawnSync(
27972836
process.execPath,
2798-
['--input-type=module', '-e', `await import(${JSON.stringify(pathToFileURL(fileURLToPath(import.meta.url)).href)});\nconsole.log('ALIVE');`],
2837+
['--input-type=module', '-e', `${preamble}\nconsole.log('ALIVE');`],
27992838
{ encoding: 'utf8' },
28002839
);
2840+
const withoutPid = (stream) => (stream || '').replace(/^\(node:\d+\)/gm, '(node:PID)').trim();
2841+
/** What `probe` wrote on stderr BEYOND what this runtime writes unprompted. */
2842+
const stderrBeyondRuntime = (probe, baseline) => (
2843+
withoutPid(probe.stderr) === withoutPid(baseline.stderr) ? '' : withoutPid(probe.stderr)
2844+
);
2845+
const importOf = (target) => `await import(${JSON.stringify(pathToFileURL(target).href)});`;
2846+
2847+
const runtimeBaseline = spawnImportProbe('');
2848+
const importProbe = spawnImportProbe(importOf(fileURLToPath(import.meta.url)));
28012849
ok(
2802-
'importing this module prints NOTHING -- the dispatch is behind the entry guard',
2803-
(importProbe.stdout || '').trim() === 'ALIVE' && (importProbe.stderr || '').trim() === '',
2850+
'importing this module prints NOTHING OF ITS OWN -- the dispatch is behind the entry guard',
2851+
(importProbe.stdout || '').trim() === 'ALIVE' && stderrBeyondRuntime(importProbe, runtimeBaseline) === '',
28042852
);
28052853
ok(
28062854
'importing this module does not exit the importer -- it survives to run its own code',
28072855
importProbe.status === 0 && (importProbe.stdout || '').includes('ALIVE'),
28082856
);
28092857

2858+
// The controls that keep the case above from passing by ignoring everything.
2859+
// ⚠️ Without these, "prints nothing of its own" and "prints nothing that is
2860+
// ever looked at" are the same green.
2861+
{
2862+
const guardDir = mkdtempSync(join(tmpdir(), 'crosspkg-entryguard-'));
2863+
try {
2864+
const probeImporting = (file, source) => {
2865+
writeFileSync(file, source);
2866+
return spawnImportProbe(importOf(file));
2867+
};
2868+
2869+
const quiet = join(guardDir, 'quiet.mjs');
2870+
ok(
2871+
'NEGATIVE CONTROL: importing a module that writes nothing is clean under THIS runtime',
2872+
stderrBeyondRuntime(probeImporting(quiet, 'export const nothing = 1;\n'), runtimeBaseline) === '',
2873+
);
2874+
2875+
const noisy = join(guardDir, 'noisy.mjs');
2876+
ok(
2877+
'POSITIVE CONTROL: one line the module itself writes to stderr still reds',
2878+
stderrBeyondRuntime(
2879+
probeImporting(noisy, "console.error('a line this module wrote itself');\n"),
2880+
runtimeBaseline,
2881+
).includes('a line this module wrote itself'),
2882+
);
2883+
2884+
// The case that separates a MEASURED baseline from a shape filter: this
2885+
// line is shaped exactly like one of node's own warnings. A filter
2886+
// matching that shape swallows it; subtracting a measured baseline
2887+
// cannot, because the baseline child never wrote it.
2888+
const disguised = join(guardDir, 'disguised.mjs');
2889+
ok(
2890+
'POSITIVE CONTROL: a module line DISGUISED as a node warning still reds',
2891+
stderrBeyondRuntime(
2892+
probeImporting(disguised, "console.error('(node:4242) [FAKE-CODE] Warning: written by the MODULE, not the runtime');\n"),
2893+
runtimeBaseline,
2894+
).includes('written by the MODULE, not the runtime'),
2895+
);
2896+
2897+
// The stdout half is unchanged and stays an exact match, so a verdict
2898+
// printed on the importer's stdout -- the #4449 defect itself -- reds
2899+
// without consulting the baseline at all.
2900+
const stdoutLeak = join(guardDir, 'stdout-leak.mjs');
2901+
ok(
2902+
'POSITIVE CONTROL: a module that writes to STDOUT still reds -- that half is untouched',
2903+
(probeImporting(stdoutLeak, "console.log('a verdict on the importer stdout');\n").stdout || '').trim() !== 'ALIVE',
2904+
);
2905+
} finally {
2906+
rmSync(guardDir, { recursive: true, force: true });
2907+
}
2908+
}
2909+
28102910
// The floor runs BEFORE the verdict below, so a success line can only be
28112911
// printed by a run in which every declared battery registered its cases.
28122912
for (const message of batteryFloorFailures()) cases.push({ label: message, cond: false });

0 commit comments

Comments
 (0)