1- import { cloneRepoAndCheckoutLatestTag , cloneRepo , createAIInstructions } from './init.js'
1+ import { cloneRepoAndCheckoutLatestTag , cloneRepo , createAIInstructions , createAIInstructionFiles } from './init.js'
22import { describe , expect , vi , test , beforeEach } from 'vitest'
33import { downloadGitRepository , removeGitRemote } from '@shopify/cli-kit/node/git'
4- import { rmdir , fileExists , copyDirectoryContents } from '@shopify/cli-kit/node/fs'
4+ import { rmdir , fileExists , readFile , writeFile , symlink } from '@shopify/cli-kit/node/fs'
55import { joinPath } from '@shopify/cli-kit/node/path'
66
77vi . mock ( '@shopify/cli-kit/node/git' )
@@ -11,7 +11,9 @@ vi.mock('@shopify/cli-kit/node/fs', async () => {
1111 ...actual ,
1212 fileExists : vi . fn ( ) ,
1313 rmdir : vi . fn ( ) ,
14- copyDirectoryContents : vi . fn ( ) ,
14+ readFile : vi . fn ( ) ,
15+ writeFile : vi . fn ( ) ,
16+ symlink : vi . fn ( ) ,
1517 inTemporaryDirectory : vi . fn ( async ( callback ) => {
1618 // eslint-disable-next-line node/no-callback-literal
1719 return callback ( '/tmp' )
@@ -151,26 +153,97 @@ describe('createAIInstructions()', () => {
151153
152154 beforeEach ( ( ) => {
153155 vi . mocked ( joinPath ) . mockImplementation ( ( ...paths ) => paths . join ( '/' ) )
156+ vi . mocked ( readFile ) . mockResolvedValue ( 'Sample AI instructions content' as any )
157+ vi . mocked ( writeFile ) . mockResolvedValue ( )
158+ vi . mocked ( symlink ) . mockResolvedValue ( )
154159 } )
155160
156- test ( 'creates AI instructions if it exists ' , async ( ) => {
161+ test ( 'creates AI instructions for a single instruction type ' , async ( ) => {
157162 // Given
158163 vi . mocked ( downloadGitRepository ) . mockResolvedValue ( )
159- vi . mocked ( copyDirectoryContents ) . mockResolvedValue ( )
160164
161165 // When
162166 await createAIInstructions ( destination , 'cursor' )
163167
164168 // Then
165169 expect ( downloadGitRepository ) . toHaveBeenCalled ( )
166- expect ( copyDirectoryContents ) . toHaveBeenCalledWith ( '/tmp/ai/cursor' , '/path/to/theme/.cursor' )
170+ expect ( readFile ) . toHaveBeenCalledWith ( '/tmp/ai/github/copilot-instructions.md' )
171+ expect ( writeFile ) . toHaveBeenCalledWith ( '/path/to/theme/AGENTS.md' , expect . stringContaining ( '# AGENTS.md' ) )
172+ expect ( symlink ) . not . toHaveBeenCalled ( )
167173 } )
168174
169- test ( 'throws an error when the AI instructions directory does not exist ' , async ( ) => {
175+ test ( 'creates AI instructions for all instruction types when "all" is selected ' , async ( ) => {
170176 // Given
171177 vi . mocked ( downloadGitRepository ) . mockResolvedValue ( )
172- vi . mocked ( copyDirectoryContents ) . mockRejectedValue ( new Error ( 'Directory does not exist' ) )
178+
179+ // When
180+ await createAIInstructions ( destination , 'all' )
181+
182+ // Then
183+ expect ( downloadGitRepository ) . toHaveBeenCalled ( )
184+ expect ( readFile ) . toHaveBeenCalledTimes ( 1 )
185+ expect ( writeFile ) . toHaveBeenCalledTimes ( 1 )
186+ expect ( symlink ) . toHaveBeenCalledTimes ( 2 )
187+ expect ( symlink ) . toHaveBeenCalledWith ( '/path/to/theme/AGENTS.md' , '/path/to/theme/copilot-instructions.md' )
188+ expect ( symlink ) . toHaveBeenCalledWith ( '/path/to/theme/AGENTS.md' , '/path/to/theme/CLAUDE.md' )
189+ } )
190+
191+ test ( 'throws an error when file operations fail' , async ( ) => {
192+ // Given
193+ vi . mocked ( downloadGitRepository ) . mockResolvedValue ( )
194+ vi . mocked ( readFile ) . mockRejectedValue ( new Error ( 'File not found' ) )
173195
174196 await expect ( createAIInstructions ( destination , 'cursor' ) ) . rejects . toThrow ( 'Failed to create AI instructions' )
175197 } )
176198} )
199+
200+ describe ( 'createAIInstructionFiles()' , ( ) => {
201+ const themeRoot = '/path/to/theme'
202+ const agentsPath = '/path/to/theme/AGENTS.md'
203+
204+ beforeEach ( ( ) => {
205+ vi . mocked ( joinPath ) . mockImplementation ( ( ...paths ) => paths . join ( '/' ) )
206+ vi . mocked ( readFile ) . mockResolvedValue ( 'AI instruction content' as any )
207+ vi . mocked ( writeFile ) . mockResolvedValue ( )
208+ vi . mocked ( symlink ) . mockResolvedValue ( )
209+ } )
210+
211+ test ( 'creates symlink for github instruction' , async ( ) => {
212+ // Givin/When
213+ await createAIInstructionFiles ( themeRoot , agentsPath , 'github' )
214+
215+ // Then
216+ expect ( symlink ) . toHaveBeenCalledWith ( '/path/to/theme/AGENTS.md' , '/path/to/theme/copilot-instructions.md' )
217+ } )
218+
219+ test ( 'does not create symlink for cursor instruction (uses AGENTS.md natively)' , async ( ) => {
220+ // When
221+ await createAIInstructionFiles ( themeRoot , agentsPath , 'cursor' )
222+
223+ // Then
224+ expect ( symlink ) . not . toHaveBeenCalled ( )
225+ } )
226+
227+ test ( 'creates symlink for claude instruction' , async ( ) => {
228+ // When
229+ await createAIInstructionFiles ( themeRoot , agentsPath , 'claude' )
230+
231+ // Then
232+ expect ( symlink ) . toHaveBeenCalledWith ( '/path/to/theme/AGENTS.md' , '/path/to/theme/CLAUDE.md' )
233+ } )
234+
235+ test ( 'falls back to copying file when symlink fails with EPERM' , async ( ) => {
236+ // Given
237+ vi . mocked ( symlink ) . mockRejectedValue ( new Error ( 'EPERM: operation not permitted' ) )
238+ vi . mocked ( readFile ) . mockResolvedValue ( 'AGENTS.md content' as any )
239+
240+ // When
241+ const result = await createAIInstructionFiles ( themeRoot , agentsPath , 'github' )
242+
243+ // Then
244+ expect ( symlink ) . toHaveBeenCalled ( )
245+ expect ( readFile ) . toHaveBeenCalledWith ( agentsPath )
246+ expect ( writeFile ) . toHaveBeenCalledWith ( '/path/to/theme/copilot-instructions.md' , 'AGENTS.md content' )
247+ expect ( result . copiedFile ) . toBe ( 'copilot-instructions.md' )
248+ } )
249+ } )
0 commit comments