@@ -4,6 +4,7 @@ import { describe, it, expect, vi } from 'vitest';
44import {
55 installAttachmentLifecycleHooks ,
66 createSysFileReapGuard ,
7+ createUploadSessionReapGuard ,
78 type AttachmentLifecycleEngine ,
89} from './attachment-lifecycle.js' ;
910
@@ -247,3 +248,93 @@ describe('createSysFileReapGuard', () => {
247248 expect ( confirmed ) . toEqual ( [ ] ) ;
248249 } ) ;
249250} ) ;
251+
252+ describe ( 'createUploadSessionReapGuard' , ( ) => {
253+ /** Swappable-style storage fake: `getInner()` exposes an S3-like inner with
254+ * `setUploadKey`; `abortChunkedUpload` is forwarded. */
255+ const s3Storage = ( abortImpl ?: ( ) => Promise < void > ) => {
256+ const inner = {
257+ setUploadKey : vi . fn ( ( _id : string , _key : string ) => { } ) ,
258+ } ;
259+ return {
260+ getInner : ( ) => inner ,
261+ abortChunkedUpload : vi . fn ( abortImpl ?? ( async ( ) => { } ) ) ,
262+ _inner : inner ,
263+ } as any ;
264+ } ;
265+
266+ it ( 'aborts the backend multipart (re-seeding the key) then reaps an abandoned session' , async ( ) => {
267+ const s = s3Storage ( ) ;
268+ const guard = createUploadSessionReapGuard ( ( ) => s , silentLogger ( ) ) ;
269+
270+ const confirmed = await guard ( 'sys_upload_session' , [
271+ { id : 'u1' , backend_upload_id : 'mp-1' , key : 'attachments/u1.bin' , status : 'in_progress' } ,
272+ ] ) ;
273+
274+ expect ( s . _inner . setUploadKey ) . toHaveBeenCalledWith ( 'mp-1' , 'attachments/u1.bin' ) ;
275+ expect ( s . abortChunkedUpload ) . toHaveBeenCalledWith ( 'mp-1' ) ;
276+ expect ( confirmed ) . toEqual ( [ 'u1' ] ) ;
277+ } ) ;
278+
279+ it ( 'does NOT abort a completed session (its multipart is already an object) — just reaps the row' , async ( ) => {
280+ const s = s3Storage ( ) ;
281+ const guard = createUploadSessionReapGuard ( ( ) => s , silentLogger ( ) ) ;
282+
283+ const confirmed = await guard ( 'sys_upload_session' , [
284+ { id : 'u2' , backend_upload_id : 'mp-2' , key : 'attachments/u2.bin' , status : 'completed' } ,
285+ ] ) ;
286+
287+ expect ( s . abortChunkedUpload ) . not . toHaveBeenCalled ( ) ;
288+ expect ( confirmed ) . toEqual ( [ 'u2' ] ) ;
289+ } ) ;
290+
291+ it ( 'reaps a session with no backend_upload_id without calling abort' , async ( ) => {
292+ const s = s3Storage ( ) ;
293+ const guard = createUploadSessionReapGuard ( ( ) => s , silentLogger ( ) ) ;
294+
295+ const confirmed = await guard ( 'sys_upload_session' , [
296+ { id : 'u3' , key : 'attachments/u3.bin' , status : 'expired' } ,
297+ ] ) ;
298+
299+ expect ( s . abortChunkedUpload ) . not . toHaveBeenCalled ( ) ;
300+ expect ( confirmed ) . toEqual ( [ 'u3' ] ) ;
301+ } ) ;
302+
303+ it ( 'VETOES on abort failure so backend_upload_id survives for the retry' , async ( ) => {
304+ const s = s3Storage ( async ( ) => {
305+ throw new Error ( 'S3 abort transient failure' ) ;
306+ } ) ;
307+ const logger = silentLogger ( ) ;
308+ const guard = createUploadSessionReapGuard ( ( ) => s , logger ) ;
309+
310+ const confirmed = await guard ( 'sys_upload_session' , [
311+ { id : 'u4' , backend_upload_id : 'mp-4' , key : 'attachments/u4.bin' , status : 'failed' } ,
312+ ] ) ;
313+
314+ expect ( confirmed ) . toEqual ( [ ] ) ; // kept
315+ expect ( logger . warn ) . toHaveBeenCalled ( ) ;
316+ } ) ;
317+
318+ it ( 'works with a local-style adapter (no setUploadKey / getInner) — aborts by id' , async ( ) => {
319+ const local = { abortChunkedUpload : vi . fn ( async ( ) => { } ) } as any ;
320+ const guard = createUploadSessionReapGuard ( ( ) => local , silentLogger ( ) ) ;
321+
322+ const confirmed = await guard ( 'sys_upload_session' , [
323+ { id : 'u5' , backend_upload_id : 'local-5' , key : 'attachments/u5.bin' , status : 'expired' } ,
324+ ] ) ;
325+
326+ expect ( local . abortChunkedUpload ) . toHaveBeenCalledWith ( 'local-5' ) ;
327+ expect ( confirmed ) . toEqual ( [ 'u5' ] ) ;
328+ } ) ;
329+
330+ it ( 'reaps the row when the adapter cannot abort at all' , async ( ) => {
331+ const noAbort = { } as any ;
332+ const guard = createUploadSessionReapGuard ( ( ) => noAbort , silentLogger ( ) ) ;
333+
334+ const confirmed = await guard ( 'sys_upload_session' , [
335+ { id : 'u6' , backend_upload_id : 'mp-6' , key : 'k' , status : 'in_progress' } ,
336+ ] ) ;
337+
338+ expect ( confirmed ) . toEqual ( [ 'u6' ] ) ;
339+ } ) ;
340+ } ) ;
0 commit comments