fix(blocks): prevent circular block nesting and cut render cycles

Introduce would_create_cycle in block_service to prevent recursive block definitions with case-insensitive matching across block records. Enforce cycle refusal at INSERT commit in BEDIT and during block redefinition (redefine_block_raw) with a shared error message. Ensure render-time recursion truncates safely with a one-time session warning on cyclic documents.
This commit is contained in:
Karim Jerbi committed 2026-10-13 11:10:11 +01:00
1 parent 59bb6faabd
commit 9c08e8a3ed
8 files changed
+435 -24

No files matched your search

+63 -16
View File
@@ -538,14 +538,16 @@ fn locked_redefine_with_a_changed_base_moves_the_instance() {
);
// The redefine path: the same contents, but a new base point (0,20,0).
scene.redefine_block_raw(
NAME,
v(0.0, 20.0, 0.0),
vec![EntityType::Line(Line::from_points(
v(0.0, 0.0, 0.0),
v(20.0, 0.0, 0.0),
))],
);
assert!(scene
.redefine_block_raw(
NAME,
v(0.0, 20.0, 0.0),
vec![EntityType::Line(Line::from_points(
v(0.0, 0.0, 0.0),
v(20.0, 0.0, 0.0),
))],
)
.is_ok());
assert_close(
block_base_point(&scene.document, NAME),
[0.0, 20.0, 0.0],
@@ -616,14 +618,16 @@ fn locked_noop_redefine_leaves_the_instance_where_it_is() {
assert_close(end, [62.990381, 27.5, 0.0], 1e-6, "line end before");
// No-op redefine: identical contents, same base.
scene.redefine_block_raw(
NAME,
base,
vec![EntityType::Line(Line::from_points(
v(6.0, 4.0, 0.0),
v(16.0, 4.0, 0.0),
))],
);
assert!(scene
.redefine_block_raw(
NAME,
base,
vec![EntityType::Line(Line::from_points(
v(6.0, 4.0, 0.0),
v(16.0, 4.0, 0.0),
))],
)
.is_ok());
assert_close(
block_base_point(&scene.document, NAME),
[6.0, 4.0, 0.0],
@@ -1153,3 +1157,46 @@ fn locked_nested_insert_with_attributes_exploded_one_level() {
"outer attdef insertion point",
);
}
#[test]
fn locked_redefine_block_raw_refuses_cycle() {
let mut scene = Scene::new();
let blk_a = "REDEF_A";
let blk_b = "REDEF_B";
scene.define_block_raw(
blk_a,
Vector3::ZERO,
vec![EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(1.0, 0.0, 0.0),
))],
);
// B contains A
scene.define_block_raw(
blk_b,
Vector3::ZERO,
vec![EntityType::Insert(Insert::new(
blk_a.to_string(),
Vector3::ZERO,
))],
);
// Redefining A to contain B would create cycle A -> B -> A; must refuse
let res = scene.redefine_block_raw(
blk_a,
Vector3::ZERO,
vec![EntityType::Insert(Insert::new(
blk_b.to_string(),
Vector3::ZERO,
))],
);
assert!(res.is_err());
let err = res.unwrap_err();
assert!(
err.contains("circular dependency detected"),
"expected cycle error, got: {err}"
);
}