test: failing regression test for #267: timeline.delete of a sketch leaves later features that use its body by name (#282)
* test: reproduce #267 (timeline.delete of a sketch leaves later features that use its body by name) * fix: timeline.delete reports features that newly fail (#267) A later feature that names a deleted body (a combine targeting a deleted extrude's body) is listed under failing: [{feature, error}] in the reply instead of failing silently. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RjGw4gA5ZvKrU8MQP6sBgT --------- Co-authored-by: bherbruck <brennen.herbruck@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+37
-2
No files matched your search
@@ -21,7 +21,7 @@ pub static COMMANDS: &[CommandSpec] = &[
|
|||||||
.at("SOLID", "MODIFY")
|
.at("SOLID", "MODIFY")
|
||||||
.icon("delete")
|
.icon("delete")
|
||||||
.key("Delete")
|
.key("Delete")
|
||||||
.params("features: [id or name] (a sketch takes its dependent features with it)"),
|
.params("features: [id or name] (a sketch takes its dependent features with it) → deleted: [ids]; failing?: [{feature, error}] (later features that worked and now fail)"),
|
||||||
CommandSpec::new("timeline.rename", "Rename", rename).params("feature: id|name, name"),
|
CommandSpec::new("timeline.rename", "Rename", rename).params("feature: id|name, name"),
|
||||||
CommandSpec::new("timeline.rollback", "Move Timeline Marker", rollback).params("position: number of features to keep active (omit = end)"),
|
CommandSpec::new("timeline.rollback", "Move Timeline Marker", rollback).params("position: number of features to keep active (omit = end)"),
|
||||||
CommandSpec::new("timeline.roll_to", "Roll History Marker", rollback)
|
CommandSpec::new("timeline.roll_to", "Roll History Marker", rollback)
|
||||||
@@ -120,6 +120,9 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
if keys.is_empty() {
|
if keys.is_empty() {
|
||||||
return Err(bad(cmd, "`features` must list features"));
|
return Err(bad(cmd, "`features` must list features"));
|
||||||
}
|
}
|
||||||
|
// Features that work now; any of them failing after the delete is reported (a later
|
||||||
|
// feature that names a deleted body, #267).
|
||||||
|
let worked: std::collections::BTreeSet<u64> = s.model.results.iter().filter(|r| r.error.is_none() && !r.skipped).map(|r| r.id).collect();
|
||||||
let mut gone = Vec::new();
|
let mut gone = Vec::new();
|
||||||
for k in keys {
|
for k in keys {
|
||||||
let Some(id) = s.doc.find_feature(&k).map(|f| f.id) else {
|
let Some(id) = s.doc.find_feature(&k).map(|f| f.id) else {
|
||||||
@@ -134,7 +137,18 @@ fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
s.active_sketch = None;
|
s.active_sketch = None;
|
||||||
}
|
}
|
||||||
s.selection.clear();
|
s.selection.clear();
|
||||||
Ok(json!({"deleted": gone}))
|
s.refresh();
|
||||||
|
let failing: Vec<Value> = s
|
||||||
|
.model
|
||||||
|
.results
|
||||||
|
.iter()
|
||||||
|
.filter(|r| worked.contains(&r.id))
|
||||||
|
.filter_map(|r| r.error.as_ref().map(|e| json!({"feature": r.name, "error": e})))
|
||||||
|
.collect();
|
||||||
|
if failing.is_empty() {
|
||||||
|
return Ok(json!({"deleted": gone}));
|
||||||
|
}
|
||||||
|
Ok(json!({"deleted": gone, "failing": failing}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
fn rename(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
|||||||
@@ -2489,6 +2489,27 @@ fn special_files_are_refused_by_every_file_command() {
|
|||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Deleting a sketch takes its extrude with it; a later combine that names the extrude's body must
|
||||||
|
/// not be left failing silently: it is deleted too, or the reply reports it (#267).
|
||||||
|
#[test]
|
||||||
|
fn deleting_a_sketch_leaves_no_silently_failing_feature() {
|
||||||
|
let mut s = Session::default();
|
||||||
|
run(&mut s, "sketch.create", json!({"plane": "XY", "name": "S"}));
|
||||||
|
run(&mut s, "sketch.rectangle.two_point", json!({"p0": [0, 0], "p1": [20, 20]}));
|
||||||
|
run(&mut s, "sketch.finish", json!({}));
|
||||||
|
run(&mut s, "solid.extrude", json!({"sketch": "S", "distance": "10", "body_name": "P"}));
|
||||||
|
run(&mut s, "solid.box", json!({"length": 5, "width": 5, "height": 5, "corner": [18, 5, 0], "operation": "new_body", "body_name": "Q"}));
|
||||||
|
run(&mut s, "solid.combine", json!({"target": "P", "tools": ["Q"], "name": "JoinPQ"}));
|
||||||
|
let reply = run(&mut s, "timeline.delete", json!({"features": ["S"]}));
|
||||||
|
if let Ok(join) = s.execute("timeline.inspect", &json!({"feature": "JoinPQ"})) {
|
||||||
|
assert!(
|
||||||
|
join["error"].is_null() || reply.to_string().contains("JoinPQ"),
|
||||||
|
"JoinPQ now fails ({}) but the reply only says {reply}",
|
||||||
|
join["error"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Every body a multi-profile `new` extrude creates carries its `body_name`, not a generic BodyN (#266).
|
/// Every body a multi-profile `new` extrude creates carries its `body_name`, not a generic BodyN (#266).
|
||||||
#[test]
|
#[test]
|
||||||
fn multi_profile_extrude_names_every_body_after_body_name() {
|
fn multi_profile_extrude_names_every_body_after_body_name() {
|
||||||
|
|||||||
Reference in new issue
Block a user