CAD: undo() never rolled back variables, so a bad one bricked the recipe

checkpoint() snapshotted `features` and undo() restored `features`, but
`variables` is a separate member of CadDocument. Every caller of the documented
checkpoint -> mutate -> recompute -> undo-on-failure pattern therefore failed to
roll a variable back: the bad value stayed in the document and every later
recompute failed, which is exactly the corruption the pattern exists to prevent.
Feature `expr` bindings were unaffected only because expr lives inside
CadFeature and rode along in the features snapshot — which is why the feature
side appeared to work.

This was a kernel gap, not a GUI one: McpControl::action_set_variable has the
same sequence and was equally broken.

The undo/redo stacks now hold a {features, variables} Snapshot. Nothing here is
serialized, so no recipe version change and no golden-fixture regeneration.

Two tests, both verified to FAIL against a faithful reproduction of the bug
(undo() leaving `variables` untouched) at test_caddocument.cpp:4420 and :4441:
one covers restoring a variable's previous value, the other covers removing a
variable that did not exist before the checkpoint. Worth recording that the
first mutation attempt was NOT faithful — it dropped the restore but kept
std::move(variables) into the redo stack, which empties the map as a side effect
and made the second test pass for the wrong reason. A mutation has to reproduce
the original defect, not merely break the code.

Second defect, same area: undo() calls recompute(), which succeeds and clears
doc.error, so the reason an edit was rejected was destroyed before anything
could display it. Six sites — four in DesignPanel, two in McpControl — now carry
the message across the rollback. on_remove_variable additionally asserted
"referenced by a feature expression" as fact; it now offers that as the likely
cause and appends the real error, since that diagnosis is wrong for any other
failure.

Suite 141 cases / 1972 assertions (was 139/1960). GUI compiles clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tommaso Bianchi
2026-07-25 23:56:34 +02:00
co-authored by Claude Opus 5
parent 33275f2c74
commit c565d6ba86
5 changed files with 94 additions and 20 deletions
+45
View File
@@ -4503,6 +4503,51 @@ TEST_CASE("unknown identifier fails recompute", "[CadDocument][variables]")
REQUIRE_CONTAINS(doc.error, "unknown identifier");
}
// The checkpoint -> mutate -> recompute -> undo-on-failure pattern is what both the GUI and
// McpControl use to keep a bad edit out of the document. It only works if the snapshot covers
// `variables` as well as `features`: undo() used to restore features alone, so a bad variable
// survived the rollback and every later recompute failed — the document was left unusable.
TEST_CASE("undo rolls back a bad variable, not just features", "[CadDocument][variables]")
{
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude");
doc.features[sk].expr = {{"width", "w"}};
doc.variables = {{"w", "20"}};
REQUIRE(doc.recompute());
// Caller sets a variable to something unevaluable, exactly as action_set_variable does.
doc.checkpoint();
doc.variables["w"] = "nosuchvar + 1";
REQUIRE_FALSE(doc.recompute());
REQUIRE(doc.undo());
// The good value must be back...
REQUIRE(doc.variables.at("w") == "20");
// ...and, the part that actually bit, the document must still be usable.
REQUIRE(doc.recompute());
REQUIRE(doc.error.empty());
}
// A variable ADDED under a checkpoint must disappear entirely on undo, not linger with a
// stale value: the pre-mutation snapshot simply did not contain the key.
TEST_CASE("undo removes a variable that did not exist before the checkpoint", "[CadDocument][variables]")
{
CadDocument doc;
int sk = doc.add_sketch(SketchShape::Rectangle, SketchPlane::XY(), 20, 20, 10, "Sketch");
doc.add_extrude(sk, 10.0, false, BooleanMode::New, "Extrude");
REQUIRE(doc.recompute());
REQUIRE(doc.variables.empty());
doc.checkpoint();
doc.variables["bogus"] = "1/0 +"; // syntactically broken
REQUIRE_FALSE(doc.recompute());
REQUIRE(doc.undo());
REQUIRE(doc.variables.count("bogus") == 0);
REQUIRE(doc.recompute());
}
TEST_CASE("parametric recipe round-trips through serialize/deserialize", "[CadDocument][variables]")
{
using Catch::Matchers::WithinAbs;