@@ -202,4 +202,95 @@ describe('CacheStore', () => {
202202 pathExistsSpy . mockRestore ( )
203203 readSpy . mockRestore ( )
204204 } )
205+
206+ it ( 'skips cache writes when the destination cannot be removed (async)' , async ( ) => {
207+ const cachePath = path . join ( tempDir , 'cache.json' )
208+ await fs . writeJSON ( cachePath , [ 'old' ] )
209+
210+ const store = new CacheStore ( {
211+ enabled : true ,
212+ cwd : tempDir ,
213+ dir : tempDir ,
214+ file : 'cache.json' ,
215+ path : cachePath ,
216+ strategy : 'merge' ,
217+ } )
218+
219+ const writeJSONSpy = vi . spyOn ( fs , 'writeJSON' )
220+ const debugSpy = vi . spyOn ( logger , 'debug' )
221+ const renameSpy = vi . spyOn ( fs , 'rename' ) . mockRejectedValueOnce (
222+ Object . assign ( new Error ( 'exists' ) , { code : 'EEXIST' as NodeJS . ErrnoException [ 'code' ] } ) ,
223+ )
224+ const realRemove = fs . remove
225+ let removeAttempts = 0
226+ const removeSpy = vi . spyOn ( fs , 'remove' ) . mockImplementation ( async ( ...args ) => {
227+ removeAttempts += 1
228+ if ( removeAttempts === 1 ) {
229+ const error = Object . assign ( new Error ( 'locked' ) , { code : 'EPERM' as NodeJS . ErrnoException [ 'code' ] } )
230+ throw error
231+ }
232+ return realRemove . apply ( fs , args as Parameters < typeof fs . remove > )
233+ } )
234+
235+ const result = await store . write ( new Set ( [ 'fresh' ] ) )
236+ expect ( result ) . toBeUndefined ( )
237+ expect ( renameSpy ) . toHaveBeenCalledTimes ( 1 )
238+ expect ( removeSpy ) . toHaveBeenCalledTimes ( 2 )
239+ expect ( debugSpy ) . toHaveBeenCalled ( )
240+
241+ const tempPath = writeJSONSpy . mock . calls [ 0 ] ?. [ 0 ] as string
242+ expect ( tempPath ) . toBeTruthy ( )
243+ expect ( await fs . pathExists ( tempPath ) ) . toBe ( false )
244+
245+ writeJSONSpy . mockRestore ( )
246+ debugSpy . mockRestore ( )
247+ renameSpy . mockRestore ( )
248+ removeSpy . mockRestore ( )
249+ } )
250+
251+ it ( 'skips cache writes when the destination cannot be removed (sync)' , ( ) => {
252+ const cachePath = path . join ( tempDir , 'cache.json' )
253+ fs . writeJSONSync ( cachePath , [ 'old' ] )
254+
255+ const store = new CacheStore ( {
256+ enabled : true ,
257+ cwd : tempDir ,
258+ dir : tempDir ,
259+ file : 'cache.json' ,
260+ path : cachePath ,
261+ strategy : 'merge' ,
262+ } )
263+
264+ const writeJSONSpy = vi . spyOn ( fs , 'writeJSONSync' )
265+ const debugSpy = vi . spyOn ( logger , 'debug' )
266+ const renameSpy = vi . spyOn ( fs , 'renameSync' ) . mockImplementationOnce ( ( ) => {
267+ const error = Object . assign ( new Error ( 'exists' ) , { code : 'EEXIST' as NodeJS . ErrnoException [ 'code' ] } )
268+ throw error
269+ } )
270+ const realRemove = fs . removeSync
271+ let removeAttempts = 0
272+ const removeSpy = vi . spyOn ( fs , 'removeSync' ) . mockImplementation ( ( ...args ) => {
273+ removeAttempts += 1
274+ if ( removeAttempts === 1 ) {
275+ const error = Object . assign ( new Error ( 'locked' ) , { code : 'EPERM' as NodeJS . ErrnoException [ 'code' ] } )
276+ throw error
277+ }
278+ return realRemove . apply ( fs , args as Parameters < typeof fs . removeSync > )
279+ } )
280+
281+ const result = store . writeSync ( new Set ( [ 'fresh' ] ) )
282+ expect ( result ) . toBeUndefined ( )
283+ expect ( renameSpy ) . toHaveBeenCalledTimes ( 1 )
284+ expect ( removeSpy ) . toHaveBeenCalledTimes ( 2 )
285+ expect ( debugSpy ) . toHaveBeenCalled ( )
286+
287+ const tempPath = writeJSONSpy . mock . calls [ 0 ] ?. [ 0 ] as string
288+ expect ( tempPath ) . toBeTruthy ( )
289+ expect ( fs . pathExistsSync ( tempPath ) ) . toBe ( false )
290+
291+ writeJSONSpy . mockRestore ( )
292+ debugSpy . mockRestore ( )
293+ renameSpy . mockRestore ( )
294+ removeSpy . mockRestore ( )
295+ } )
205296} )
0 commit comments