fix: suppress failed event for cancelled downloads

Manual download cancellation uses the same internal error path as transfer
failures after the terminal-event ordering fix. That made the Tauri UI receive
DownloadGameFilesFailed and show a red failure state even though the user had
asked for the cancellation.

Keep a clone of the cancellation token in the download task and check it after
the transfer task returns an error. Cancelled downloads still refresh local
state and clear active operation tracking, but they no longer emit the failed
event. Real, uncancelled errors continue to send DownloadGameFilesFailed.

Add unit coverage for both branches so the UI-facing event contract stays
explicit.

Test Plan:
- just fmt
- just test
- just clippy
- git diff --check

Refs: manual cancel regression from app-state follow-up
This commit is contained in:
2026-05-21 17:17:36 +02:00
parent 9835e77e8d
commit a5307d3d6a
+48 -3
View File
@@ -321,7 +321,7 @@ pub async fn handle_download_game_files_command(
peer_whitelist,
file_peer_map,
tx_notify_ui_clone.clone(),
cancel_token,
cancel_token.clone(),
)
.await;
@@ -398,8 +398,17 @@ pub async fn handle_download_game_files_command(
}
end_download_operation(&ctx_clone, &tx_notify_ui_clone, &download_id).await;
download_state_guard.disarm();
log::error!("Download failed for {download_id}: {e}");
send_download_failed(&tx_notify_ui_clone, &download_id);
let download_was_cancelled = cancel_token.is_cancelled();
if download_was_cancelled {
log::info!("Download cancelled for {download_id}: {e}");
} else {
log::error!("Download failed for {download_id}: {e}");
}
send_download_failed_unless_cancelled(
&tx_notify_ui_clone,
&download_id,
download_was_cancelled,
);
}
}
});
@@ -787,6 +796,19 @@ fn send_download_failed(tx_notify_ui: &UnboundedSender<PeerEvent>, id: &str) {
}
}
fn send_download_failed_unless_cancelled(
tx_notify_ui: &UnboundedSender<PeerEvent>,
id: &str,
cancelled: bool,
) -> bool {
if cancelled {
return false;
}
send_download_failed(tx_notify_ui, id);
true
}
async fn end_download_operation(ctx: &Ctx, tx_notify_ui: &UnboundedSender<PeerEvent>, id: &str) {
end_operation(ctx, tx_notify_ui, id).await;
clear_active_download(ctx, id).await;
@@ -1097,6 +1119,29 @@ mod tests {
)
}
#[test]
fn cancelled_download_error_does_not_emit_failed_event() {
let (tx, mut rx) = mpsc::unbounded_channel();
let emitted = send_download_failed_unless_cancelled(&tx, "game", true);
assert!(!emitted);
assert!(rx.try_recv().is_err());
}
#[test]
fn uncancelled_download_error_emits_failed_event() {
let (tx, mut rx) = mpsc::unbounded_channel();
let emitted = send_download_failed_unless_cancelled(&tx, "game", false);
assert!(emitted);
assert!(matches!(
rx.try_recv(),
Ok(PeerEvent::DownloadGameFilesFailed { id }) if id == "game"
));
}
async fn recv_event(rx: &mut mpsc::UnboundedReceiver<PeerEvent>) -> PeerEvent {
tokio::time::timeout(Duration::from_secs(1), rx.recv())
.await