From d309c85c3193b1f199bcdeb977997cc4a660c64b Mon Sep 17 00:00:00 2001 From: prxnvt <53838599+prxnvt@users.noreply.github.com> Date: Sun, 25 Oct 2026 02:52:43 -0400 Subject: [PATCH] Engine: Stop Action waits while a macro is being debugged `automate.stopAction` rolls back the undo groups open on the workbook. While a macro was being debugged it rolled back the debugger's own group and left the debugger set, pointing at a group that was gone. It is now refused under the debugger's lock with "Finish or stop the macro first", as edits are; `macro.debug {action: "stop"}` ends the run. - Test: `stop_action_waits_while_a_macro_is_being_debugged`. --- crates/engine/src/lib.rs | 5 ++++- crates/engine/src/tests/macros.rs | 16 ++++++++++++++++ docs/macros.md | 5 +++-- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 844494ec5..19e76227a 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -615,7 +615,10 @@ impl Session { // recording, so stopping again after the run keeps what was recorded. let stops_record_actions = id == "automate.recordActions" && (params.get("stop").and_then(Json::as_bool) == Some(true) || self.recording.is_some()); - if (spec.undoable || stops_record_actions) && id != "macro.debug" && locked { + // Stop Action rolls back the open groups, the debugger's among them, and would leave the + // debugger pointing at a group that is gone: stopping the macro is the debugger's own. + let stops_groups = id == "automate.stopAction"; + if (spec.undoable || stops_record_actions || stops_groups) && id != "macro.debug" && locked { return Err(EngineError::Disabled(id.to_string(), macros::debug::BUSY.into())); } let before = self.active().map(|d| (d.uid, d.wb.clone(), d.selection.clone())); diff --git a/crates/engine/src/tests/macros.rs b/crates/engine/src/tests/macros.rs index 0082f783d..7f5adf0f9 100644 --- a/crates/engine/src/tests/macros.rs +++ b/crates/engine/src/tests/macros.rs @@ -295,6 +295,22 @@ fn shifting_makes_room_and_is_checked_again() { assert!(Arc::ptr_eq(&s.doc().unwrap().wb, &start)); } +#[test] +fn stop_action_waits_while_a_macro_is_being_debugged() { + // Stop Action rolls back the open undo groups; the debugger's is one, so it waits for the + // debugger, which stops the macro itself. + let mut s = s(); + data(&mut s); + put(&mut s, totals()); + s.execute("macro.debug", json!({"action": "start", "name": "Totals", "inputs": "B2:C5", "breakpoints": ["1"]})).unwrap(); + let r = s.execute("automate.stopAction", json!({})); + assert!(matches!(&r, Err(EngineError::Disabled(_, why)) if why == crate::macros::debug::BUSY), "{r:?}"); + assert!(s.macro_debug.is_some(), "the debugger is still there"); + s.execute("macro.debug", json!({"action": "stop"})).unwrap(); + assert!(s.macro_debug.is_none()); + s.execute("automate.stopAction", json!({})).unwrap(); +} + #[test] fn closing_the_debugged_workbook_ends_the_debugger() { // `file.close` is not an edit, so the debugger's lock lets it through; the group went with diff --git a/docs/macros.md b/docs/macros.md index a126c80d2..4006096a7 100644 --- a/docs/macros.md +++ b/docs/macros.md @@ -150,8 +150,9 @@ on, as after any recalculation. be given with any action; `breakpointsOnly: true` makes the list replace the steps' own flags. The run stays one undo step; while it is open, edits of the workbooks it touched (its own, and any other its steps changed) are refused with "Finish or stop the macro first", also when another -command makes them (stopping Record Actions saves a script), and other workbooks stay -editable. Each answer gives the next step with its parameters as they would run, the parameters +command makes them (stopping Record Actions saves a script), and so is Automate › Stop Action, +which would roll the run's undo group back under the debugger; `stop` ends the run instead. Other +workbooks stay editable. Each answer gives the next step with its parameters as they would run, the parameters of the step that just ran, and the cells changed so far. ## Storage