1- import { existsSync , mkdirSync , mkdtempSync , readdirSync , renameSync , writeFileSync } from 'node:fs'
1+ import {
2+ existsSync ,
3+ mkdtempSync ,
4+ readdirSync ,
5+ readFileSync ,
6+ renameSync ,
7+ writeFileSync ,
8+ } from 'node:fs'
29import { tmpdir } from 'node:os'
310import { basename , dirname , join } from 'node:path'
411import type { MenuItemConstructorOptions , WebContents } from 'electron'
@@ -3570,19 +3577,41 @@ describe('browser-agent session', () => {
35703577 }
35713578 } )
35723579
3580+ it ( 'never overwrites a file that takes the allocated name before the download claims it' , async ( ) => {
3581+ const directory = mkdtempSync ( join ( tmpdir ( ) , 'sim-browser-downloads-' ) )
3582+ writeFileSync ( join ( directory , 'taken.bin' ) , 'user data' )
3583+ session = freshSession ( win , { } , undefined , {
3584+ getDirectory : ( ) => directory ,
3585+ getFreeDiskBytes : ( ) => Number . MAX_SAFE_INTEGER ,
3586+ pathExists : ( ) => false ,
3587+ } )
3588+ const contents = ( session . ensureTab ( ) . view as unknown as MockView ) . webContents
3589+ const download = mockDownloadItem ( { filename : 'taken.bin' , totalBytes : 100 } )
3590+
3591+ startMockDownload ( contents , download )
3592+
3593+ await vi . waitFor ( ( ) => expect ( download . item . cancel ) . toHaveBeenCalledOnce ( ) )
3594+ expect ( download . item . resume ) . not . toHaveBeenCalled ( )
3595+ expect ( readFileSync ( join ( directory , 'taken.bin' ) , 'utf8' ) ) . toBe ( 'user data' )
3596+ } )
3597+
35733598 it ( 'interrupts a completed download whose staging file cannot be moved into place' , async ( ) => {
35743599 const directory = mkdtempSync ( join ( tmpdir ( ) , 'sim-browser-downloads-' ) )
3600+ const moveFile = vi . fn ( ( ) =>
3601+ Promise . reject ( Object . assign ( new Error ( 'cross-device link' ) , { code : 'EXDEV' } ) )
3602+ )
35753603 session = freshSession ( win , { } , undefined , {
35763604 getDirectory : ( ) => directory ,
35773605 getFreeDiskBytes : ( ) => Number . MAX_SAFE_INTEGER ,
3606+ moveFile,
35783607 } )
35793608 const contents = ( session . ensureTab ( ) . view as unknown as MockView ) . webContents
35803609 const download = mockDownloadItem ( { filename : 'blocked.bin' , totalBytes : 100 } )
35813610
35823611 startMockDownload ( contents , download )
35833612 const stagingPath = expectOnlyStagingSavePath ( download . item , directory )
35843613 await vi . waitFor ( ( ) => expect ( download . item . resume ) . toHaveBeenCalledOnce ( ) )
3585- mkdirSync ( join ( directory , 'blocked.bin' ) )
3614+ expect ( readFileSync ( join ( directory , 'blocked.bin' ) , 'utf8' ) ) . toBe ( '' )
35863615 download . emitDone ( 'completed' )
35873616
35883617 await vi . waitFor ( ( ) =>
@@ -3591,7 +3620,9 @@ describe('browser-agent session', () => {
35913620 state : 'interrupted' ,
35923621 } )
35933622 )
3594- await vi . waitFor ( ( ) => expect ( existsSync ( stagingPath ) ) . toBe ( false ) )
3623+ await vi . waitFor ( ( ) => expect ( readdirSync ( directory ) ) . toEqual ( [ ] ) )
3624+ expect ( existsSync ( stagingPath ) ) . toBe ( false )
3625+ expect ( moveFile ) . toHaveBeenCalledOnce ( )
35953626 const { id } = session . getBrowserDownloadsState ( 'chat-test' ) . downloads [ 0 ]
35963627 expect ( session . completedBrowserDownload ( 'chat-test' , id ) ) . toBeNull ( )
35973628
@@ -3970,6 +4001,7 @@ describe('browser-agent session', () => {
39704001 } )
39714002
39724003 download . emitDone ( 'cancelled' )
4004+ await vi . waitFor ( ( ) => expect ( readdirSync ( directory ) ) . toEqual ( [ ] ) )
39734005 const replacement = mockDownloadItem ( { filename : 'stream.bin' , totalBytes : 100 } )
39744006 startMockDownload ( contents , replacement )
39754007 expect ( replacement . item . cancel ) . not . toHaveBeenCalled ( )
@@ -4118,6 +4150,7 @@ describe('browser-agent session', () => {
41184150 expect ( second . item . resume ) . not . toHaveBeenCalled ( )
41194151 expect ( session . getBrowserDownloadsState ( 'chat-test' ) . downloads ) . toEqual ( [ ] )
41204152 expect ( snapshots . get ( 'chat-test' ) ?. downloads ) . toEqual ( [ ] )
4153+ await vi . waitFor ( ( ) => expect ( finishedDownloadFiles ( directory ) ) . toEqual ( [ ] ) )
41214154
41224155 const nextContents = ( session . ensureTab ( ) . view as unknown as MockView ) . webContents
41234156 const replacement = mockDownloadItem ( { filename : 'same-name.bin' , totalBytes : 100 } )
@@ -4131,14 +4164,18 @@ describe('browser-agent session', () => {
41314164 startMockDownload ( nextContents , concurrent )
41324165 await vi . waitFor ( ( ) => expect ( concurrent . item . resume ) . toHaveBeenCalledOnce ( ) )
41334166 concurrent . emitDone ( 'completed' )
4167+ // The torn-down downloads settling late must not remove the replacement's claimed name.
41344168 await vi . waitFor ( ( ) =>
41354169 expect ( finishedDownloadFiles ( directory ) ) . toEqual ( [
41364170 expect . stringMatching ( / ^ s a m e - n a m e \( .+ \) \. b i n $ / ) ,
4171+ 'same-name.bin' ,
41374172 ] )
41384173 )
4174+ expect ( readFileSync ( join ( directory , 'same-name.bin' ) , 'utf8' ) ) . toBe ( '' )
41394175 replacement . emitDone ( 'completed' )
4140- await vi . waitFor ( ( ) => expect ( finishedDownloadFiles ( directory ) ) . toHaveLength ( 2 ) )
4141- expect ( finishedDownloadFiles ( directory ) ) . toContain ( 'same-name.bin' )
4176+ await vi . waitFor ( ( ) =>
4177+ expect ( readFileSync ( join ( directory , 'same-name.bin' ) , 'utf8' ) ) . toBe ( 'same-name.bin' )
4178+ )
41424179 await vi . waitFor ( ( ) =>
41434180 expect ( readdirSync ( directory ) . filter ( ( name ) => name . startsWith ( '.' ) ) ) . toEqual ( [ ] )
41444181 )
@@ -4208,10 +4245,13 @@ describe('browser-agent session', () => {
42084245 await vi . waitFor ( ( ) =>
42094246 expect ( finishedDownloadFiles ( directory ) ) . toEqual ( [
42104247 expect . stringMatching ( / ^ s h a r e d \( .+ \) \. b i n $ / ) ,
4248+ 'shared.bin' ,
42114249 ] )
42124250 )
42134251 second . emitDone ( 'completed' )
4214- await vi . waitFor ( ( ) => expect ( finishedDownloadFiles ( directory ) ) . toContain ( 'shared.bin' ) )
4252+ await vi . waitFor ( ( ) =>
4253+ expect ( readFileSync ( join ( directory , 'shared.bin' ) , 'utf8' ) ) . toBe ( 'shared.bin' )
4254+ )
42154255 } )
42164256
42174257 it ( 'cancels only the disposed scope and ignores its late download callbacks' , async ( ) => {
@@ -4334,7 +4374,9 @@ describe('browser-agent session', () => {
43344374 expect ( rejected . item . cancel ) . toHaveBeenCalledOnce ( )
43354375
43364376 first . emitDone ( 'completed' )
4337- await vi . waitFor ( ( ) => expect ( finishedDownloadFiles ( directory ) ) . toEqual ( [ 'first.txt' ] ) )
4377+ await vi . waitFor ( ( ) =>
4378+ expect ( readFileSync ( join ( directory , 'first.txt' ) , 'utf8' ) ) . toBe ( 'first.txt' )
4379+ )
43384380 const replacement = mockDownloadItem ( { filename : 'fourth.txt' , totalBytes : 100 } )
43394381 startMockDownload ( contents , replacement )
43404382 expect ( replacement . item . cancel ) . not . toHaveBeenCalled ( )
0 commit comments