@@ -52,30 +52,97 @@ describe("media store", () => {
5252 segment : string ;
5353 run : ( store : typeof import ( "./store.js" ) , home : string ) => Promise < { path : string } > ;
5454 } ) {
55- await withTempStore ( async ( store , home ) => {
56- const originalWriteFile = fs . writeFile . bind ( fs ) ;
57- let injectedEnoent = false ;
58- vi . spyOn ( fs , "writeFile" ) . mockImplementation ( async ( ...args ) => {
59- const [ filePath ] = args ;
60- if (
61- ! injectedEnoent &&
62- typeof filePath === "string" &&
63- filePath . includes ( `${ path . sep } ${ params . segment } ${ path . sep } ` )
64- ) {
65- injectedEnoent = true ;
66- await fs . rm ( path . dirname ( filePath ) , { recursive : true , force : true } ) ;
67- const err = new Error ( "missing dir" ) as NodeJS . ErrnoException ;
68- err . code = "ENOENT" ;
69- throw err ;
70- }
71- return await originalWriteFile ( ...args ) ;
55+ const mockKey = `./store.js?scope=retry-pruned-write-${ params . segment } -${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ;
56+ let injectedEnoent = false ;
57+ vi . doMock ( "../infra/file-store.js" , async ( importOriginal ) => {
58+ const actual = await importOriginal < typeof import ( "../infra/file-store.js" ) > ( ) ;
59+ return {
60+ ...actual ,
61+ fileStore : ( options : Parameters < typeof actual . fileStore > [ 0 ] ) => {
62+ const actualStore = actual . fileStore ( options ) ;
63+ return {
64+ ...actualStore ,
65+ write : async ( ...args : Parameters < typeof actualStore . write > ) => {
66+ const [ relativePath ] = args ;
67+ if ( ! injectedEnoent && relativePath . includes ( `${ params . segment } ${ path . sep } ` ) ) {
68+ injectedEnoent = true ;
69+ await fs . rm ( path . dirname ( actualStore . path ( relativePath ) ) , {
70+ recursive : true ,
71+ force : true ,
72+ } ) ;
73+ const err = new Error ( "missing dir" ) as NodeJS . ErrnoException ;
74+ err . code = "ENOENT" ;
75+ throw err ;
76+ }
77+ return await actualStore . write ( ...args ) ;
78+ } ,
79+ } ;
80+ } ,
81+ } ;
82+ } ) ;
83+
84+ try {
85+ const storeWithMock = await importFreshModule < typeof import ( "./store.js" ) > (
86+ import . meta. url ,
87+ mockKey ,
88+ ) ;
89+ await withTempStore ( async ( _store , home ) => {
90+ const saved = await params . run ( storeWithMock , home ) ;
91+ const savedStat = await fs . stat ( saved . path ) ;
92+ expect ( injectedEnoent ) . toBe ( true ) ;
93+ expect ( savedStat . isFile ( ) ) . toBe ( true ) ;
7294 } ) ;
95+ } finally {
96+ vi . doUnmock ( "../infra/file-store.js" ) ;
97+ }
98+ }
7399
74- const saved = await params . run ( store , home ) ;
75- const savedStat = await fs . stat ( saved . path ) ;
76- expect ( injectedEnoent ) . toBe ( true ) ;
77- expect ( savedStat . isFile ( ) ) . toBe ( true ) ;
100+ async function expectFailedBufferWriteCase ( ) {
101+ const mockKey = `./store.js?scope=failed-buffer-write-${ Date . now ( ) } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 ) } ` ;
102+ const attemptedRelPaths : string [ ] = [ ] ;
103+ vi . doMock ( "../infra/file-store.js" , async ( importOriginal ) => {
104+ const actual = await importOriginal < typeof import ( "../infra/file-store.js" ) > ( ) ;
105+ return {
106+ ...actual ,
107+ fileStore : ( options : Parameters < typeof actual . fileStore > [ 0 ] ) => {
108+ const actualStore = actual . fileStore ( options ) ;
109+ return {
110+ ...actualStore ,
111+ write : async ( ...args : Parameters < typeof actualStore . write > ) => {
112+ const [ relativePath ] = args ;
113+ if ( relativePath . includes ( `failed-buffer${ path . sep } ` ) ) {
114+ attemptedRelPaths . push ( relativePath ) ;
115+ const err = new Error ( "no space left on device" ) as NodeJS . ErrnoException ;
116+ err . code = "ENOSPC" ;
117+ throw err ;
118+ }
119+ return await actualStore . write ( ...args ) ;
120+ } ,
121+ } ;
122+ } ,
123+ } ;
78124 } ) ;
125+
126+ try {
127+ const storeWithMock = await importFreshModule < typeof import ( "./store.js" ) > (
128+ import . meta. url ,
129+ mockKey ,
130+ ) ;
131+ await withTempStore ( async ( _store ) => {
132+ const mediaDir = await storeWithMock . ensureMediaDir ( ) ;
133+ await expect (
134+ storeWithMock . saveMediaBuffer ( Buffer . from ( "voice" ) , "audio/ogg" , "failed-buffer" ) ,
135+ ) . rejects . toMatchObject ( { code : "ENOSPC" } ) ;
136+
137+ const failedDir = path . join ( mediaDir , "failed-buffer" ) ;
138+ const entries = await fs . readdir ( failedDir ) . catch ( ( ) => [ ] ) ;
139+ expect ( attemptedRelPaths ) . toHaveLength ( 1 ) ;
140+ expect ( path . basename ( attemptedRelPaths [ 0 ] ?? "" ) ) . toMatch ( / ^ [ ^ / \\ ] + \. o g g $ / ) ;
141+ expect ( entries ) . toEqual ( [ ] ) ;
142+ } ) ;
143+ } finally {
144+ vi . doUnmock ( "../infra/file-store.js" ) ;
145+ }
79146 }
80147
81148 async function expectSavedOriginalFilenameCase ( params : {
@@ -310,35 +377,7 @@ describe("media store", () => {
310377 {
311378 name : "does not leave final media artifacts when buffer writes fail" ,
312379 run : async ( ) => {
313- await withTempStore ( async ( store ) => {
314- const mediaDir = await store . ensureMediaDir ( ) ;
315- const originalWriteFile = fs . writeFile . bind ( fs ) ;
316- const attemptedPaths : string [ ] = [ ] ;
317- vi . spyOn ( fs , "writeFile" ) . mockImplementation ( async ( ...args ) => {
318- const [ filePath ] = args ;
319- if (
320- typeof filePath === "string" &&
321- filePath . includes ( `${ path . sep } failed-buffer${ path . sep } ` )
322- ) {
323- attemptedPaths . push ( filePath ) ;
324- await originalWriteFile ( filePath , Buffer . alloc ( 0 ) , args [ 2 ] ) ;
325- const err = new Error ( "no space left on device" ) as NodeJS . ErrnoException ;
326- err . code = "ENOSPC" ;
327- throw err ;
328- }
329- return await originalWriteFile ( ...args ) ;
330- } ) ;
331-
332- await expect (
333- store . saveMediaBuffer ( Buffer . from ( "voice" ) , "audio/ogg" , "failed-buffer" ) ,
334- ) . rejects . toMatchObject ( { code : "ENOSPC" } ) ;
335-
336- const failedDir = path . join ( mediaDir , "failed-buffer" ) ;
337- const entries = await fs . readdir ( failedDir ) . catch ( ( ) => [ ] ) ;
338- expect ( attemptedPaths ) . toHaveLength ( 1 ) ;
339- expect ( path . basename ( attemptedPaths [ 0 ] ?? "" ) ) . toMatch ( / ^ \. .+ \. t m p $ / ) ;
340- expect ( entries ) . toEqual ( [ ] ) ;
341- } ) ;
380+ await expectFailedBufferWriteCase ( ) ;
342381 } ,
343382 } ,
344383 {
0 commit comments