From 6f5cebdb022c2b21c0d1960a23c5a97fe0122fdc Mon Sep 17 00:00:00 2001 From: funman300 Date: Sun, 17 May 2026 22:57:03 -0700 Subject: [PATCH] fix(engine): fire WarningToastEvent on sync pull failure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync errors were silently swallowed — the player had no feedback when a pull failed due to network issues or an expired session. Now `poll_pull_result` emits a `WarningToastEvent` with a human-readable message for every error variant, and reopens the Connect modal on auth failure so the player can re-enter credentials without navigating through Settings. Co-Authored-By: Claude Sonnet 4.6 --- solitaire_engine/src/sync_plugin.rs | 37 +++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 5 deletions(-) diff --git a/solitaire_engine/src/sync_plugin.rs b/solitaire_engine/src/sync_plugin.rs index 8cd8325..7f3fce4 100644 --- a/solitaire_engine/src/sync_plugin.rs +++ b/solitaire_engine/src/sync_plugin.rs @@ -26,8 +26,8 @@ use solitaire_sync::{merge, SyncPayload, SyncResponse}; use crate::achievement_plugin::{AchievementsResource, AchievementsStoragePath}; use crate::events::{ - GameWonEvent, InfoToastEvent, ManualSyncRequestEvent, SyncCompleteEvent, - SyncConfigureRequestEvent, + GameWonEvent, ManualSyncRequestEvent, SyncCompleteEvent, SyncConfigureRequestEvent, + WarningToastEvent, }; use crate::game_plugin::RecordingReplay; use crate::progress_plugin::{ProgressResource, ProgressStoragePath}; @@ -109,7 +109,7 @@ impl Plugin for SyncPlugin { .add_message::() .add_message::() .add_message::() - .add_message::() + .add_message::() .add_systems(Startup, start_pull) .add_systems( Update, @@ -191,7 +191,7 @@ fn poll_pull_result( progress_path: Res, mut complete_writer: MessageWriter, mut configure_sync: MessageWriter, - mut toast: MessageWriter, + mut warning_toast: MessageWriter, ) { let Some(task) = task_res.0.as_mut() else { return; @@ -245,13 +245,13 @@ fn poll_pull_result( SyncError::Serialization(_) => format!("Unexpected server response: {e}"), SyncError::UnsupportedPlatform => unreachable!("handled above"), }; + warning_toast.write(WarningToastEvent(msg.clone())); // On auth failure, reopen the Connect modal so the player can // re-enter credentials without having to navigate through Settings. // `open_sync_setup_modal` is idempotent — it ignores the event when // the modal is already on screen, so repeated pull failures don't // stack multiple modals. if matches!(e, SyncError::Auth(_)) { - toast.write(InfoToastEvent("Session expired — please reconnect".to_string())); configure_sync.write(SyncConfigureRequestEvent); } status.0 = SyncStatus::Error(msg.clone()); @@ -550,6 +550,33 @@ mod tests { ); } + #[test] + fn pull_failure_fires_warning_toast() { + use bevy::ecs::message::Messages; + let mut app = headless_app_with(FailingProvider); + let deadline = + std::time::Instant::now() + std::time::Duration::from_secs(5); + loop { + app.update(); + if matches!( + app.world().resource::().0, + SyncStatus::Error(_) + ) { + break; + } + if std::time::Instant::now() >= deadline { + break; + } + std::thread::yield_now(); + } + let msgs = app.world().resource::>(); + let mut cursor = msgs.get_cursor(); + assert!( + cursor.read(msgs).next().is_some(), + "a WarningToastEvent must fire when the pull fails" + ); + } + #[test] fn build_payload_sets_nil_user_id() { let payload = build_payload(