@@ -10,10 +10,15 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
1010vi . mock ( '@/lib/uploads/core/setup.server' , ( ) => uploadsSetupMock )
1111vi . mock ( '@/lib/uploads/config' , ( ) => uploadsConfigMock )
1212vi . mock ( '@/lib/uploads/server/metadata' , ( ) => uploadsMetadataMock )
13+ vi . mock ( '@/lib/uploads/core/prefix-sweep' , async ( importOriginal ) => {
14+ const actual = await importOriginal < typeof import ( '@/lib/uploads/core/prefix-sweep' ) > ( )
15+ return { ...actual , listStorageKeyPage : vi . fn ( actual . listStorageKeyPage ) }
16+ } )
1317
1418import type { OutboxEventContext } from '@/lib/core/outbox/service'
1519import { PROJECT_FILE_PREFIX_CLEANUP_EVENT } from '@/lib/projects/files/outbox-events'
1620import { projectFilePrefixCleanupOutboxHandlers } from '@/lib/projects/files/prefix-cleanup'
21+ import { listStorageKeyPage } from '@/lib/uploads/core/prefix-sweep'
1722
1823const cleanup = projectFilePrefixCleanupOutboxHandlers [ PROJECT_FILE_PREFIX_CLEANUP_EVENT ]
1924
@@ -68,12 +73,56 @@ describe('Project prefix cleanup on local storage', () => {
6873 expect ( await exists ( 'project/p10/keep.txt' ) ) . toBe ( true )
6974 } )
7075
71- it ( 'spends an attempt when a full page holds only objects it cannot remove' , async ( ) => {
76+ it ( 'reaches objects behind a full page it cannot remove' , async ( ) => {
7277 for ( let index = 0 ; index < 520 ; index ++ ) await put ( `project/p1/legacy file ${ index } .txt` )
78+ await put ( 'project/p1/zz.txt' )
79+
80+ await expect (
81+ cleanup ( { projectId : 'p1' } , outboxContext ( Date . now ( ) + 60_000 ) )
82+ ) . resolves . toBeUndefined ( )
83+
84+ expect ( await exists ( 'project/p1/zz.txt' ) ) . toBe ( false )
85+ } )
86+
87+ it ( 'keeps sweeping after a short page that is not the last' , async ( ) => {
88+ await put ( 'project/p1/a.txt' )
89+ await put ( 'project/p1/b.txt' )
90+ vi . mocked ( listStorageKeyPage ) . mockResolvedValueOnce ( {
91+ keys : [ 'project/p1/a.txt' ] ,
92+ done : false ,
93+ nextPageToken : 'next' ,
94+ } )
95+
96+ const result = await cleanup ( { projectId : 'p1' } , outboxContext ( ) )
97+
98+ expect ( result ) . toMatchObject ( { outcome : 'deferred' , consumeAttempt : false } )
99+ expect ( await exists ( 'project/p1/a.txt' ) ) . toBe ( false )
100+ expect ( await exists ( 'project/p1/b.txt' ) ) . toBe ( true )
101+ } )
102+
103+ it ( 'follows an empty page that is not the last to the objects after it' , async ( ) => {
104+ await put ( 'project/p1/a.txt' )
105+ vi . mocked ( listStorageKeyPage )
106+ . mockResolvedValueOnce ( { keys : [ ] , done : false , nextPageToken : 'next' } )
107+ . mockResolvedValueOnce ( { keys : [ 'project/p1/a.txt' ] , done : true } )
108+
109+ await expect ( cleanup ( { projectId : 'p1' } , outboxContext ( ) ) ) . resolves . toBeUndefined ( )
110+
111+ expect ( vi . mocked ( listStorageKeyPage ) . mock . calls [ 1 ] ?. [ 3 ] ) . toBe ( 'next' )
112+ expect ( await exists ( 'project/p1/a.txt' ) ) . toBe ( false )
113+ } )
114+
115+ it ( 'spends an attempt when its listing stops advancing without removing anything' , async ( ) => {
116+ vi . mocked ( listStorageKeyPage ) . mockResolvedValue ( {
117+ keys : [ ] ,
118+ done : false ,
119+ nextPageToken : 'stuck' ,
120+ } )
73121
74- const result = await cleanup ( { projectId : 'p1' } , outboxContext ( Date . now ( ) + 60_000 ) )
122+ const result = await cleanup ( { projectId : 'p1' } , outboxContext ( ) )
75123
76124 expect ( result ) . toMatchObject ( { outcome : 'deferred' } )
77125 expect ( result ) . not . toMatchObject ( { consumeAttempt : false } )
126+ expect ( listStorageKeyPage ) . toHaveBeenCalledTimes ( 2 )
78127 } )
79128} )
0 commit comments