@@ -6,16 +6,13 @@ import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
66import { FunctionConfigType } from '../../models/extensions/specifications/function.js'
77import { selectFunctionRunPrompt } from '../../prompts/function/replay.js'
88import { randomUUID } from '@shopify/cli-kit/node/crypto'
9- import { readFile } from '@shopify/cli-kit/node/fs'
10- import { describe , expect , beforeAll , test , vi } from 'vitest'
9+ import { writeFile , mkdir } from '@shopify/cli-kit/node/fs'
10+ import { joinPath } from '@shopify/cli-kit/node/path'
11+ import { describe , expect , beforeAll , vi } from 'vitest'
1112import { AbortError } from '@shopify/cli-kit/node/error'
1213import { outputInfo } from '@shopify/cli-kit/node/output'
13- import { getLogsDir } from '@shopify/cli-kit/node/logs '
14+ import { testWithTempDir } from '@shopify/cli-kit/node/testing/test-with-temp-dir '
1415
15- import { existsSync , readdirSync } from 'fs'
16-
17- vi . mock ( 'fs' )
18- vi . mock ( '@shopify/cli-kit/node/fs' )
1916vi . mock ( '../generate-schema.js' )
2017vi . mock ( '../../prompts/function/replay.js' )
2118vi . mock ( '../dev/extension/bundler.js' )
@@ -45,17 +42,19 @@ describe('replay', () => {
4542 extension = await testFunctionExtension ( { config : defaultConfig } )
4643 } )
4744
48- test ( 'runs selected function' , async ( ) => {
45+ testWithTempDir ( 'runs selected function' , async ( { tempDir } ) => {
4946 // Given
50- const file1 = createFunctionRunFile ( { handle : extension . handle } )
51- const file2 = createFunctionRunFile ( { handle : extension . handle } )
52- mockFileOperations ( [ file1 , file2 ] )
47+ const app = testAppLinked ( { directory : tempDir } )
48+ const logsDir = app . getLogsDir ( )
49+ const file1 = createFunctionRunFile ( { handle : extension . handle , index : 1 } )
50+ const file2 = createFunctionRunFile ( { handle : extension . handle , index : 2 } )
51+ await writeFunctionRunFiles ( logsDir , [ file1 , file2 ] )
5352
5453 vi . mocked ( selectFunctionRunPrompt ) . mockResolvedValue ( file1 . run )
5554
5655 // When
5756 await replay ( {
58- app : testAppLinked ( ) ,
57+ app,
5958 extension,
6059 stdout : false ,
6160 path : 'test-path' ,
@@ -64,20 +63,24 @@ describe('replay', () => {
6463 } )
6564
6665 // Then
67- expect ( selectFunctionRunPrompt ) . toHaveBeenCalledWith ( [ file1 . run , file2 . run ] )
66+ expect ( selectFunctionRunPrompt ) . toHaveBeenCalledWith ( [ file2 . run , file1 . run ] )
6867 expectFunctionRun ( extension , file1 . run . payload . input )
6968 expect ( outputInfo ) . not . toHaveBeenCalled ( )
7069 } )
7170
72- test ( 'only allows selection of the most recent 100 runs' , async ( ) => {
71+ testWithTempDir ( 'only allows selection of the most recent 100 runs' , async ( { tempDir } ) => {
7372 // Given
74- const files = new Array ( 101 ) . fill ( undefined ) . map ( ( _ ) => createFunctionRunFile ( { handle : extension . handle } ) )
75- mockFileOperations ( files )
73+ const app = testAppLinked ( { directory : tempDir } )
74+ const logsDir = app . getLogsDir ( )
75+ const files = new Array ( 101 )
76+ . fill ( undefined )
77+ . map ( ( _ , i ) => createFunctionRunFile ( { handle : extension . handle , index : i } ) )
78+ await writeFunctionRunFiles ( logsDir , files )
7679 vi . mocked ( selectFunctionRunPrompt ) . mockResolvedValue ( files [ 100 ] ! . run )
7780
7881 // When
7982 await replay ( {
80- app : testAppLinked ( ) ,
83+ app,
8184 extension,
8285 stdout : false ,
8386 path : 'test-path' ,
@@ -86,20 +89,27 @@ describe('replay', () => {
8689 } )
8790
8891 // Then
89- expect ( selectFunctionRunPrompt ) . toHaveBeenCalledWith ( files . map ( ( { run} ) => run ) . slice ( 0 , 100 ) )
92+ expect ( selectFunctionRunPrompt ) . toHaveBeenCalledWith (
93+ files
94+ . map ( ( { run} ) => run )
95+ . reverse ( )
96+ . slice ( 0 , 100 ) ,
97+ )
9098 } )
9199
92- test ( 'does not allow selection of runs for other functions' , async ( ) => {
100+ testWithTempDir ( 'does not allow selection of runs for other functions' , async ( { tempDir } ) => {
93101 // Given
94- const file1 = createFunctionRunFile ( { handle : extension . handle } )
95- const file2 = createFunctionRunFile ( { handle : 'another-function-handle' } )
96- mockFileOperations ( [ file1 , file2 ] )
102+ const app = testAppLinked ( { directory : tempDir } )
103+ const logsDir = app . getLogsDir ( )
104+ const file1 = createFunctionRunFile ( { handle : extension . handle , index : 1 } )
105+ const file2 = createFunctionRunFile ( { handle : 'another-function-handle' , index : 2 } )
106+ await writeFunctionRunFiles ( logsDir , [ file1 , file2 ] )
97107
98108 vi . mocked ( selectFunctionRunPrompt ) . mockResolvedValue ( file1 . run )
99109
100110 // When
101111 await replay ( {
102- app : testAppLinked ( ) ,
112+ app,
103113 extension,
104114 stdout : false ,
105115 path : 'test-path' ,
@@ -111,51 +121,57 @@ describe('replay', () => {
111121 expect ( selectFunctionRunPrompt ) . toHaveBeenCalledWith ( [ file1 . run ] )
112122 } )
113123
114- test ( 'throws error if no logs available' , async ( ) => {
124+ testWithTempDir ( 'throws error if no logs available' , async ( { tempDir } ) => {
115125 // Given
116- mockFileOperations ( [ ] )
126+ const app = testAppLinked ( { directory : tempDir } )
127+ const logsDir = app . getLogsDir ( )
128+ await mkdir ( logsDir )
117129
118130 // When/Then
119131 await expect ( async ( ) => {
120132 await replay ( {
121- app : testAppLinked ( ) ,
133+ app,
122134 extension,
123135 stdout : false ,
124136 path : 'test-path' ,
125137 json : true ,
126138 watch : false ,
127139 } )
128- } ) . rejects . toThrow ( new AbortError ( `No logs found in ${ getLogsDir ( ) } ` ) )
140+ } ) . rejects . toThrow ( new AbortError ( `No logs found in ${ logsDir } ` ) )
129141 } )
130142
131- test ( 'throws error if log directory does not exist' , async ( ) => {
143+ testWithTempDir ( 'throws error if log directory does not exist' , async ( { tempDir } ) => {
132144 // Given
133- vi . mocked ( existsSync ) . mockReturnValue ( false )
145+ const app = testAppLinked ( { directory : tempDir } )
146+ const logsDir = app . getLogsDir ( )
147+ // logsDir does not exist
134148
135149 // When/Then
136150 await expect ( async ( ) => {
137151 await replay ( {
138- app : testAppLinked ( ) ,
152+ app,
139153 extension,
140154 stdout : false ,
141155 path : 'test-path' ,
142156 json : true ,
143157 watch : false ,
144158 } )
145- } ) . rejects . toThrow ( new AbortError ( `No logs found in ${ getLogsDir ( ) } ` ) )
159+ } ) . rejects . toThrow ( new AbortError ( `No logs found in ${ logsDir } ` ) )
146160 } )
147161
148- test ( 'delegates to renderReplay when watch is true' , async ( ) => {
162+ testWithTempDir ( 'delegates to renderReplay when watch is true' , async ( { tempDir } ) => {
149163 // Given
164+ const app = testAppLinked ( { directory : tempDir } )
165+ const logsDir = app . getLogsDir ( )
150166 const file = createFunctionRunFile ( { handle : extension . handle } )
151- mockFileOperations ( [ file ] )
167+ await writeFunctionRunFiles ( logsDir , [ file ] )
152168 vi . mocked ( selectFunctionRunPrompt ) . mockResolvedValue ( file . run )
153169
154170 vi . mocked ( renderReplay )
155171
156172 // When
157173 await replay ( {
158- app : testAppLinked ( ) ,
174+ app,
159175 extension,
160176 stdout : false ,
161177 path : 'test-path' ,
@@ -166,18 +182,20 @@ describe('replay', () => {
166182 expect ( renderReplay ) . toHaveBeenCalledOnce ( )
167183 } )
168184
169- test ( 'aborts on error' , async ( ) => {
185+ testWithTempDir ( 'aborts on error' , async ( { tempDir } ) => {
170186 // Given
187+ const app = testAppLinked ( { directory : tempDir } )
188+ const logsDir = app . getLogsDir ( )
171189 const file = createFunctionRunFile ( { handle : extension . handle } )
172- mockFileOperations ( [ file ] )
190+ await writeFunctionRunFiles ( logsDir , [ file ] )
173191
174192 vi . mocked ( selectFunctionRunPrompt ) . mockResolvedValue ( file . run )
175193 vi . mocked ( renderReplay ) . mockRejectedValueOnce ( 'failure' )
176194
177195 // When
178196 await expect ( async ( ) =>
179197 replay ( {
180- app : testAppLinked ( ) ,
198+ app,
181199 extension,
182200 stdout : false ,
183201 path : 'test-path' ,
@@ -192,18 +210,20 @@ describe('replay', () => {
192210 expect ( abortSignal . aborted ) . toBeTruthy ( )
193211 } )
194212
195- test ( 'runs the log specified by the --log flag for the current function' , async ( ) => {
213+ testWithTempDir ( 'runs the log specified by the --log flag for the current function' , async ( { tempDir } ) => {
196214 // Given
215+ const app = testAppLinked ( { directory : tempDir } )
216+ const logsDir = app . getLogsDir ( )
197217 const identifier = '000000'
198- const file1 = createFunctionRunFile ( { handle : extension . handle } )
199- const file2 = createFunctionRunFile ( { handle : extension . handle , identifier} )
200- const file3 = createFunctionRunFile ( { handle : extension . handle } )
201- const file4 = createFunctionRunFile ( { handle : 'another-extension' , identifier} )
202- mockFileOperations ( [ file1 , file2 , file3 , file4 ] )
218+ const file1 = createFunctionRunFile ( { handle : extension . handle , index : 1 } )
219+ const file2 = createFunctionRunFile ( { handle : extension . handle , identifier, index : 2 } )
220+ const file3 = createFunctionRunFile ( { handle : extension . handle , index : 3 } )
221+ const file4 = createFunctionRunFile ( { handle : 'another-extension' , identifier, index : 4 } )
222+ await writeFunctionRunFiles ( logsDir , [ file1 , file2 , file3 , file4 ] )
203223
204224 // When
205225 await replay ( {
206- app : testAppLinked ( ) ,
226+ app,
207227 extension,
208228 stdout : false ,
209229 path : 'test-path' ,
@@ -216,17 +236,19 @@ describe('replay', () => {
216236 expectFunctionRun ( extension , file2 . run . payload . input )
217237 } )
218238
219- test ( 'throws error if the log specified by the --log flag is not found' , async ( ) => {
239+ testWithTempDir ( 'throws error if the log specified by the --log flag is not found' , async ( { tempDir } ) => {
220240 // Given
241+ const app = testAppLinked ( { directory : tempDir } )
242+ const logsDir = app . getLogsDir ( )
221243 const identifier = '000000'
222- const file1 = createFunctionRunFile ( { handle : extension . handle } )
223- const file2 = createFunctionRunFile ( { handle : extension . handle } )
224- mockFileOperations ( [ file1 , file2 ] )
244+ const file1 = createFunctionRunFile ( { handle : extension . handle , index : 1 } )
245+ const file2 = createFunctionRunFile ( { handle : extension . handle , index : 2 } )
246+ await writeFunctionRunFiles ( logsDir , [ file1 , file2 ] )
225247
226248 // When
227249 await expect ( async ( ) =>
228250 replay ( {
229- app : testAppLinked ( ) ,
251+ app,
230252 extension,
231253 stdout : false ,
232254 path : 'test-path' ,
@@ -237,19 +259,25 @@ describe('replay', () => {
237259 ) . rejects . toThrow ( )
238260 } )
239261
240- test ( 'ignores runs with no input and keeps reading chunks until past the threshold' , async ( ) => {
262+ testWithTempDir ( 'ignores runs with no input and keeps reading chunks until past the threshold' , async ( { tempDir } ) => {
241263 // Given
242- const filesWithInput = new Array ( 99 ) . fill ( undefined ) . map ( ( _ ) => createFunctionRunFile ( { handle : extension . handle } ) )
243- const fileWithoutInput = createFunctionRunFile ( { handle : extension . handle , partialPayload : { input : null } } )
244- const additionalFiles = new Array ( 199 ) . fill ( undefined ) . map ( ( _ ) => createFunctionRunFile ( { handle : extension . handle } ) )
245-
246- mockFileOperations ( [ ...filesWithInput , fileWithoutInput , ...additionalFiles ] )
264+ const app = testAppLinked ( { directory : tempDir } )
265+ const logsDir = app . getLogsDir ( )
266+ const filesWithInput = new Array ( 99 )
267+ . fill ( undefined )
268+ . map ( ( _ , i ) => createFunctionRunFile ( { handle : extension . handle , index : i } ) )
269+ const fileWithoutInput = createFunctionRunFile ( { handle : extension . handle , partialPayload : { input : null } , index : 99 } )
270+ const additionalFiles = new Array ( 199 )
271+ . fill ( undefined )
272+ . map ( ( _ , i ) => createFunctionRunFile ( { handle : extension . handle , index : 100 + i } ) )
273+
274+ await writeFunctionRunFiles ( logsDir , [ ...filesWithInput , fileWithoutInput , ...additionalFiles ] )
247275
248276 vi . mocked ( selectFunctionRunPrompt ) . mockResolvedValue ( filesWithInput [ 0 ] ! . run )
249277
250278 // When
251279 await replay ( {
252- app : testAppLinked ( ) ,
280+ app,
253281 extension,
254282 stdout : false ,
255283 path : 'test-path' ,
@@ -259,7 +287,7 @@ describe('replay', () => {
259287
260288 // Then
261289 expect ( selectFunctionRunPrompt ) . toHaveBeenCalledWith (
262- [ ...filesWithInput , ...additionalFiles . slice ( 0 , 100 ) ] . map ( ( { run} ) => run ) ,
290+ [ ...additionalFiles . reverse ( ) , ...filesWithInput . reverse ( ) ] . slice ( 0 , 100 ) . map ( ( { run} ) => run ) ,
263291 )
264292 } )
265293} )
@@ -268,12 +296,15 @@ interface FunctionRunFileOptions {
268296 handle : string
269297 identifier ?: string
270298 partialPayload ?: object
299+ index ?: number
271300}
272301function createFunctionRunFile ( options : FunctionRunFileOptions ) {
273302 const handle = options . handle
274303 const identifier = options . identifier ?? randomUUID ( ) . substring ( 0 , 6 )
275304 const partialPayload = options . partialPayload ?? { }
276- const path = `20240522_150641_827Z_extensions_${ handle } _${ identifier } .json`
305+ const index = options . index ?? 0
306+ const seconds = index . toString ( ) . padStart ( 6 , '0' )
307+ const path = `20240522_00${ seconds } _827Z_extensions_${ handle } _${ identifier } .json`
277308 const run : FunctionRunData = {
278309 identifier,
279310 shopId : 1 ,
@@ -304,14 +335,10 @@ function expectFunctionRun(functionExtension: ExtensionInstance<FunctionConfigTy
304335 expect ( runFunction ) . toHaveBeenCalledWith ( { functionExtension, json : true , export : 'run' , input : JSON . stringify ( input ) } )
305336}
306337
307- function mockFileOperations ( data : { run : FunctionRunData ; path : string } [ ] ) {
308- vi . mocked ( existsSync ) . mockReturnValue ( true )
309- vi . mocked ( readdirSync ) . mockReturnValue ( [ ...data ] . reverse ( ) . map ( ( { path} ) => path ) as any )
310- vi . mocked ( readFile ) . mockImplementation ( ( path ) => {
311- const run = data . find ( ( file ) => path . endsWith ( file . path ) )
312- if ( ! run ) {
313- throw new AbortError ( `Mock file not found: ${ path } ` )
314- }
315- return Promise . resolve ( Buffer . from ( JSON . stringify ( run . run ) , 'utf8' ) )
316- } )
338+ async function writeFunctionRunFiles ( logsDir : string , data : { run : FunctionRunData ; path : string } [ ] ) {
339+ await mkdir ( logsDir )
340+ for ( const file of data ) {
341+ // eslint-disable-next-line no-await-in-loop
342+ await writeFile ( joinPath ( logsDir , file . path ) , JSON . stringify ( file . run ) )
343+ }
317344}
0 commit comments