@@ -16,13 +16,16 @@ import path from 'node:path';
1616
1717import {
1818 ARTIFACT_NAME ,
19+ BASE_REGISTRY ,
1920 SUMMARY_DIFF_CAP ,
2021 describeDeltas ,
2122 fenceFor ,
23+ generateBaseRegistry ,
2224 idDeltas ,
2325 renderProjectionDiff ,
2426 renderSummary ,
2527 type Generate ,
28+ type RunScript ,
2629} from './render-projection-diff' ;
2730
2831let tmp : string ;
@@ -149,6 +152,113 @@ describe('render-projection-diff — the generated diff on a pull request', () =
149152 } ) ;
150153} ) ;
151154
155+ describe ( 'render-projection-diff — a base whose migration registry is git-ignored' , ( ) => {
156+ /** A base `packages/spec` laid out as its archive would be: the generator and/or the registry, or neither. */
157+ function baseSpecDir ( has : { generator : boolean ; registry : boolean } ) : string {
158+ const dir = path . join ( tmp , 'base-tree' , 'packages' , 'spec' ) ;
159+ fs . mkdirSync ( path . join ( dir , 'scripts' ) , { recursive : true } ) ;
160+ if ( has . generator ) fs . writeFileSync ( path . join ( dir , 'scripts' , BASE_REGISTRY . generator ) , '' ) ;
161+ if ( has . registry ) {
162+ fs . mkdirSync ( path . dirname ( path . join ( dir , BASE_REGISTRY . file ) ) , { recursive : true } ) ;
163+ fs . writeFileSync ( path . join ( dir , BASE_REGISTRY . file ) , '// committed\n' ) ;
164+ }
165+ return dir ;
166+ }
167+
168+ /** A fake runner that records each call and, unless told otherwise, writes the registry the way the generator does. */
169+ function recorder ( result : { status : number ; write : boolean ; output ?: string } ) {
170+ const calls : [ string , string ] [ ] = [ ] ;
171+ const run : RunScript = ( dir , script ) => {
172+ calls . push ( [ dir , script ] ) ;
173+ if ( result . write ) fs . writeFileSync ( path . join ( dir , BASE_REGISTRY . file ) , '// generated\n' ) ;
174+ return { status : result . status , output : result . output ?? '' } ;
175+ } ;
176+ return { calls, run } ;
177+ }
178+
179+ it ( 'an archive with the generator but no registry runs the BASE generator in the base tree' , ( ) => {
180+ const dir = baseSpecDir ( { generator : true , registry : false } ) ;
181+ fs . mkdirSync ( path . dirname ( path . join ( dir , BASE_REGISTRY . file ) ) , { recursive : true } ) ;
182+ const { calls, run } = recorder ( { status : 0 , write : true } ) ;
183+
184+ expect ( generateBaseRegistry ( dir , run ) ) . toBe ( true ) ;
185+ expect ( calls ) . toEqual ( [ [ dir , 'build-migration-registry.ts' ] ] ) ;
186+ expect ( fs . readFileSync ( path . join ( dir , BASE_REGISTRY . file ) , 'utf8' ) ) . toBe ( '// generated\n' ) ;
187+ } ) ;
188+
189+ it ( 'a base that committed its registry keeps it, and a base without the generator is left as it is' , ( ) => {
190+ for ( const has of [ { generator : true , registry : true } , { generator : false , registry : true } , { generator : false , registry : false } ] ) {
191+ fs . rmSync ( path . join ( tmp , 'base-tree' ) , { recursive : true , force : true } ) ;
192+ const dir = baseSpecDir ( has ) ;
193+ const { calls, run } = recorder ( { status : 0 , write : true } ) ;
194+
195+ expect ( generateBaseRegistry ( dir , run ) ) . toBe ( false ) ;
196+ expect ( calls ) . toEqual ( [ ] ) ;
197+ if ( has . registry ) expect ( fs . readFileSync ( path . join ( dir , BASE_REGISTRY . file ) , 'utf8' ) ) . toBe ( '// committed\n' ) ;
198+ }
199+ } ) ;
200+
201+ it ( 'a base registry generator that fails, or exits 0 without writing, is thrown with its output' , ( ) => {
202+ const dir = baseSpecDir ( { generator : true , registry : false } ) ;
203+
204+ // First line names the generator and its exit; the generator's output follows it — the
205+ // shape the failure summary splits on.
206+ expect ( ( ) => generateBaseRegistry ( dir , recorder ( { status : 1 , write : false , output : 'entry failed to parse' } ) . run ) ) . toThrow (
207+ / ^ b u i l d - m i g r a t i o n - r e g i s t r y \. t s e x i t e d 1 \b [ ^ \n ] * \n e n t r y f a i l e d t o p a r s e $ / ,
208+ ) ;
209+ expect ( ( ) => generateBaseRegistry ( dir , recorder ( { status : 0 , write : false } ) . run ) ) . toThrow (
210+ / ^ b u i l d - m i g r a t i o n - r e g i s t r y \. t s e x i t e d 0 \b [ ^ \n ] * s r c \/ m i g r a t i o n s \/ r e g i s t r y \. t s / ,
211+ ) ;
212+ } ) ;
213+
214+ it ( 'a base whose registry cannot be generated is RED, named as the base' , ( ) => {
215+ const dir = baseSpecDir ( { generator : true , registry : false } ) ;
216+ const generate : Generate = ( _dir , script , out ) => {
217+ fs . writeFileSync ( out , script === 'build-spec-changes.ts' ? BASE . json : BASE . guide ) ;
218+ return { status : 0 , output : '' } ;
219+ } ;
220+ const head = path . join ( tmp , 'head-tree' , 'packages' , 'spec' ) ;
221+ fs . mkdirSync ( head , { recursive : true } ) ;
222+ const outDir = path . join ( tmp , 'out' ) ;
223+ const result = renderProjectionDiff ( {
224+ repoRoot : tmp ,
225+ baseSha : 'b' . repeat ( 40 ) ,
226+ baseLabel : 'HEAD^1 = bbbbbbbbbb' ,
227+ outDir,
228+ headSpecDir : head ,
229+ generate,
230+ materializeBase : ( ) => {
231+ generateBaseRegistry ( dir , recorder ( { status : 1 , write : false , output : 'entry failed to parse' } ) . run ) ;
232+ return dir ;
233+ } ,
234+ } ) ;
235+
236+ expect ( result . code ) . toBe ( 1 ) ;
237+ expect ( result . headline ) . toContain ( 'base HEAD^1 = bbbbbbbbbb: build-migration-registry.ts exited 1' ) ;
238+ expect ( result . summary ) . toContain ( 'entry failed to parse' ) ;
239+ } ) ;
240+
241+ it ( 'the default runner executes the base generator by its own path, with the base package as cwd' , ( ) => {
242+ const dir = baseSpecDir ( { generator : true , registry : false } ) ;
243+ // Resolves its package root from its own path, as the real generator does.
244+ fs . writeFileSync (
245+ path . join ( dir , 'scripts' , BASE_REGISTRY . generator ) ,
246+ [
247+ "import { mkdirSync, writeFileSync } from 'node:fs';" ,
248+ "import { dirname, join } from 'node:path';" ,
249+ "import { fileURLToPath } from 'node:url';" ,
250+ "const pkgRoot = join(dirname(fileURLToPath(import.meta.url)), '..');" ,
251+ "mkdirSync(join(pkgRoot, 'src', 'migrations'), { recursive: true });" ,
252+ "writeFileSync(join(pkgRoot, 'src', 'migrations', 'registry.ts'), `// cwd ${process.cwd()}\\n`);" ,
253+ '' ,
254+ ] . join ( '\n' ) ,
255+ ) ;
256+
257+ expect ( generateBaseRegistry ( dir ) ) . toBe ( true ) ;
258+ expect ( fs . readFileSync ( path . join ( dir , BASE_REGISTRY . file ) , 'utf8' ) ) . toBe ( `// cwd ${ fs . realpathSync ( dir ) } \n` ) ;
259+ } ) ;
260+ } ) ;
261+
152262describe ( 'render-projection-diff — the pieces' , ( ) => {
153263 it ( 'idDeltas names ids gained and lost per record, and skips records that did not move' , ( ) => {
154264 const deltas = idDeltas ( manifest ( [ 'a' , 'b' ] , [ 'c1' ] ) , manifest ( [ 'b' , 'z' ] , [ 'c1' , 'c2' ] ) ) ;
0 commit comments