diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 9f79dd37f..bf1100082 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -826,10 +826,17 @@ impl Session { /// queued a file dialog because it got no file (`file.open`, `file.saveAs`, `file.save` on a workbook with no /// file yet, `data.getData`) is an error instead of a silent success. pub fn execute_headless(&mut self, id: &str, params: Json) -> Result { + let recorded = self.recorder.as_ref().map(|r| (r.steps.len(), r.dropped)); let r = self.execute(id, params); let requests = self.take_ui_requests(); let asked_for_file = requests.iter().any(|q| matches!(q, UiRequest::Dialog(name, _) if name == "open" || name == "saveAs")); if r.is_ok() && asked_for_file { + // The caller gets an error, so the recording keeps no step for the command. + if let Some((steps, dropped)) = recorded + && let Some(rec) = self.recorder.as_mut() + { + rec.truncate(steps, dropped); + } let what = match id { "file.open" => "`path` or `base64`", "data.getData" | "data.fromTextCsv" => "`path` or `text`", diff --git a/crates/engine/src/record.rs b/crates/engine/src/record.rs index fefd07234..5b8100ed4 100644 --- a/crates/engine/src/record.rs +++ b/crates/engine/src/record.rs @@ -58,4 +58,13 @@ impl Recorder { self.dropped = self.dropped.saturating_add(1); } } + + /// Forgets the steps after the first `steps` and puts the dropped count back to `dropped`, + /// for a command the caller received as failed after it was recorded. + pub fn truncate(&mut self, steps: usize, dropped: usize) { + for step in self.steps.drain(steps.min(self.steps.len())..) { + self.bytes = self.bytes.saturating_sub(serde_json::to_vec(&step).map_or(0, |v| v.len())); + } + self.dropped = dropped; + } } diff --git a/crates/engine/src/tests/seams.rs b/crates/engine/src/tests/seams.rs index dedd5c67b..8a87279d3 100644 --- a/crates/engine/src/tests/seams.rs +++ b/crates/engine/src/tests/seams.rs @@ -571,6 +571,19 @@ fn recorded_ids(s: &Session) -> Vec { s.recorder.as_ref().unwrap().steps.iter().map(|st| st.id.clone()).collect() } +#[test] +fn a_headless_call_that_fails_for_want_of_a_file_is_not_recorded() { + // `file.open` with no file queues a dialog and succeeds; a headless caller gets an error for + // it instead, so the recording keeps no step for it either. + let mut s = s(); + s.start_recording(); + set(&mut s, "A1", "1"); + assert!(s.execute_headless("file.open", json!({})).is_err()); + assert_eq!(recorded_ids(&s), ["cell.set"]); + let rec = s.stop_recording().unwrap(); + assert_eq!(rec.dropped, 0); +} + #[test] fn the_recorder_keeps_top_level_calls_with_where_they_ran() { let mut s = s();