From 5aa41703f743b5e8b2256113152c034e008d2142 Mon Sep 17 00:00:00 2001 From: Homero Thompson Date: Fri, 30 Oct 2026 05:27:06 +0000 Subject: [PATCH] Control channel: ui.home refuses a sample index that names no sample (#9087) (#9212) * claim: #9087 * fix(#9087): ui.home refuses a sample index that names no sample The control channel passed the sample index straight to open_sample, which drops an index that names no sample and says nothing. The request then replied ok, so an agent could not tell a typo from a sample that had opened. The index is checked before it reaches open_sample: an index that names no sample is refused with the valid range in the error. A sample that exists still opens, as before. --- crates/ui-egui/src/control.rs | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index db6aae908d..298107eb49 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -585,8 +585,17 @@ fn dispatch(app: &mut SolveApp, ctx: &egui::Context, method: &str, p: &Value) -> Some(false) => app.home.open = false, None => {} } - if let Some(i) = p.get("sample").and_then(Value::as_u64) { - crate::home::open_sample(app, i as usize); + if let Some(sample) = p.get("sample") { + // The index is checked here: `open_sample` drops an index that names no sample + // and says nothing, so the request would look like it had worked (#9087). + let count = solidcraft_engine::sample::samples().len(); + let Some(index) = sample.as_i64() else { + return err(format!("`sample` must be an index from 0 to {}", count - 1)); + }; + if !(0..count as i64).contains(&index) { + return err(format!("`sample` must be an index from 0 to {}, not {index}", count - 1)); + } + crate::home::open_sample(app, index as usize); } ok(json!({"open": app.home.open, "recent": app.home.recent})) } @@ -2276,4 +2285,23 @@ mod tests { assert!(h.app.dialog.is_none() && h.app.tool.is_none() && h.app.session.analysis.is_none(), "{command} started nothing"); } } + + /// #9087: a sample index that names no sample is refused, so an agent can tell a typo from + /// a sample that opened. The index is checked before it reaches `open_sample`, which would + /// otherwise drop it and reply `ok`. + #[test] + fn ui_home_refuses_a_sample_index_that_names_no_sample() { + let mut h = crate::scenario::Harness::new(); + let count = solidcraft_engine::sample::samples().len(); + for bad in [json!({"sample": count}), json!({"sample": 99}), json!({"sample": -1})] { + let r = h.call("ui.home", bad.clone()); + assert_eq!(r["ok"], false, "{bad}: {r}"); + let error = r["error"].as_str().unwrap_or_default(); + assert!(error.contains(&format!("0 to {}", count - 1)), "the error names the valid range: {bad}: {r}"); + } + // A sample that exists still opens. + let r = h.call("ui.home", json!({"sample": 0})); + assert_eq!(r["ok"], true, "{r}"); + assert_eq!(r["result"]["open"], true, "the sample was asked for: {r}"); + } }