@@ -2,7 +2,7 @@ import fs from 'node:fs/promises'
22import os from 'node:os'
33import path from 'node:path'
44import sharp from 'sharp'
5- import { afterEach , beforeEach , describe , expect , it } from 'vitest'
5+ import { afterEach , beforeEach , describe , expect , it , vi } from 'vitest'
66import { imagePreset , resolveImagesConfig } from './config'
77import { ImageProcessor } from './processor'
88
@@ -12,6 +12,7 @@ describe('image processor and persistent cache', () => {
1212 root = await fs . mkdtemp ( path . join ( os . tmpdir ( ) , 'purestack-image-test-' ) )
1313 } )
1414 afterEach ( async ( ) => {
15+ vi . restoreAllMocks ( )
1516 await fs . rm ( root , { recursive : true , force : true } )
1617 } )
1718 const config = ( options = { } ) =>
@@ -71,7 +72,8 @@ describe('image processor and persistent cache', () => {
7172 first ?. variants . map ( ( variant ) => variant . hash ) ,
7273 )
7374 const file = path . join ( settings . cacheDir , first ?. key ?? '' , fallback . file )
74- await fs . writeFile ( file , 'Corrupt cache' )
75+ // Same-size corruption must still be detected by content verification.
76+ await fs . writeFile ( file , Buffer . alloc ( fallback . data . length ) )
7577 expect (
7678 ( await new ImageProcessor ( settings ) . process ( source ( ) , settings ) ) ?. cache ,
7779 ) . toBe ( 'miss' )
@@ -234,4 +236,63 @@ describe('image processor and persistent cache', () => {
234236 ) . rejects . toThrow ( 'including symlink targets' )
235237 expect ( await fs . readdir ( content ) ) . toEqual ( [ ] )
236238 } )
239+
240+ it ( 'renews cache hits and prunes expired variants as source images change' , async ( ) => {
241+ const settings = config ( { widths : [ 16 ] , formats : [ ] , cacheMaxAgeDays : 1 } )
242+ const write = ( background : string ) =>
243+ sharp ( { create : { width : 32 , height : 16 , channels : 3 , background } } )
244+ . png ( )
245+ . toFile ( source ( ) . absPath )
246+ await write ( 'red' )
247+ const first = await new ImageProcessor ( settings ) . process ( source ( ) , settings )
248+ if ( ! first ) throw new Error ( 'Expected image' )
249+ const manifest = path . join ( settings . cacheDir , first . key , 'manifest.json' )
250+ const old = new Date ( Date . now ( ) - 3 * 86_400_000 )
251+ await fs . utimes ( manifest , old , old )
252+ // A requested entry is reused and renewed even if its prior age exceeded the limit.
253+ expect (
254+ ( await new ImageProcessor ( settings ) . process ( source ( ) , settings ) ) ?. cache ,
255+ ) . toBe ( 'hit' )
256+ expect ( ( await fs . stat ( manifest ) ) . mtimeMs ) . toBeGreaterThan ( old . getTime ( ) )
257+ await fs . utimes ( manifest , old , old )
258+ await write ( 'blue' )
259+ const next = await new ImageProcessor ( settings ) . process ( source ( ) , settings )
260+ expect ( next ?. key ) . not . toBe ( first . key )
261+ await expect ( fs . stat ( manifest ) ) . rejects . toMatchObject ( { code : 'ENOENT' } )
262+ expect (
263+ ( await new ImageProcessor ( settings ) . process ( source ( ) , settings ) ) ?. cache ,
264+ ) . toBe ( 'hit' )
265+ } )
266+
267+ it ( 'publishes complete entries when independent processors share a cache, including corruption repair' , async ( ) => {
268+ await sharp ( {
269+ create : { width : 32 , height : 16 , channels : 3 , background : 'green' } ,
270+ } )
271+ . png ( )
272+ . toFile ( source ( ) . absPath )
273+ const settings = config ( { widths : [ 16 ] , formats : [ 'webp' ] } )
274+ const processTogether = ( ) =>
275+ Promise . all (
276+ Array . from ( { length : 4 } , ( ) =>
277+ new ImageProcessor ( settings ) . process ( source ( ) , settings ) ,
278+ ) ,
279+ )
280+ const first = await processTogether ( )
281+ expect ( new Set ( first . map ( ( value ) => value ?. key ) ) . size ) . toBe ( 1 )
282+ const value = first [ 0 ]
283+ if ( ! value ) throw new Error ( 'Expected image' )
284+ await fs . writeFile (
285+ path . join ( settings . cacheDir , value . key , value . variants [ 0 ] . file ) ,
286+ Buffer . alloc ( value . variants [ 0 ] . data . length ) ,
287+ )
288+ const repaired = await processTogether ( )
289+ for ( const result of repaired )
290+ expect ( result ?. variants . map ( ( variant ) => variant . hash ) ) . toEqual (
291+ value . variants . map ( ( variant ) => variant . hash ) ,
292+ )
293+ expect (
294+ ( await new ImageProcessor ( settings ) . process ( source ( ) , settings ) ) ?. cache ,
295+ ) . toBe ( 'hit' )
296+ expect ( await fs . readdir ( settings . cacheDir ) ) . toEqual ( [ value . key ] )
297+ } )
237298} )
0 commit comments