Skip to content

Commit e21ca6d

Browse files
committed
Bugfix: MTP delete emits write-cancelled on mid-iteration cancel
The volume-delete file loop's `?` propagation of `VolumeError::Cancelled` from `delete_with_cancel` mapped to `WriteOperationError::Cancelled` and exited the function without emitting `write-cancelled`. The top-of-loop check only catches cancels that land between iterations; a cancel that arrives during the in-flight per-file call dropped the terminal event, leaving the M4 settle contract incomplete (FE got `write-settled` but no `write-cancelled`, so the dialog waited 10 s for the fallback timer). - Explicit `match` on `delete_with_cancel`'s result handles `VolumeError::Cancelled` by emitting `write-cancelled` before returning, and maps every other error as before. - Adds an E2E throttle hook (`effective_copy_throttle_ms`) in the delete loop so cancel-during-delete tests on fast virtual MTP have a deterministic window. Zero-cost outside E2E mode. - `mtp-cancel-volume-settled` spec sets a 200 ms throttle before F8 (clears in `finally`) so the cancel reliably lands while the BE is still in the delete loop, exercising the new mid-iteration emit path. End-to-end verified: Linux Docker e2e 157/157 (was 153/4), macOS playwright e2e 145/145.
1 parent 0752861 commit e21ca6d

2 files changed

Lines changed: 47 additions & 3 deletions

File tree

apps/desktop/src-tauri/src/file_system/write_operations/delete.rs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ use super::types::{
1515
};
1616
use super::volume_copy::map_volume_error;
1717
use crate::file_system::listing::caching::try_get_watched_listing;
18-
use crate::file_system::volume::Volume;
18+
use crate::file_system::volume::{Volume, VolumeError};
1919

2020
// ============================================================================
2121
// Delete implementation
@@ -714,10 +714,39 @@ pub(super) async fn delete_volume_files_with_progress_inner(
714714
});
715715
}
716716

717-
volume
717+
// E2E throttle so cancel-during-delete tests on fast virtual MTP have a
718+
// deterministic window in which to click Cancel before all files are
719+
// gone. Reuses the copy throttle knob (`set_test_throttle` exposes a
720+
// generic per-step throttle). Zero-cost outside E2E.
721+
if let Some(ms) = crate::test_mode::effective_copy_throttle_ms()
722+
&& ms > 0
723+
{
724+
tokio::time::sleep(Duration::from_millis(ms)).await;
725+
}
726+
727+
match volume
718728
.delete_with_cancel(&entry.path, Some(&state.backend_cancel))
719729
.await
720-
.map_err(|e| map_volume_error(&entry.path.display().to_string(), e))?;
730+
{
731+
Ok(()) => {}
732+
// Cancel landed mid-iteration (during the throttle sleep or inside
733+
// delete_with_cancel itself). The top-of-loop cancel check only
734+
// catches cancels that land between iterations; without this arm,
735+
// the `?` propagation would surface as a Cancelled error with no
736+
// `write-cancelled` event, breaking the M4 settle contract.
737+
Err(VolumeError::Cancelled(_)) => {
738+
events.emit_cancelled(WriteCancelledEvent {
739+
operation_id: operation_id.to_string(),
740+
operation_type: WriteOperationType::Delete,
741+
files_processed: files_done,
742+
rolled_back: false,
743+
});
744+
return Err(WriteOperationError::Cancelled {
745+
message: "Operation cancelled by user".to_string(),
746+
});
747+
}
748+
Err(e) => return Err(map_volume_error(&entry.path.display().to_string(), e)),
749+
}
721750

722751
files_done += 1;
723752
bytes_done += entry.size;

apps/desktop/test/e2e-playwright/mtp-cancel-volume-settled.spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ test.describe('MTP cancel: settle gate keeps "Cancelling…" until BE quiets dow
108108
await mcpNavToPath('left', `${mtpPath}/DCIM`)
109109
await mcpAwaitItem('left', 'cancel-00.jpg', 30)
110110

111+
// Slow each per-file delete so the Cancel click reliably lands BEFORE the
112+
// op completes. Linux's virtual MTP can blow through the 12 tiny
113+
// `cancel-*.jpg` files fast enough that without a throttle `write-complete`
114+
// fires before our Cancel click is processed and no `write-cancelled` /
115+
// `write-settled` events ever land — exactly what the test is verifying.
116+
// 200 ms × 12 = 2.4 s worst case, plenty of room for the BE-side cancel
117+
// round-trip.
118+
await tauriPage.evaluate(
119+
`window.__TAURI_INTERNALS__.invoke('set_test_throttle', { ms: 200 })`,
120+
)
121+
111122
// Subscribe to write-cancelled, write-settled, and write-complete so the
112123
// assertions can sequence events from the BE.
113124
await tauriPage.evaluate(`(async function() {
@@ -255,6 +266,10 @@ test.describe('MTP cancel: settle gate keeps "Cancelling…" until BE quiets dow
255266
)
256267
}
257268
} finally {
269+
// Always clear the throttle so it doesn't slow down following tests.
270+
await tauriPage.evaluate(
271+
`window.__TAURI_INTERNALS__.invoke('set_test_throttle', { ms: null })`,
272+
)
258273
await tauriPage.evaluate(`(async function() {
259274
const ids = ['__cancelledListenerId', '__settledListenerId', '__completeListenerId'];
260275
const events = ['write-cancelled', 'write-settled', 'write-complete'];

0 commit comments

Comments
 (0)