Engine: a headless call that fails for want of a file is not recorded

file.open with no file succeeds by queuing a dialog, which execute_headless
turns into an error for callers with no UI; the recording kept the step.
Recorder::truncate forgets it and keeps the byte count right. Regression test.
This commit is contained in:
prxnvt
2026-10-23 20:08:40 -04:00
parent a3b5f8e45c
commit 456cf73886
3 changed files with 29 additions and 0 deletions
+7
View File
@@ -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<Json> {
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`",
+9
View File
@@ -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;
}
}
+13
View File
@@ -571,6 +571,19 @@ fn recorded_ids(s: &Session) -> Vec<String> {
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();