11import { spawnSync } from 'node:child_process'
2- import { mkdirSync , mkdtempSync , readFileSync , rmSync , writeFileSync } from 'node:fs'
2+ import {
3+ existsSync ,
4+ mkdirSync ,
5+ mkdtempSync ,
6+ readdirSync ,
7+ readFileSync ,
8+ rmSync ,
9+ symlinkSync ,
10+ writeFileSync ,
11+ } from 'node:fs'
312import { tmpdir } from 'node:os'
413import path from 'node:path'
514import { afterEach , expect , test } from 'vitest'
@@ -58,6 +67,8 @@ afterEach(() => {
5867
5968test ( 'CLI discovers new APIs, narrows recipe variants to public props and is byte deterministic' , ( ) => {
6069 const root = fixture ( )
70+ expect ( run ( root , '--check' ) . status ) . toBe ( 1 )
71+ expect ( existsSync ( path . join ( root , 'scripts' ) ) ) . toBe ( false )
6172 const first = run ( root )
6273 expect ( first . status , first . stderr ) . toBe ( 0 )
6374 const bytes = readFileSync ( path . join ( root , artifact ) , 'utf8' )
@@ -73,6 +84,112 @@ test('CLI discovers new APIs, narrows recipe variants to public props and is byt
7384 expect ( readFileSync ( path . join ( root , artifact ) , 'utf8' ) ) . toBe ( '{}' )
7485} , 60_000 )
7586
87+ test . each ( [ 'file' , 'dangling' , 'scripts' , 'design-conformance' ] ) (
88+ 'generation rejects a linked %s output boundary without modifying external files' ,
89+ ( kind ) => {
90+ const root = fixture ( )
91+ const external = mkdtempSync ( path . join ( tmpdir ( ) , 'design-output-target-' ) )
92+ roots . push ( external )
93+ const sentinel = path . join ( external , 'sentinel' )
94+ writeFileSync ( sentinel , 'external bytes must survive' )
95+ if ( kind === 'file' || kind === 'dangling' ) {
96+ mkdirSync ( path . dirname ( path . join ( root , artifact ) ) , { recursive : true } )
97+ symlinkSync (
98+ kind === 'file' ? sentinel : path . join ( external , 'missing' ) ,
99+ path . join ( root , artifact )
100+ )
101+ } else {
102+ const parent = kind === 'scripts' ? 'scripts' : path . dirname ( artifact )
103+ mkdirSync ( path . dirname ( path . join ( root , parent ) ) , { recursive : true } )
104+ symlinkSync ( external , path . join ( root , parent ) , 'dir' )
105+ }
106+ const child = run ( root )
107+ expect ( readFileSync ( sentinel , 'utf8' ) ) . toBe ( 'external bytes must survive' )
108+ expect ( readdirSync ( external ) ) . toEqual ( [ 'sentinel' ] )
109+ expect ( child . status , child . stderr ) . toBe ( 2 )
110+ expect ( run ( root , '--check' ) . status ) . toBe ( 2 )
111+ expect ( readdirSync ( external ) ) . toEqual ( [ 'sentinel' ] )
112+ } ,
113+ 60_000
114+ )
115+
116+ test . each ( [ 'symlink' , 'fifo' , 'nonregular' , 'invalid-descriptor' ] ) (
117+ 'generation refuses an output changed to %s at descriptor opening' ,
118+ ( kind ) => {
119+ const root = fixture ( )
120+ const external = mkdtempSync ( path . join ( tmpdir ( ) , 'design-output-race-' ) )
121+ roots . push ( external )
122+ const sentinel = path . join ( external , 'sentinel' )
123+ writeFileSync ( sentinel , 'external bytes must survive' )
124+ write ( root , artifact , 'existing output must survive validation failure' )
125+ const output = path . join ( root , artifact )
126+ const preload = path . join ( external , 'replace-output.cjs' )
127+ const attack =
128+ kind === 'symlink'
129+ ? `fs.unlinkSync(output);fs.symlinkSync(${ JSON . stringify ( sentinel ) } ,output);`
130+ : kind === 'invalid-descriptor'
131+ ? 'const descriptor=open(file,flags,mode);fs.closeSync(descriptor);return descriptor;'
132+ : `fs.unlinkSync(output);require('node:child_process').execFileSync('mkfifo',[output]);${ kind === 'nonregular' ? 'open(output,fs.constants.O_RDWR|fs.constants.O_NONBLOCK);' : '' } `
133+ writeFileSync (
134+ preload ,
135+ `
136+ const fs=require('node:fs');const requested=${ JSON . stringify ( output ) } ;const output=fs.realpathSync(requested);const open=fs.openSync;const write=fs.writeFileSync;
137+ fs.openSync=(file,flags,mode)=>{if((file===output||file===requested)&&(flags&fs.constants.O_WRONLY)){process.stderr.write('Output replacement applied\\n');${ attack } }return open(file,flags,mode)};
138+ fs.writeFileSync=(file,...args)=>{if(file===output||file===requested){const descriptor=fs.openSync(file,fs.constants.O_WRONLY|fs.constants.O_CREAT|fs.constants.O_TRUNC,0o644);try{return write(descriptor,...args)}finally{fs.closeSync(descriptor)}}return write(file,...args)};
139+ `
140+ )
141+ const child = spawnSync (
142+ bun ,
143+ [ '--no-env-file' , '--preload' , preload , cli , '--repo' , root , '--ref' , 'HEAD' ] ,
144+ {
145+ encoding : 'utf8' ,
146+ timeout : 5000 ,
147+ }
148+ )
149+ expect ( child . stderr ) . toContain ( 'Output replacement applied' )
150+ expect ( child . status , child . stderr ) . toBe ( 2 )
151+ expect ( readFileSync ( sentinel , 'utf8' ) ) . toBe ( 'external bytes must survive' )
152+ if ( kind === 'invalid-descriptor' )
153+ expect ( readFileSync ( output , 'utf8' ) ) . toBe ( 'existing output must survive validation failure' )
154+ if ( kind === 'nonregular' ) expect ( child . stderr ) . toContain ( 'not a regular file' )
155+ } ,
156+ 15_000
157+ )
158+
159+ test ( 'generation rejects a directory output and preserves immutable checks through a repository alias' , ( ) => {
160+ const root = fixture ( )
161+ mkdirSync ( path . join ( root , artifact ) , { recursive : true } )
162+ write ( root , `${ artifact } /sentinel` , 'directory bytes must survive' )
163+ expect ( run ( root , '--ref' , 'HEAD' ) . status ) . toBe ( 2 )
164+ expect ( readFileSync ( path . join ( root , artifact , 'sentinel' ) , 'utf8' ) ) . toBe (
165+ 'directory bytes must survive'
166+ )
167+ rmSync ( path . join ( root , artifact ) , { recursive : true } )
168+ expect ( run ( root ) . status ) . toBe ( 0 )
169+ git ( root , 'add' , '.' )
170+ git (
171+ root ,
172+ '-c' ,
173+ 'user.name=Test' ,
174+ '-c' ,
175+ 'user.email=test@example.com' ,
176+ 'commit' ,
177+ '-qm' ,
178+ 'generated'
179+ )
180+ const external = mkdtempSync ( path . join ( tmpdir ( ) , 'design-output-alias-' ) )
181+ roots . push ( external )
182+ const alias = path . join ( external , 'repo' )
183+ symlinkSync ( root , alias , 'dir' )
184+ expect ( run ( alias ) . status ) . toBe ( 0 )
185+ const sentinel = path . join ( external , 'sentinel' )
186+ writeFileSync ( sentinel , 'external bytes must survive' )
187+ rmSync ( path . join ( root , artifact ) )
188+ symlinkSync ( sentinel , path . join ( root , artifact ) )
189+ expect ( run ( alias , '--check' , '--ref' , 'HEAD' ) . status ) . toBe ( 0 )
190+ expect ( readFileSync ( sentinel , 'utf8' ) ) . toBe ( 'external bytes must survive' )
191+ } , 60_000 )
192+
76193test ( 'nested finite style lookups own only the selected slot chrome' , ( ) => {
77194 const root = fixture ( )
78195 write (
0 commit comments