From aea16f2b5ab9c6508b0b0677fd2923fe1f940187 Mon Sep 17 00:00:00 2001 From: Jonas Wrede Date: Mon, 12 Oct 2026 16:59:08 +0200 Subject: [PATCH] test: failing regression test for #265: Renaming a pattern copy renumbers the remaining copies (#280) * test: reproduce #265 (Renaming a pattern copy renumbers the remaining copies) * fix: renaming a body keeps its sibling bodies' names (#265) body.rename pinned only the names of the feature's bodies before the renamed one, so unpinned pattern copies after it were renumbered into the freed name. Pin every sibling's current name. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: bherbruck Co-authored-by: Claude Opus 5.5 --- crates/engine/src/cmd/browser.rs | 5 +++-- crates/engine/src/tests.rs | 14 ++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/cmd/browser.rs b/crates/engine/src/cmd/browser.rs index acb1f4a9..710f76e7 100644 --- a/crates/engine/src/cmd/browser.rs +++ b/crates/engine/src/cmd/browser.rs @@ -445,13 +445,14 @@ fn rename_body(s: &mut Session, p: &Value) -> Result { if st.body(name).is_some() { return Err(bad(cmd, format!("another body is called `{name}`"))); } - // The body is the k-th new body of its feature; the names before it are kept as they are. + // The body is the k-th new body of its feature; its siblings keep the names they have now + // (unpinned pattern copies would otherwise be renumbered into the freed name, #265). let fid = b.feature; let siblings: Vec = st.bodies.iter().filter(|x| x.feature == fid).map(|x| x.name.clone()).collect(); let k = siblings.iter().position(|n| *n == old).unwrap_or(0); let d = s.doc_mut(); let f = d.feature_mut(fid).ok_or_else(|| bad(cmd, "the body's feature is gone"))?; - while f.body_names.len() <= k { + while f.body_names.len() < siblings.len().max(k + 1) { let i = f.body_names.len(); f.body_names.push(siblings.get(i).cloned().unwrap_or_default()); } diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 69b4f930..4cdcc6f6 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2478,6 +2478,20 @@ fn special_files_are_refused_by_every_file_command() { let _ = std::fs::remove_dir_all(&dir); } +/// Renaming one pattern copy leaves the other copies' names alone (#265). +#[test] +fn renaming_a_pattern_copy_keeps_the_other_copies_names() { + let mut s = Session::default(); + run(&mut s, "solid.box", json!({"length": 10, "width": 10, "height": 10, "operation": "new_body", "body_name": "X"})); + run(&mut s, "solid.pattern.rectangular", json!({"bodies": ["X"], "dir1": [1, 0, 0], "count1": 3, "spacing1": 20})); + run(&mut s, "body.rename", json!({"body": "X (2)", "name": "Second"})); + let third = run(&mut s, "inspect.measure", json!({"bodies": ["X (3)"]})); + assert_eq!(third["bodies"][0]["bbox"]["min"][0].as_f64().map(f64::round), Some(40.0), "X (3) is still the copy at x = 40: {third}"); + run(&mut s, "body.rename", json!({"body": "X (3)", "name": "Third"})); + let names: Vec = s.model.state().bodies.iter().map(|b| b.name.clone()).collect(); + assert_eq!(names, ["X", "Second", "Third"]); +} + /// Parameters that work are listed in the command's params doc, which is all agents see (#271). #[test] fn params_docs_list_working_parameters() {