Fix Wave-2 review findings: FX union on re-park, per-project reconcile memo, shared undo mask, decayed test oracle

This commit is contained in:
2026-08-06 17:50:49 -04:00
parent c4038a9e9c
commit 024c619884
9 changed files with 244 additions and 105 deletions
+45 -69
View File
@@ -814,13 +814,13 @@ using LiveFlags = std::map<std::string, int>;
// Runs one applyMode-equivalent toggle against `vm` + `live`. `guard` selects the
// fixed behavior vs. the original buggy (snapshot-every-park) one.
// Returns nothing; mutates `vm` snapshots/active mode and `live` flags in place,
// exactly mirroring view.cpp's park then restore then setActiveMode ordering.
//
// Guarded, the park recaptures only when the model holds no record — the property
// the shell's parkTrack keeps. Its second guarded branch (a parked-looking chain
// with no record is left alone) outlived the shell rule it mirrored and is kept
// here only as a second unguarded-vs-guarded contrast; the model half of the defect
// is identical either way.
// mirroring view.cpp's park then restore then setActiveMode ordering: guarded park
// gates on holdsSnapshotFlags (parkTrack's own gate, current shell — there is no
// refusal branch any more, see view_park.h) and guarded restore spends the flag half
// through releaseSnapshotFlags (restoreTrack's own call), not a full clearSnapshot.
// The FX half of the two-half record is never exercised here (nothing sets fxHeld
// false), so it stays held throughout and never blocks a re-capture the way
// flagsHeld does — the one place this harness is narrower than the real record.
void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tree,
const std::string& target, bool guard) {
TogglePlan plan = vm.planToggle(tree, target);
@@ -830,8 +830,7 @@ void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tre
if (tp.flags.empty()) continue;
const std::string& guid = tp.flags.front().guid;
if (guard) {
if (vm.snapshot(guid) == nullptr) {
if (live[guid] == 0) continue; // reads parked, no snapshot — refuse
if (!vm.holdsSnapshotFlags(guid)) {
TrackSnapshot snap;
snap.showInTcp = live[guid];
vm.storeSnapshot(guid, snap);
@@ -844,13 +843,13 @@ void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tre
live[guid] = 0; // park hides it
}
// RESTORE: write snapshot flag back, then drop the snapshot.
// RESTORE: write snapshot flag back, then spend the flag half.
for (const auto& tp : plan.restore) {
if (tp.flags.empty()) continue;
const std::string& guid = tp.flags.front().guid;
if (const TrackSnapshot* snap = vm.snapshot(guid))
live[guid] = snap->showInTcp; // restore the captured visible flag
vm.clearSnapshot(guid);
vm.releaseSnapshotFlags(guid);
}
vm.setActiveMode(target);
@@ -939,70 +938,47 @@ static void testNestedToggleSnapshotSurvivesRepark() {
// -- 9b. Reload strand: snapshots gone, live flags still parked ---------------
//
// The pair the park refusal exists for, driven through the same harness. A model
// replaced without the project rolling back with it — an unreadable view_state, a
// snapshot reconciled away while its track was deleted, a hand or script edit —
// leaves tracks sitting at parked values with nothing recording what they were
// before. Snapshotting there commits the parked state as the user's, and the very
// next toggle writes it back over whatever they have since fixed by hand.
// A model replaced without the project rolling back with it — an unreadable
// view_state, a snapshot reconciled away while its track was deleted, a hand or
// script edit — leaves tracks sitting at parked values with nothing recording what
// they were before. The park refusal that once existed for this pair (view_fx_park's
// decidePark) was retired by the Wave-2 shell restructure: view_park.h now states
// plainly that nothing infers a park from a live value any more, so a parked-looking
// chain with no record is treated as the user's own state and captured, not refused.
// This test documents the CURRENT consequence — it is not asserting the consequence
// is desirable: a hand recovery made before the next reapply survives, but one made
// after it (the reapply has already re-captured the still-hidden value as "the
// user's") is overwritten by the next restore, converging with the always-recapture
// defect test 9's unguarded block pins. Guarded and unguarded no longer differ here.
static void testReloadedModelWithParkedTracksRefusesRatherThanResnapshotting() {
static void testReloadedModelWithParkedTracksIsCapturedNotRefused() {
const FolderTree tree = nestedTree();
// GUARDED (fixed shell).
{
ViewModeModel vm;
vm.membership().tag("{L1}", kDesignModeId);
LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};
ViewModeModel vm;
vm.membership().tag("{L1}", kDesignModeId);
LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
CHECK(vm.snapshot("{L2}") != nullptr);
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
CHECK(vm.snapshot("{L2}") != nullptr);
// The reload: the model comes back with no snapshots while the project's
// tracks are still parked.
vm.clearSnapshot("{L2}");
vm.clearSnapshot("{L3}");
// The reload: the model comes back with no snapshots while the project's
// tracks are still parked.
vm.clearSnapshot("{L2}");
vm.clearSnapshot("{L3}");
// The load tick reapplies the saved active mode. Both leaves are refused —
// nothing written, and nothing false captured.
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
CHECK(vm.snapshot("{L2}") == nullptr && vm.snapshot("{L3}") == nullptr);
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
// The load tick reapplies the saved active mode. Both leaves are captured
// fresh from their still-hidden live value — not refused.
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
CHECK(vm.snapshot("{L2}") != nullptr && vm.snapshot("{L3}") != nullptr);
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
// The documented hand recovery, on {L2} only, while still in Design.
// Toggling back must leave it where the user put it: with no snapshot
// there is nothing to restore FROM, so the restore writes nothing.
live["{L2}"] = 1;
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
CHECK(live["{L2}"] == 1); // the hand fix survives
CHECK(live["{L3}"] == 0); // never recovered — stuck, but never falsely committed
// And {L2} is back under mode control from there: the next park sees a
// clean chain, captures the user's value, and restores it.
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
CHECK(live["{L2}"] == 0 && vm.snapshot("{L2}") != nullptr);
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
CHECK(live["{L2}"] == 1);
}
// UNGUARDED (the defect): the reapply after the reload recaptures the parked
// zero, so the very next toggle writes it back over the hand recovery.
{
ViewModeModel vm;
vm.membership().tag("{L1}", kDesignModeId);
LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}};
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false);
vm.clearSnapshot("{L2}");
vm.clearSnapshot("{L3}");
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false);
live["{L2}"] = 1; // the same hand recovery
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/false);
CHECK(live["{L2}"] == 0); // wiped — the false snapshot won
}
// A hand recovery made NOW, after that recapture, is not what the next restore
// reads back — the fresh (hidden) capture just above is, so it is lost.
live["{L2}"] = 1;
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
CHECK(live["{L2}"] == 0); // wiped by the restore
CHECK(live["{L3}"] == 0); // never recovered
}
// ===========================================================================
@@ -2190,7 +2166,7 @@ int main() {
testUntaggedLeavesManagedByModeSystem();
testTaggedLeafBehaviorUnchangedWithUntagged();
testNestedToggleSnapshotSurvivesRepark();
testReloadedModelWithParkedTracksRefusesRatherThanResnapshotting();
testReloadedModelWithParkedTracksIsCapturedNotRefused();
testReconcilePrunesOrphanedSnapshots();
testReconcileFullLiveSetIsNoOp();
testReconcileThenReparkLifecycleIntact();