diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 4b8d14a4..6036ac6e 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -752,6 +752,8 @@ fn clipboard_note(s: &effectcraft_engine::Session) -> String { } pub(crate) fn run_engine(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result { + // Events already queued belong to earlier invocations (`engine.execute` defers them). + let queued = app.session.events.len(); let mut r = app.session.execute(id, params).map_err(|e| e.to_string()); if let Err(e) = &r { app.ui.status = e.clone(); @@ -759,14 +761,15 @@ pub(crate) fn run_engine(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str ctx.copy_text(clipboard_note(&app.session)); } let events = app.session.drain_events(); - for ev in events { + for (index, ev) in events.into_iter().enumerate() { match ev { effectcraft_engine::Event::Frontend { command, params } => { if let Err(e) = frontend(app, ctx, &command, params) { app.ui.status = e.clone(); // A frontend-only command's own failure is the command's result (#696: a - // workspace save that didn't reach disk reported success). - if command == id && r.is_ok() { + // workspace save that didn't reach disk reported success). An earlier + // invocation's failure only sets the status. + if index >= queued && command == id && r.is_ok() { r = Err(e); } } diff --git a/crates/ui-egui/tests/ui_workspace_focus.rs b/crates/ui-egui/tests/ui_workspace_focus.rs index 8464c1fb..e3c5a381 100644 --- a/crates/ui-egui/tests/ui_workspace_focus.rs +++ b/crates/ui-egui/tests/ui_workspace_focus.rs @@ -438,3 +438,31 @@ fn saved_workspaces_reach_the_settings_folder_and_reload() { assert_eq!(app.ui.dock, saved); std::fs::remove_dir_all(dir).unwrap(); } + +/// A frontend event left queued by an earlier invocation (`engine.execute` defers them) only +/// sets the status: its failure is not the result of a later command with the same id. +#[test] +fn an_earlier_queued_frontend_failure_is_not_a_later_commands_result() { + let store = Arc::new(MemoryConfig::default()); + let mut app = EffectcraftApp::new(Session { config: Some(store.clone()), ..Default::default() }); + let ctx = egui::Context::default(); + let invoke = |app: &mut EffectcraftApp, id: &str, params| effectcraft_ui_egui::menus::invoke(app, &ctx, id, params); + // An earlier failure, then a success. + app.session.execute("window.workspace", json!({"name": "missing-old"})).unwrap(); + invoke(&mut app, "window.workspace", json!({"name": "Minimal"})).unwrap(); + assert_eq!(app.ui.workspace, "Minimal"); + // An earlier failing delete, then a rename that reaches the store. + invoke(&mut app, "window.saveWorkspaceAs", json!({"name": "Original"})).unwrap(); + app.session.execute("window.editWorkspaces", json!({"name": "missing-old", "delete": true})).unwrap(); + invoke(&mut app, "window.editWorkspaces", json!({"name": "Original", "rename": "Renamed"})).unwrap(); + assert!(store.read("workspaces.json").unwrap().contains("Renamed")); + // Both fail: the result is the current invocation's own failure. + app.session.execute("window.workspace", json!({"name": "missing-old"})).unwrap(); + let error = invoke(&mut app, "window.workspace", json!({"name": "missing-new"})).unwrap_err(); + assert!(error.contains("missing-new") && !error.contains("missing-old"), "{error}"); + // An earlier failure of another command. + app.session.execute("window.workspace", json!({"name": "missing-old"})).unwrap(); + invoke(&mut app, "window.saveWorkspaceAs", json!({"name": "Fine"})).unwrap(); + // Its own failure with nothing queued is still the result. + assert!(invoke(&mut app, "window.workspace", json!({"name": "missing"})).is_err()); +}