@@ -19,6 +19,7 @@ import {
1919 loadGuardConfig ,
2020 parseAllowlistText ,
2121 parseGuardConfig ,
22+ parseInjectedScannedFiles ,
2223 parseTsPruneLine ,
2324 tsPruneSpawn ,
2425 validateAllowlistEntry ,
@@ -28,6 +29,19 @@ import {
2829
2930const repoRoot = join ( import . meta. dir , ".." ) ;
3031
32+ // tsc --listFilesOnly costs ~1.5s per spawn and the count is deterministic
33+ // for a clean tree. The floor tests and the end-to-end probe share one run
34+ // through this cache; each caller computes it on first use, so no test
35+ // depends on another having run.
36+ let cachedScannedFileCount : number | undefined ;
37+ function liveScannedFileCount ( ) : number {
38+ cachedScannedFileCount ??= countScannedFiles (
39+ repoRoot ,
40+ loadGuardConfig ( ) . tsconfig ,
41+ ) ;
42+ return cachedScannedFileCount ;
43+ }
44+
3145// A probe dead export in one of the scoped files must fail the guard: the
3246// exact-name exemptions cover only the remaining deferred-cleanup flags
3347// (the usage-formatter flags were removed with their exports by the CL-6815
@@ -310,6 +324,17 @@ describe("pinned scan invocation", () => {
310324 expect ( ( ) => parseGuardConfig ( null ) ) . toThrow ( ) ;
311325 expect ( ( ) => parseGuardConfig ( [ ] ) ) . toThrow ( ) ;
312326 } ) ;
327+
328+ test ( "the scanned-files injection accepts no override or a positive integer" , ( ) => {
329+ expect ( parseInjectedScannedFiles ( undefined ) ) . toBeUndefined ( ) ;
330+ expect ( parseInjectedScannedFiles ( "1217" ) ) . toBe ( 1217 ) ;
331+ } ) ;
332+
333+ test ( "the scanned-files injection fails closed on a bogus value" , ( ) => {
334+ for ( const bad of [ "" , "0" , "-5" , "1.5" , "abc" ] ) {
335+ expect ( ( ) => parseInjectedScannedFiles ( bad ) ) . toThrow ( ) ;
336+ }
337+ } ) ;
313338} ) ;
314339
315340describe ( "scan coverage floor" , ( ) => {
@@ -321,14 +346,14 @@ describe("scan coverage floor", () => {
321346
322347 test ( "the live program file count clears the checked-in floor" , ( ) => {
323348 const config = loadGuardConfig ( ) ;
324- const scanned = countScannedFiles ( repoRoot , config . tsconfig ) ;
325- expect ( scanned ) . toBeGreaterThanOrEqual ( config . minScannedFiles ) ;
349+ expect ( liveScannedFileCount ( ) ) . toBeGreaterThanOrEqual (
350+ config . minScannedFiles ,
351+ ) ;
326352 } , 120_000 ) ;
327353
328354 test ( "the checked-in floor stays tight to the live count" , ( ) => {
329355 const config = loadGuardConfig ( ) ;
330- const scanned = countScannedFiles ( repoRoot , config . tsconfig ) ;
331- expect ( scanned ) . toBeLessThan ( config . minScannedFiles * 1.1 ) ;
356+ expect ( liveScannedFileCount ( ) ) . toBeLessThan ( config . minScannedFiles * 1.1 ) ;
332357 } , 120_000 ) ;
333358} ) ;
334359
@@ -337,24 +362,58 @@ describe("scan coverage floor", () => {
337362// scripts/ tree, the real guard runs as a subprocess, and the run must exit
338363// nonzero naming the probe. The probe lives only for the test (never
339364// committed) so the keep-alive check cannot itself become a dead export.
365+ //
366+ // The guard normally scans the full project via ts-prune (~5s); the probe's
367+ // temp tsconfig includes only the probe file, so this run pays only ts-prune
368+ // startup while still exercising the same wired-up gate (config load,
369+ // allowlist validation, node-spawned ts-prune, output parsing, coverage
370+ // floor, exit code).
340371describe ( "violation end to end" , ( ) => {
341372 test ( "a temp dead export fails the guard, which names it" , ( ) => {
342373 const probeFile = `dead-export-guard-probe-${ process . pid } .ts` ;
343374 const probeName = `deadExportGuardProbe${ process . pid } ` ;
344375 const probePath = join ( repoRoot , "scripts" , probeFile ) ;
376+ const narrowTsconfigFile = `dead-export-guard-tsconfig-${ process . pid } .json` ;
377+ const narrowTsconfigPath = join ( repoRoot , "scripts" , narrowTsconfigFile ) ;
345378 writeFileSync ( probePath , `export const ${ probeName } = 1;\n` ) ;
379+ writeFileSync (
380+ narrowTsconfigPath ,
381+ JSON . stringify ( {
382+ compilerOptions : {
383+ noEmit : true ,
384+ target : "ESNext" ,
385+ module : "ESNext" ,
386+ moduleResolution : "bundler" ,
387+ skipLibCheck : true ,
388+ } ,
389+ // Relative to the narrow tsconfig's own directory (scripts/).
390+ files : [ probeFile ] ,
391+ } ) ,
392+ ) ;
346393 try {
347394 const ran = spawnSync (
348395 process . execPath ,
349396 [ "scripts/check-dead-exports.ts" ] ,
350- { cwd : repoRoot , encoding : "utf8" } ,
397+ {
398+ cwd : repoRoot ,
399+ encoding : "utf8" ,
400+ // The file-count subprocess is exercised by the floor tests above;
401+ // inject the same deterministic value so this run pays only
402+ // ts-prune.
403+ env : {
404+ ...process . env ,
405+ DEAD_EXPORT_GUARD_SCANNED_FILES : String ( liveScannedFileCount ( ) ) ,
406+ DEAD_EXPORT_GUARD_TSCONFIG : `scripts/${ narrowTsconfigFile } ` ,
407+ } ,
408+ } ,
351409 ) ;
352410 expect ( ran . status ) . toBe ( 1 ) ;
353411 expect ( `${ ran . stdout } \n${ ran . stderr } ` ) . toContain (
354412 `${ probeFile } : ${ probeName } ` ,
355413 ) ;
356414 } finally {
357415 rmSync ( probePath , { force : true } ) ;
416+ rmSync ( narrowTsconfigPath , { force : true } ) ;
358417 }
359418 } , 120_000 ) ;
360419} ) ;
0 commit comments