Report an error when file commands get no file in MCP and CLI (#135)

This commit is contained in:
MarekSpodBiedry
2026-10-10 19:05:56 +02:00
parent 19094e2d62
commit fe162f8f23
4 changed files with 33 additions and 8 deletions
+1 -2
View File
@@ -145,8 +145,7 @@ impl Args {
// ---------------------------------------------------------------- helpers
fn exec(s: &mut Session, id: &str, params: Value) -> Result<Value, String> {
let r = s.execute(id, params).map_err(|e| e.to_string());
s.take_ui_requests();
let r = s.execute_headless(id, params).map_err(|e| e.to_string());
if id == "file.open"
&& let Ok(value) = &r
&& let Some(warnings) = value.get("warnings").and_then(Value::as_array)
+19
View File
@@ -311,6 +311,25 @@ impl Session {
pub fn take_ui_requests(&mut self) -> Vec<UiRequest> {
std::mem::take(&mut self.ui_requests)
}
/// Runs a command for a caller with no UI (MCP, CLI): queued UI requests are dropped, and a command that only
/// 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 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 {
let what = match id {
"file.open" => "`path` or `base64`",
"data.getData" | "data.fromTextCsv" => "`path` or `text`",
_ => "`path`",
};
let msg = format!("missing {what}: programmatic calls never open dialogs");
return Err(EngineError::BadParams { cmd: id.to_string(), msg });
}
r
}
}
/// Convenience: the active workbook's active sheet.
+2 -6
View File
@@ -34,12 +34,8 @@ impl Headless {
}
fn execute(&mut self, id: &str, params: Value) -> Result<Value, String> {
let r = self.session.execute(id, params).map_err(|e| e.to_string());
// Programmatic calls never open dialogs; drop anything a command queued for a UI.
for req in self.session.take_ui_requests() {
log::debug!("headless: ignoring UI request {req:?}");
}
r
// Programmatic calls never open dialogs; anything a command queued for a UI is dropped.
self.session.execute_headless(id, params).map_err(|e| e.to_string())
}
fn commands(&self, params: &Value) -> Value {
+11
View File
@@ -381,3 +381,14 @@ fn remote_backend_over_tcp() {
handle.join().unwrap();
assert!(Remote::connect("127.0.0.1:1").is_err());
}
#[test]
fn file_commands_without_a_file_fail() {
let mut s = server();
// `file.save` on Book1, which has no file yet, would ask for one too.
for command in ["file.open", "file.saveAs", "file.save", "data.getData", "data.fromTextCsv"] {
let msg = fails(&mut s, "execute_command", json!({"command": command}));
assert!(msg.contains("missing") && msg.contains("never open dialogs"), "{command}: {msg}");
fails(&mut s, "execute_command", json!({"command": command, "params": {}}));
}
}