1+ import { spawnSync } from "node:child_process" ;
2+ import {
3+ existsSync ,
4+ readdirSync ,
5+ readFileSync ,
6+ rmSync ,
7+ writeFileSync ,
8+ } from "node:fs" ;
9+ import { join , relative } from "node:path" ;
110import { describe , expect , test } from "bun:test" ;
211
312import {
@@ -8,6 +17,8 @@ import {
817 parseTsPruneLine ,
918} from "./check-dead-exports.js" ;
1019
20+ const repoRoot = join ( import . meta. dir , ".." ) ;
21+
1122// A probe dead export in one of the scoped files must fail the guard: the
1223// exact-name exemptions cover only the five deferred-cleanup flags, never
1324// the whole module.
@@ -116,3 +127,88 @@ describe("stale allowlist entries", () => {
116127 expect ( outcome . unused ) . toEqual ( [ ] ) ;
117128 } ) ;
118129} ) ;
130+
131+ // The unit tests above prove the rule engine flags a probe; this one proves
132+ // the wired-up gate does. A temp probe export lands in the tsconfig-covered
133+ // scripts/ tree, the real guard runs as a subprocess, and the run must exit
134+ // nonzero naming the probe. The probe lives only for the test (never
135+ // committed) so the keep-alive check cannot itself become a dead export.
136+ describe ( "violation end to end" , ( ) => {
137+ test ( "a temp dead export fails the guard, which names it" , ( ) => {
138+ const probeFile = `dead-export-guard-probe-${ process . pid } .ts` ;
139+ const probeName = `deadExportGuardProbe${ process . pid } ` ;
140+ const probePath = join ( repoRoot , "scripts" , probeFile ) ;
141+ writeFileSync ( probePath , `export const ${ probeName } = 1;\n` ) ;
142+ try {
143+ const ran = spawnSync (
144+ process . execPath ,
145+ [ "scripts/check-dead-exports.ts" ] ,
146+ { cwd : repoRoot , encoding : "utf8" } ,
147+ ) ;
148+ expect ( ran . status ) . toBe ( 1 ) ;
149+ expect ( `${ ran . stdout } \n${ ran . stderr } ` ) . toContain (
150+ `${ probeFile } : ${ probeName } ` ,
151+ ) ;
152+ } finally {
153+ rmSync ( probePath , { force : true } ) ;
154+ }
155+ } , 60_000 ) ;
156+ } ) ;
157+
158+ // The purge deleted four fully-dead barrel files; a re-created barrel (or a
159+ // new import of its path) would silently resurrect the surface the guard was
160+ // built to shrink. Pin the paths, not the file text.
161+ describe ( "deleted barrels stay deleted" , ( ) => {
162+ const barrels = [
163+ "src/agent/directors/index.ts" ,
164+ "src/auth/codex/index.ts" ,
165+ "src/auth/xai/index.ts" ,
166+ "src/web/index.ts" ,
167+ ] ;
168+ const barrelDirs = barrels . map ( ( barrel ) =>
169+ barrel . slice ( 0 , - "/index.ts" . length ) ,
170+ ) ;
171+ const barrelTails = [
172+ "agent/directors/index" ,
173+ "auth/codex/index" ,
174+ "auth/xai/index" ,
175+ "web/index" ,
176+ ] ;
177+ const roots = [ "src" , "tests" , "evals" , "scripts" , "packages" ] ;
178+
179+ test ( "the barrel files do not exist" , ( ) => {
180+ for ( const barrel of barrels ) {
181+ expect ( existsSync ( join ( repoRoot , barrel ) ) ) . toBe ( false ) ;
182+ }
183+ } ) ;
184+
185+ test ( "no source file imports the deleted barrel paths" , ( ) => {
186+ const offenders : string [ ] = [ ] ;
187+ const walk = ( dir : string ) : void => {
188+ for ( const entry of readdirSync ( dir , { withFileTypes : true } ) ) {
189+ const absolute = join ( dir , entry . name ) ;
190+ if ( entry . isDirectory ( ) ) {
191+ walk ( absolute ) ;
192+ continue ;
193+ }
194+ if ( ! entry . name . endsWith ( ".ts" ) ) continue ;
195+ const rel = relative ( repoRoot , absolute ) . split ( "/" ) . join ( "/" ) ;
196+ const dirRel = rel . slice ( 0 , - entry . name . length - 1 ) ;
197+ const text = readFileSync ( absolute , "utf8" ) ;
198+ const specifiers = [
199+ ...text . matchAll ( / (?: f r o m \s * [ " ' ] | i m p o r t \s * \( \s * [ " ' ] ) ( [ ^ " ' ] + ) [ " ' ] / g) ,
200+ ] . map ( ( match ) => ( match [ 1 ] ?? "" ) . replace ( / \. (?: j s | t s ) $ / , "" ) ) ;
201+ for ( const spec of specifiers ) {
202+ const hitsBarrel =
203+ barrelTails . some (
204+ ( tail ) => spec === tail || spec . endsWith ( `/${ tail } ` ) ,
205+ ) ||
206+ ( spec === "./index" && barrelDirs . includes ( dirRel ) ) ;
207+ if ( hitsBarrel ) offenders . push ( `${ rel } : ${ spec } ` ) ;
208+ }
209+ }
210+ } ;
211+ for ( const root of roots ) walk ( join ( repoRoot , root ) ) ;
212+ expect ( offenders ) . toEqual ( [ ] ) ;
213+ } ) ;
214+ } ) ;
0 commit comments