From 8a5da53870d57347bcad2715f21f6b27ceb39efe Mon Sep 17 00:00:00 2001 From: NetterSilver <133535268+Silvertree2010@users.noreply.github.com> Date: Wed, 14 Oct 2026 19:03:00 +0000 Subject: [PATCH 1/2] Reject invalid send slot in automation.copy_to_send Found while testing crates/engine/src/cmd/mix.rs:697 Co-Authored-By: Claude Haiku 5.5 Co-Authored-By: Claude Sonnet 5.5 --- crates/engine/src/cmd/mix.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/cmd/mix.rs b/crates/engine/src/cmd/mix.rs index c0569429..f72f9ca0 100644 --- a/crates/engine/src/cmd/mix.rs +++ b/crates/engine/src/cmd/mix.rs @@ -694,7 +694,9 @@ fn clip_gain_to_vol(e: &mut Engine, p: &Value) -> Result { fn copy_to_send(e: &mut Engine, p: &Value) -> Result { let tracks = tracks_required(e, "automation.copy_to_send", p)?; - let slot = slot_of(p, "send", SEND_SLOTS).unwrap_or(0) as u8; + let slot = slot_of(p, "send", SEND_SLOTS).ok_or_else(|| { + bad("automation.copy_to_send", format!("`send` must be 0..{} or a..{}", SEND_SLOTS - 1, (b'a' + SEND_SLOTS as u8 - 1) as char)) + })? as u8; let s = e.session_mut(); for t in &tracks { if let Some(tr) = s.track_mut(*t) { @@ -874,6 +876,23 @@ mod tests { assert_eq!(e.session().tracks[0].mixer.inserts[0].as_ref().unwrap().preset, "Saved"); } + #[test] + fn copy_to_send_rejects_an_invalid_send_slot() { + let mut e = crate::demo::demo_engine(); + let history = e.undo_history().len(); + let lanes = |e: &Engine| e.session().tracks.iter().filter(|t| t.lane(&AutoParam::SendLevel(0)).is_some_and(|l| !l.points.is_empty())).count(); + let before = lanes(&e); + for bad_params in [json!({"track": "Kick", "send": 99}), json!({"track": "Kick", "send": "zz"}), json!({"track": "Kick"})] { + let r = e.execute("automation.copy_to_send", &bad_params); + assert!(r.is_err(), "{bad_params} accepted: {r:?}"); + assert_eq!(e.undo_history().len(), history, "{bad_params} pushed an undo step"); + assert_eq!(lanes(&e), before, "{bad_params} edited Send A"); + } + // A valid slot still copies. + e.execute("automation.copy_to_send", &json!({"track": "Kick", "send": "a"})).unwrap(); + assert_eq!(lanes(&e), before + 1); + } + #[test] fn retargeting_a_send_keeps_its_settings() { let mut e = Engine::default(); From f53051a35f0421df7330f93c1d5a46c74e996e66 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Mon, 19 Oct 2026 02:42:40 -0400 Subject: [PATCH 2/2] copy_to_send: no send still means Send A, as the menu item calls it Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/mix.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/engine/src/cmd/mix.rs b/crates/engine/src/cmd/mix.rs index f72f9ca0..f5f50682 100644 --- a/crates/engine/src/cmd/mix.rs +++ b/crates/engine/src/cmd/mix.rs @@ -694,9 +694,13 @@ fn clip_gain_to_vol(e: &mut Engine, p: &Value) -> Result { fn copy_to_send(e: &mut Engine, p: &Value) -> Result { let tracks = tracks_required(e, "automation.copy_to_send", p)?; - let slot = slot_of(p, "send", SEND_SLOTS).ok_or_else(|| { - bad("automation.copy_to_send", format!("`send` must be 0..{} or a..{}", SEND_SLOTS - 1, (b'a' + SEND_SLOTS as u8 - 1) as char)) - })? as u8; + // No `send` means Send A (the menu item passes none); a given one must be valid. + let slot = match p.get("send") { + None | Some(Value::Null) => 0, + Some(_) => slot_of(p, "send", SEND_SLOTS).ok_or_else(|| { + bad("automation.copy_to_send", format!("`send` must be 0..{} or a..{}", SEND_SLOTS - 1, (b'a' + SEND_SLOTS as u8 - 1) as char)) + })?, + } as u8; let s = e.session_mut(); for t in &tracks { if let Some(tr) = s.track_mut(*t) { @@ -882,12 +886,15 @@ mod tests { let history = e.undo_history().len(); let lanes = |e: &Engine| e.session().tracks.iter().filter(|t| t.lane(&AutoParam::SendLevel(0)).is_some_and(|l| !l.points.is_empty())).count(); let before = lanes(&e); - for bad_params in [json!({"track": "Kick", "send": 99}), json!({"track": "Kick", "send": "zz"}), json!({"track": "Kick"})] { + for bad_params in [json!({"track": "Kick", "send": 99}), json!({"track": "Kick", "send": "zz"})] { let r = e.execute("automation.copy_to_send", &bad_params); assert!(r.is_err(), "{bad_params} accepted: {r:?}"); assert_eq!(e.undo_history().len(), history, "{bad_params} pushed an undo step"); assert_eq!(lanes(&e), before, "{bad_params} edited Send A"); } + // No `send` (the menu item's call) still means Send A. + e.execute("automation.copy_to_send", &json!({"track": "Kick"})).unwrap(); + assert_eq!(lanes(&e), before + 1); // A valid slot still copies. e.execute("automation.copy_to_send", &json!({"track": "Kick", "send": "a"})).unwrap(); assert_eq!(lanes(&e), before + 1);