Skip to content

Commit e4e22c7

Browse files
claude[bot]claude
andauthored
fix(scripts): measure the entry-guard probe's noise floor so both derived gates share one NODE_OPTIONS (#17764)
Closes #15234 Clause-②: no Two gates derived for the same change surface disagreed about what `NODE_OPTIONS` may be, and following the correct advice of one turned the other red. ## What was wrong `check-required-contexts.mjs --verify-required-set` exits `2 = NOT VERIFIED` without `--use-env-proxy` and prints that remedy itself, citing #9642 for why the inference it prevents matters. `dispatch-gates` derives both gates for the same change surface, so a seat runs them in one batch under one `NODE_OPTIONS`. Under that flag node opens **every** child process with two stderr lines, before any user code runs: ``` (node:NNN) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, expect them to change at any time. (Use `node --trace-warnings ...` to show where the warning was created) ``` The entry-guard case in `check-cross-package-test-inputs.mjs --self-test` asserted a **literally empty** child stderr. So obeying gate A turned gate B red on a tree the seat had just changed, and the natural reading is "my diff broke the entry guard". The other direction is worse because it is silent: a seat that reads gate A's exit 2 as noise never runs the required-set sweep at all, which is the entire reason #9642 exists. ## The fences, held Neither assertion was the defect, and neither is weakened. The entry-guard property is still pinned; gate A's proxy advice is still printed, and `check-required-contexts.mjs` is **not touched by this PR**. ## The direction chosen, and the evidence that decided it Triage listed three candidates and deliberately chose none. Two were rejected on measurement, not taste: **Rejected — filter node's warning shape.** Triage described this as ignoring lines matching `(node:NNN) [...] Warning:`. Measured against the real output, that shape matches only the **first** of the two lines: ``` MATCHED | (node:7905) [UNDICI-EHPA] Warning: EnvHttpProxyAgent is experimental, ... UNMATCHED | (Use `node --trace-warnings ...` to show where the warning was created) ``` A filter written to that description leaves the hint line behind and the case stays red. Worse, any such filter would also swallow a line the **module** wrote wearing the same shape. **Rejected — `--no-warnings`, or stripping the flag for that spawn.** Both mask real runtime warnings, and both are a per-flag allowlist: the next `NODE_OPTIONS` value that makes node talk re-breaks the case. **Chosen — measure the noise floor instead of describing it.** An identical child that imports **nothing** is spawned first, with the same argv shape and the same inherited env. Whatever *it* prints is what this runtime prints unprompted; anything the real probe prints beyond that came from the import. No pattern describes the noise, so this cannot rot when node changes its warning text. The single normalisation is node's pid, which differs between the two children by construction; nothing else about the text is touched. ## Four-way control (acceptance item 2) Same tree, same commit, flag as the only variable: | gate | no flag | `--use-env-proxy` | | --- | --- | --- | | A `check-required-contexts.mjs --verify-required-set` | **2** (NOT VERIFIED, unchanged) | **0** | | B `check-cross-package-test-inputs.mjs --self-test` — before | **0** | **1** (the spurious red) | | B — after this PR | **0** (156 cases) | **0** (156 cases) | Both gates now pass under one `NODE_OPTIONS=--use-env-proxy --max-old-space-size=4096`. Gate A's exit 2 without the flag is unchanged and is not a failure of the tree: it classifies the environment. ## Positive controls (acceptance item 3) The case must not be green because it looks at nothing. Four controls are added beside the two original cases and all run in both environments: - NEGATIVE CONTROL — importing a module that writes nothing is clean under this runtime. - POSITIVE CONTROL — one line the module writes to stderr still reds. - POSITIVE CONTROL — a module line **disguised as a node warning** still reds. This is the case that separates a measured baseline from a shape filter: a shape filter swallows it, subtracting a measured baseline cannot, because the baseline child never wrote it. - POSITIVE CONTROL — a module that writes to **stdout** still reds; that half of the assertion is an exact match and is untouched. The battery floor for `the entry guard, driven for real` rises `2 -> 6` so the controls cannot be dropped back out quietly. ## Ablation — the real case, not only the controls A `console.error` was inserted at module scope, outside the entry guard, so it runs on import. On-disk landing was proven by marker count (`0` before, `1` after) and by blob hash before and after; restore was proven by hash equality with the `HEAD` blob and an empty `git diff HEAD`, from a trap. | | no flag | `--use-env-proxy` | | --- | --- | --- | | mutated | **1** — `FAIL importing this module prints NOTHING OF ITS OWN` | **1** — same single FAIL | The real case reds in **both** environments, so the green above was not bought with blindness. ## The boundary of this measurement ⚠️ What this probe cannot see, since that is not legible from the green. The subtraction assumes the two children's runtime noise is **identical apart from the pid**. That holds for everything driven here, but it is an assumption about node's behaviour, not a property proved of it. If some `NODE_OPTIONS` value ever makes the runtime write something that varies between two otherwise-identical spawns — a port, a temp path, an elapsed-time figure, any token minted per process — the two stderrs differ, that difference is attributed to the import, and the case goes red with the whole probe stderr quoted at it. That is the **opposite** failure from the one fixed here, and it is the safer of the two: it is loud rather than silent, and it cannot hide a real entry-guard regression — it can only manufacture a false one. It is a real boundary all the same, and it is **not measured**. ⭐ What was actually driven is `--use-env-proxy`, plus the no-flag control, as the four-way table above records. Nothing else in the `NODE_OPTIONS` space was exercised, so that flag is the single value this baseline is known to be stable under. ⇒ If it ever bites, the remedy is to normalise the varying token at the line, exactly as the pid is normalised today, with the reason recorded beside it. ⛔ Not by widening the comparison into a general "ignore output" filter — that is precisely the trade this PR exists to refuse. ## Reverse-read — what this makes false, zeros included Two sentences, both re-judged in place and neither deleted: 1. `SELF_TEST_BATTERIES['the entry guard, driven for real'] = 2` — raised to `6`, with the reason recorded at the line. 2. The case label `importing this module prints NOTHING` — now `prints NOTHING OF ITS OWN`. Read literally, the old wording was already false under the flag; that was the defect. The zeros, each one read rather than assumed: - **0** sentences in `docs/**`, `content/docs/**` or `AGENTS.md` are falsified; the entry-guard probe's stderr assertion is described nowhere outside the file. - **0** changes owed to `check-required-contexts.mjs`; every `--use-env-proxy` mention in the tree was read and all 21 remain true. - **0** movement in the `check-published-list-mirrors` row for this module: it governs `RECOGNISED_PATH_SPELLINGS` against `AGENTS.md`, which this PR does not touch. Gate re-run green. - **0** governed surfaces touched. ## Changeset — measured, not assumed `skip-changeset`, applied as a **label**. Root manifest is `private: true`; of the 70 published packages with a `files[]`, **0** ship anything under repo-root `scripts/`, and the edited path is outside every package directory. Nothing published moves. ## 验收备注 Noted, not filed: none beyond the finding below. Filed as #17765: the same defect exists in `scripts/pm/dispatch-gates.mjs`, whose own self-test asserts `(imported.stderr ?? '').trim() === ''` on a spawned consumer and reds identically under the flag (measured directly on that consumer shape). It is **not** fixed here: the landing point for this card is the two gates it names, and that file's self-test exceeds this container's 10-minute foreground cap (it timed out at `BASE` too, before this diff), so an edit there could not be verified in this run. --- _Generated by [Claude Code](https://claude.ai/code)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 52fa9db commit e4e22c7

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)