@@ -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 ( / ^ \( n o d e : \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