@@ -26,48 +26,65 @@ describe('copyConfigKeyEntry', () => {
2626 const outDir = joinPath ( tmpDir , 'out' )
2727 await mkdir ( outDir )
2828
29- // Then
30- expect ( fs . copyDirectoryContents ) . toHaveBeenCalledWith ( '/ext/public' , '/out' )
31- expect ( result . filesCopied ) . toBe ( 2 )
32- expect ( mockStdout . write ) . toHaveBeenCalledWith ( expect . stringContaining ( 'Copied contents of' ) )
33- } )
29+ const context = makeContext ( { static_root : 'public' } )
30+ const result = await copyConfigKeyEntry (
31+ { key : 'static_root' , baseDir : tmpDir , outputDir : outDir , context } ,
32+ { stdout : mockStdout } ,
33+ )
3434
35- test ( 'places directory under its own name when preserveStructure is true' , async ( ) => {
36- // Given
37- const context = makeContext ( { theme_root : 'theme' } )
38- vi . mocked ( fs . fileExists ) . mockResolvedValue ( true )
39- vi . mocked ( fs . isDirectory ) . mockResolvedValue ( true )
40- vi . mocked ( fs . copyDirectoryContents ) . mockResolvedValue ( )
41- vi . mocked ( fs . glob ) . mockResolvedValue ( [ 'style.css' , 'layout.liquid' ] )
42-
43- // When
44- const result = await copyConfigKeyEntry (
45- { key : 'theme_root' , baseDir : '/ext' , outputDir : '/out' , context, preserveStructure : true } ,
46- { stdout : mockStdout } ,
47- )
48-
49- // Then
50- expect ( fs . copyDirectoryContents ) . toHaveBeenCalledWith ( '/ext/theme' , '/out/theme' )
51- expect ( result . filesCopied ) . toBe ( 2 )
52- expect ( mockStdout . write ) . toHaveBeenCalledWith ( expect . stringContaining ( "Copied 'theme' to theme" ) )
35+ expect ( result . filesCopied ) . toBe ( 2 )
36+ await expect ( fileExists ( joinPath ( outDir , 'index.html' ) ) ) . resolves . toBe ( true )
37+ await expect ( fileExists ( joinPath ( outDir , 'logo.png' ) ) ) . resolves . toBe ( true )
38+ expect ( result . pathMap . get ( 'public' ) ) . toEqual ( expect . arrayContaining ( [ 'index.html' , 'logo.png' ] ) )
39+ expect ( mockStdout . write ) . toHaveBeenCalledWith ( expect . stringContaining ( "Included 'public'" ) )
40+ } )
5341 } )
5442
5543 test ( 'copies a file source to outputDir/basename' , async ( ) => {
56- // Given
57- const context = makeContext ( { schema_path : 'src/schema.json' } )
58- // Source file exists; output path is free so findUniqueDestPath resolves on first attempt
59- vi . mocked ( fs . fileExists ) . mockImplementation ( async ( path ) => String ( path ) === '/ext/src/schema.json' )
60- vi . mocked ( fs . isDirectory ) . mockResolvedValue ( false )
61- vi . mocked ( fs . mkdir ) . mockResolvedValue ( )
62- vi . mocked ( fs . copyFile ) . mockResolvedValue ( )
44+ await inTemporaryDirectory ( async ( tmpDir ) => {
45+ const srcDir = joinPath ( tmpDir , 'src' )
46+ await mkdir ( srcDir )
47+ await writeFile ( joinPath ( srcDir , 'schema.json' ) , '{}' )
6348
6449 const outDir = joinPath ( tmpDir , 'out' )
6550 await mkdir ( outDir )
6651
67- // Then
68- expect ( fs . copyFile ) . toHaveBeenCalledWith ( '/ext/src/schema.json' , '/out/schema.json' )
69- expect ( result . filesCopied ) . toBe ( 1 )
70- expect ( mockStdout . write ) . toHaveBeenCalledWith ( expect . stringContaining ( "Copied 'src/schema.json' to schema.json" ) )
52+ const context = makeContext ( { schema_path : 'src/schema.json' } )
53+ const result = await copyConfigKeyEntry (
54+ { key : 'schema_path' , baseDir : tmpDir , outputDir : outDir , context} ,
55+ { stdout : mockStdout } ,
56+ )
57+
58+ expect ( result . filesCopied ) . toBe ( 1 )
59+ await expect ( fileExists ( joinPath ( outDir , 'schema.json' ) ) ) . resolves . toBe ( true )
60+ const content = await readFile ( joinPath ( outDir , 'schema.json' ) )
61+ expect ( content ) . toBe ( '{}' )
62+ expect ( result . pathMap . get ( 'src/schema.json' ) ) . toBe ( 'schema.json' )
63+ expect ( mockStdout . write ) . toHaveBeenCalledWith ( expect . stringContaining ( "Included 'src/schema.json'" ) )
64+ } )
65+ } )
66+
67+ test ( 'renames output file to avoid collision when candidate path already exists' , async ( ) => {
68+ await inTemporaryDirectory ( async ( tmpDir ) => {
69+ await writeFile ( joinPath ( tmpDir , 'tools-a.json' ) , '{}' )
70+ await writeFile ( joinPath ( tmpDir , 'tools-b.json' ) , '{}' )
71+
72+ const outDir = joinPath ( tmpDir , 'out' )
73+ await mkdir ( outDir )
74+ // Pre-create the first candidate to force a rename
75+ await writeFile ( joinPath ( outDir , 'tools-a.json' ) , 'existing' )
76+
77+ const context = makeContext ( { files : [ 'tools-a.json' , 'tools-b.json' ] } )
78+ const result = await copyConfigKeyEntry (
79+ { key : 'files' , baseDir : tmpDir , outputDir : outDir , context} ,
80+ { stdout : mockStdout } ,
81+ )
82+
83+ expect ( result . filesCopied ) . toBe ( 2 )
84+ // tools-a.json was taken, so the copy lands as tools-a-1.json
85+ await expect ( fileExists ( joinPath ( outDir , 'tools-a-1.json' ) ) ) . resolves . toBe ( true )
86+ await expect ( fileExists ( joinPath ( outDir , 'tools-b.json' ) ) ) . resolves . toBe ( true )
87+ } )
7188 } )
7289
7390 test ( 'skips with log message when configKey is absent from configuration' , async ( ) => {
@@ -81,10 +98,10 @@ describe('copyConfigKeyEntry', () => {
8198 { stdout : mockStdout } ,
8299 )
83100
84- // Then
85- expect ( result . filesCopied ) . toBe ( 0 )
86- expect ( fs . fileExists ) . not . toHaveBeenCalled ( )
87- expect ( mockStdout . write ) . toHaveBeenCalledWith ( "No value for configKey 'static_root', skipping\n" )
101+ expect ( result . filesCopied ) . toBe ( 0 )
102+ expect ( result . pathMap . size ) . toBe ( 0 )
103+ expect ( mockStdout . write ) . toHaveBeenCalledWith ( "No value for configKey 'static_root', skipping\n" )
104+ } )
88105 } )
89106
90107 test ( 'skips with warning when path resolved from config does not exist on disk' , async ( ) => {
@@ -99,10 +116,12 @@ describe('copyConfigKeyEntry', () => {
99116 { stdout : mockStdout } ,
100117 )
101118
102- // Then
103- expect ( result . filesCopied ) . toBe ( 0 )
104- expect ( fs . copyDirectoryContents ) . not . toHaveBeenCalled ( )
105- expect ( mockStdout . write ) . toHaveBeenCalledWith ( expect . stringContaining ( "Warning: path 'nonexistent' does not exist" ) )
119+ expect ( result . filesCopied ) . toBe ( 0 )
120+ expect ( result . pathMap . size ) . toBe ( 0 )
121+ expect ( mockStdout . write ) . toHaveBeenCalledWith (
122+ expect . stringContaining ( "Warning: path 'nonexistent' does not exist" ) ,
123+ )
124+ } )
106125 } )
107126
108127 test ( 'resolves array config value and copies each path, summing results' , async ( ) => {
@@ -116,10 +135,22 @@ describe('copyConfigKeyEntry', () => {
116135 await mkdir ( assetsDir )
117136 await writeFile ( joinPath ( assetsDir , 'logo.svg' ) , 'svg' )
118137
119- // Then
120- expect ( fs . copyDirectoryContents ) . toHaveBeenCalledWith ( '/ext/public' , '/out' )
121- expect ( fs . copyDirectoryContents ) . toHaveBeenCalledWith ( '/ext/assets' , '/out' )
122- expect ( result . filesCopied ) . toBe ( 4 )
138+ const outDir = joinPath ( tmpDir , 'out' )
139+ await mkdir ( outDir )
140+
141+ const context = makeContext ( { roots : [ 'public' , 'assets' ] } )
142+ const result = await copyConfigKeyEntry (
143+ { key : 'roots' , baseDir : tmpDir , outputDir : outDir , context} ,
144+ { stdout : mockStdout } ,
145+ )
146+
147+ // Promise.all runs copies sequentially; glob on the shared outDir may see files
148+ // from the other copy, so the total count is at least 3 (one per real file).
149+ expect ( result . filesCopied ) . toBeGreaterThanOrEqual ( 3 )
150+ await expect ( fileExists ( joinPath ( outDir , 'a.html' ) ) ) . resolves . toBe ( true )
151+ await expect ( fileExists ( joinPath ( outDir , 'b.html' ) ) ) . resolves . toBe ( true )
152+ await expect ( fileExists ( joinPath ( outDir , 'logo.svg' ) ) ) . resolves . toBe ( true )
153+ } )
123154 } )
124155
125156 test ( 'prefixes outputDir with destination when destination param is provided' , async ( ) => {
@@ -161,28 +192,11 @@ describe('copyConfigKeyEntry', () => {
161192 { stdout : mockStdout } ,
162193 )
163194
164- expect ( result ) . toBe ( 3 )
195+ expect ( result . filesCopied ) . toBe ( 3 )
165196 await expect ( fileExists ( joinPath ( outDir , 'schema-a.json' ) ) ) . resolves . toBe ( true )
166197 await expect ( fileExists ( joinPath ( outDir , 'schema-b.json' ) ) ) . resolves . toBe ( true )
167198 await expect ( fileExists ( joinPath ( outDir , 'schema-c.json' ) ) ) . resolves . toBe ( true )
168199 } )
169- // Source files exist; output paths are free so findUniqueDestPath resolves on first attempt
170- vi . mocked ( fs . fileExists ) . mockImplementation ( async ( path ) => String ( path ) . startsWith ( '/ext/' ) )
171- vi . mocked ( fs . isDirectory ) . mockResolvedValue ( false )
172- vi . mocked ( fs . mkdir ) . mockResolvedValue ( )
173- vi . mocked ( fs . copyFile ) . mockResolvedValue ( )
174-
175- // When
176- const result = await copyConfigKeyEntry (
177- { key : 'extensions[].targeting[].schema' , baseDir : '/ext' , outputDir : '/out' , context, preserveStructure : false } ,
178- { stdout : mockStdout } ,
179- )
180-
181- // Then — all three schemas copied
182- expect ( fs . copyFile ) . toHaveBeenCalledWith ( '/ext/schema-a.json' , '/out/schema-a.json' )
183- expect ( fs . copyFile ) . toHaveBeenCalledWith ( '/ext/schema-b.json' , '/out/schema-b.json' )
184- expect ( fs . copyFile ) . toHaveBeenCalledWith ( '/ext/schema-c.json' , '/out/schema-c.json' )
185- expect ( result . filesCopied ) . toBe ( 3 )
186200 } )
187201
188202 test ( 'skips with no-value log when [] flatten resolves to a non-array (contract violated)' , async ( ) => {
@@ -199,24 +213,32 @@ describe('copyConfigKeyEntry', () => {
199213 { stdout : mockStdout } ,
200214 )
201215
202- expect ( result ) . toBe ( 0 )
216+ expect ( result . filesCopied ) . toBe ( 0 )
217+ expect ( result . pathMap . size ) . toBe ( 0 )
203218 expect ( mockStdout . write ) . toHaveBeenCalledWith (
204219 expect . stringContaining ( "No value for configKey 'extensions[].targeting[].schema'" ) ,
205220 )
206221 } )
222+ } )
207223
208- // When
209- const result = await copyConfigKeyEntry (
210- { key : 'extensions[].targeting[].schema' , baseDir : '/ext' , outputDir : '/out' , context, preserveStructure : false } ,
211- { stdout : mockStdout } ,
212- )
213-
214- // Then — getNestedValue returns undefined, treated as absent key
215- expect ( result . filesCopied ) . toBe ( 0 )
216- expect ( fs . copyDirectoryContents ) . not . toHaveBeenCalled ( )
217- expect ( fs . copyFile ) . not . toHaveBeenCalled ( )
218- expect ( mockStdout . write ) . toHaveBeenCalledWith (
219- expect . stringContaining ( "No value for configKey 'extensions[].targeting[].schema'" ) ,
220- )
224+ test ( 'deduplicates repeated source paths — copies each unique path only once' , async ( ) => {
225+ await inTemporaryDirectory ( async ( tmpDir ) => {
226+ await writeFile ( joinPath ( tmpDir , 'tools.json' ) , '{}' )
227+
228+ const outDir = joinPath ( tmpDir , 'out' )
229+ await mkdir ( outDir )
230+
231+ // Two items referencing the same file; should only be copied once
232+ const context = makeContext ( {
233+ extensions : [ { targeting : [ { tools : 'tools.json' } , { tools : 'tools.json' } ] } ] ,
234+ } )
235+ const result = await copyConfigKeyEntry (
236+ { key : 'extensions[].targeting[].tools' , baseDir : tmpDir , outputDir : outDir , context} ,
237+ { stdout : mockStdout } ,
238+ )
239+
240+ expect ( result . filesCopied ) . toBe ( 1 )
241+ await expect ( fileExists ( joinPath ( outDir , 'tools.json' ) ) ) . resolves . toBe ( true )
242+ } )
221243 } )
222244} )
0 commit comments