@@ -23,7 +23,7 @@ import {
2323 mcpAwaitItem ,
2424 mcpSwitchPane ,
2525} from '../e2e-shared/mcp-client.js'
26- import { ensureAppReady , getFixtureRoot , sleep , TRANSFER_DIALOG } from './helpers.js'
26+ import { ensureAppReady , getFixtureRoot , pollUntil , sleep , TRANSFER_DIALOG } from './helpers.js'
2727import {
2828 waitForConflictPolicy ,
2929 selectConflictPolicy ,
@@ -34,6 +34,30 @@ import {
3434const INTERNAL_STORAGE = 'Virtual Pixel 9 - Internal Storage'
3535const LOCAL_VOLUME_NAME = os . platform ( ) === 'linux' ? 'Root' : 'Macintosh HD'
3636
37+ /** True when both panes show the local volume in cmdr://state. */
38+ async function bothPanesOnLocalVolume ( ) : Promise < boolean > {
39+ const state = await mcpReadResource ( 'cmdr://state' )
40+ const volumeLines = ( state . match ( / \n { 2 } v o l u m e : ( [ ^ \n ] + ) / g) ?? [ ] ) . map ( ( line ) => line . replace ( / ^ \n { 2 } v o l u m e : / , '' ) )
41+ return volumeLines . length >= 2 && volumeLines [ 0 ] === LOCAL_VOLUME_NAME && volumeLines [ 1 ] === LOCAL_VOLUME_NAME
42+ }
43+
44+ /**
45+ * Polls a sync filesystem predicate until it returns true or timeout is reached.
46+ * Used to wait for MTP / cross-volume operations to settle on disk.
47+ */
48+ async function pollFs (
49+ tauriPage : Parameters < typeof pollUntil > [ 0 ] ,
50+ predicate : ( ) => boolean ,
51+ timeoutMs = 15000 ,
52+ ) : Promise < boolean > {
53+ return pollUntil (
54+ tauriPage ,
55+ // eslint-disable-next-line @typescript-eslint/require-await -- pollUntil expects a Promise<boolean>
56+ async ( ) => predicate ( ) ,
57+ timeoutMs ,
58+ )
59+ }
60+
3761/** Discovers the mtp:// path prefix for a named MTP storage from cmdr://state. */
3862async function getMtpVolumePath ( storageName : string ) : Promise < string > {
3963 const state = await mcpReadResource ( 'cmdr://state' )
@@ -67,11 +91,10 @@ test.beforeEach(async ({ tauriPage }) => {
6791 invoke('plugin:event|emit', { event: 'mcp-volume-select', payload: { pane: 'left', name: '${ LOCAL_VOLUME_NAME } ' } });
6892 invoke('plugin:event|emit', { event: 'mcp-volume-select', payload: { pane: 'right', name: '${ LOCAL_VOLUME_NAME } ' } });
6993 })()` )
70- await sleep ( 2000 )
94+ await pollUntil ( tauriPage , async ( ) => bothPanesOnLocalVolume ( ) , 5000 )
7195 await tauriPage . keyboard . press ( 'Escape' )
72- await sleep ( 200 )
7396 await tauriPage . keyboard . press ( 'Escape' )
74- await sleep ( 200 )
97+ await pollUntil ( tauriPage , async ( ) => ! ( await tauriPage . isVisible ( '.modal-overlay' ) ) , 2000 )
7598} )
7699
77100// ── Cross-volume move conflicts (MTP ↔ local) ──────────────────────────────
@@ -100,15 +123,25 @@ test.describe('MTP cross-volume move conflicts', () => {
100123 await clickTransferStart ( tauriPage )
101124 await waitForDialogsToClose ( tauriPage , 30000 )
102125
103- // Wait for MTP operation
104- await sleep ( 3000 )
126+ // Wait for the MTP operation to settle on disk: dest contains MTP content AND source removed.
127+ const destPath = path . join ( fixtureRoot , 'right' , 'report.txt' )
128+ const srcPath = path . join ( MTP_FIXTURE_ROOT , 'internal' , 'Documents' , 'report.txt' )
129+ await pollFs (
130+ tauriPage ,
131+ ( ) => {
132+ if ( fs . existsSync ( srcPath ) ) return false
133+ if ( ! fs . existsSync ( destPath ) ) return false
134+ return fs . readFileSync ( destPath , 'utf-8' ) . includes ( 'Quarterly report' )
135+ } ,
136+ 15000 ,
137+ )
105138
106139 // Dest should have MTP content (overwritten)
107- const destContent = fs . readFileSync ( path . join ( fixtureRoot , 'right' , 'report.txt' ) , 'utf-8' )
140+ const destContent = fs . readFileSync ( destPath , 'utf-8' )
108141 expect ( destContent ) . toContain ( 'Quarterly report' )
109142
110143 // Source should be deleted from MTP
111- expect ( fs . existsSync ( path . join ( MTP_FIXTURE_ROOT , 'internal' , 'Documents' , 'report.txt' ) ) ) . toBe ( false )
144+ expect ( fs . existsSync ( srcPath ) ) . toBe ( false )
112145 } )
113146
114147 test ( 'MTP-to-local move with skip preserves both files' , async ( { tauriPage } ) => {
@@ -132,8 +165,6 @@ test.describe('MTP cross-volume move conflicts', () => {
132165 await clickTransferStart ( tauriPage )
133166 await waitForDialogsToClose ( tauriPage , 30000 )
134167
135- await sleep ( 3000 )
136-
137168 // Dest unchanged (skip)
138169 const destContent = fs . readFileSync ( path . join ( fixtureRoot , 'right' , 'report.txt' ) , 'utf-8' )
139170 expect ( destContent ) . toBe ( 'local-version' )
@@ -164,15 +195,27 @@ test.describe('MTP cross-volume move conflicts', () => {
164195 await clickTransferStart ( tauriPage )
165196 await waitForDialogsToClose ( tauriPage , 30000 )
166197
167- await sleep ( 3000 )
198+ // Wait for the move to settle on disk: MTP file has overwritten content AND source removed.
199+ const mtpDest = path . join ( MTP_FIXTURE_ROOT , 'internal' , 'file-a.txt' )
200+ const localSrc = path . join ( fixtureRoot , 'left' , 'file-a.txt' )
201+ const expectedContent = 'A' . repeat ( 1024 )
202+ await pollFs (
203+ tauriPage ,
204+ ( ) => {
205+ if ( fs . existsSync ( localSrc ) ) return false
206+ if ( ! fs . existsSync ( mtpDest ) ) return false
207+ return fs . readFileSync ( mtpDest , 'utf-8' ) === expectedContent
208+ } ,
209+ 15000 ,
210+ )
168211 await mcpCall ( 'refresh' , { } )
169212
170213 // MTP file should have local content (overwritten) — local fixture is 1024 'A' chars
171- const mtpContent = fs . readFileSync ( path . join ( MTP_FIXTURE_ROOT , 'internal' , 'file-a.txt' ) , 'utf-8' )
172- expect ( mtpContent ) . toBe ( 'A' . repeat ( 1024 ) )
214+ const mtpContent = fs . readFileSync ( mtpDest , 'utf-8' )
215+ expect ( mtpContent ) . toBe ( expectedContent )
173216
174217 // Local source should be gone (moved)
175- expect ( fs . existsSync ( path . join ( fixtureRoot , 'left' , 'file-a.txt' ) ) ) . toBe ( false )
218+ expect ( fs . existsSync ( localSrc ) ) . toBe ( false )
176219 } )
177220} )
178221
@@ -212,15 +255,26 @@ test.describe('MTP same-volume move conflicts', () => {
212255 await clickTransferStart ( tauriPage )
213256 await waitForDialogsToClose ( tauriPage , 30000 )
214257
215- await sleep ( 3000 )
258+ // Wait for the same-volume MTP move to settle on disk.
259+ const rootPath = path . join ( MTP_FIXTURE_ROOT , 'internal' , 'report.txt' )
260+ const docsSrc = path . join ( MTP_FIXTURE_ROOT , 'internal' , 'Documents' , 'report.txt' )
261+ await pollFs (
262+ tauriPage ,
263+ ( ) => {
264+ if ( fs . existsSync ( docsSrc ) ) return false
265+ if ( ! fs . existsSync ( rootPath ) ) return false
266+ return fs . readFileSync ( rootPath , 'utf-8' ) . includes ( 'Quarterly report' )
267+ } ,
268+ 15000 ,
269+ )
216270 await mcpCall ( 'refresh' , { } )
217271
218272 // Root report.txt should have Documents content (overwritten)
219- const rootContent = fs . readFileSync ( path . join ( MTP_FIXTURE_ROOT , 'internal' , 'report.txt' ) , 'utf-8' )
273+ const rootContent = fs . readFileSync ( rootPath , 'utf-8' )
220274 expect ( rootContent ) . toContain ( 'Quarterly report' )
221275
222276 // Source should be gone from Documents
223- expect ( fs . existsSync ( path . join ( MTP_FIXTURE_ROOT , 'internal' , 'Documents' , 'report.txt' ) ) ) . toBe ( false )
277+ expect ( fs . existsSync ( docsSrc ) ) . toBe ( false )
224278 } )
225279
226280 test ( 'same-volume MTP move with skip preserves both files' , async ( { tauriPage } ) => {
@@ -253,8 +307,6 @@ test.describe('MTP same-volume move conflicts', () => {
253307 await clickTransferStart ( tauriPage )
254308 await waitForDialogsToClose ( tauriPage , 30000 )
255309
256- await sleep ( 3000 )
257-
258310 // Root file unchanged (skip)
259311 const rootContent = fs . readFileSync ( path . join ( MTP_FIXTURE_ROOT , 'internal' , 'report.txt' ) , 'utf-8' )
260312 expect ( rootContent ) . toBe ( 'root-version' )
0 commit comments