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:
@@ -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();
|
||||
|
||||
@@ -91,6 +91,47 @@ static void testAReapplyThatWroteNothingLeavesNoPointAtAll() {
|
||||
CHECK(!applyMintsUndoPoint(/*realSwitch=*/false, /*wroteAnything=*/false));
|
||||
}
|
||||
|
||||
// -- the re-park union: a plugin added between a restore and the next park --------
|
||||
//
|
||||
// Sequence under test: park -> restore -> user adds plugin P -> re-park. Without the
|
||||
// union, the record's prior verbatim-keep would drop P silently -- the reclaimer
|
||||
// offlines it with everything else, and no snapshot ever names it to bring it back.
|
||||
|
||||
static void testAPluginAddedWhileRestoredJoinsTheUnionRatherThanBeingDropped() {
|
||||
const std::vector<FxOfflineState> prior{
|
||||
FxOfflineState{"{FX-A}", 1}, FxOfflineState{"{FX-B}", 0}};
|
||||
const std::vector<FxOfflineState> live{
|
||||
FxOfflineState{"{FX-A}", 0}, FxOfflineState{"{FX-B}", 0}, FxOfflineState{"{FX-P}", 0}};
|
||||
|
||||
const std::vector<FxOfflineState> merged = unionFxOfflineOnRepark(prior, live);
|
||||
|
||||
CHECK(merged.size() == 3);
|
||||
bool foundA = false, foundB = false, foundP = false;
|
||||
for (const FxOfflineState& fx : merged) {
|
||||
if (fx.fxGuid == "{FX-A}") { foundA = true; CHECK(fx.offline == 1); } // record wins
|
||||
if (fx.fxGuid == "{FX-B}") { foundB = true; CHECK(fx.offline == 0); } // record wins
|
||||
if (fx.fxGuid == "{FX-P}") { foundP = true; CHECK(fx.offline == 0); } // added from live
|
||||
}
|
||||
CHECK(foundA && foundB && foundP);
|
||||
}
|
||||
|
||||
static void testAPluginRemovedWhileRestoredStaysInTheUnionForResolveFxRestoreToDrop() {
|
||||
// The union is not where a removed plugin's entry disappears -- resolveFxRestore
|
||||
// owns that at restore time (missingIdentity, reported), so the merge must keep
|
||||
// naming it rather than quietly forgetting it here.
|
||||
const std::vector<FxOfflineState> prior{
|
||||
FxOfflineState{"{FX-A}", 1}, FxOfflineState{"{FX-REMOVED}", 1}};
|
||||
const std::vector<FxOfflineState> live{FxOfflineState{"{FX-A}", 0}};
|
||||
|
||||
const std::vector<FxOfflineState> merged = unionFxOfflineOnRepark(prior, live);
|
||||
|
||||
CHECK(merged.size() == 2);
|
||||
bool foundRemoved = false;
|
||||
for (const FxOfflineState& fx : merged)
|
||||
if (fx.fxGuid == "{FX-REMOVED}") foundRemoved = true;
|
||||
CHECK(foundRemoved);
|
||||
}
|
||||
|
||||
int main() {
|
||||
testEachDrivenFlagAddressesItsOwnReaperParameter();
|
||||
testNoDrivenFlagCanEverAddressMuteOrSolo();
|
||||
@@ -100,6 +141,9 @@ int main() {
|
||||
testAReapplyThatWroteSomethingLeavesAPoint();
|
||||
testAReapplyThatWroteNothingLeavesNoPointAtAll();
|
||||
|
||||
testAPluginAddedWhileRestoredJoinsTheUnionRatherThanBeingDropped();
|
||||
testAPluginRemovedWhileRestoredStaysInTheUnionForResolveFxRestoreToDrop();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "../src/shell/view/view_reconcile.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <map>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -144,6 +145,48 @@ static void testASpentFlagHalfIsNotARecordToRepairFrom() {
|
||||
CHECK(planReconcile(view::appliedState(applied), view::appliedState(reloaded)).empty());
|
||||
}
|
||||
|
||||
// -- the per-project memo: a tab switch must not erase another project's account --
|
||||
|
||||
static void testASecondProjectNotedInBetweenDoesNotEraseTheFirstProjectsMemo() {
|
||||
// The bug this replaced: a single global slot meant noting project B's applied
|
||||
// state overwrote project A's entirely, so a repair for A run afterward found no
|
||||
// memo to compare against and silently skipped -- exactly what a tab-switch then
|
||||
// an undo on the ORIGINAL project produces within one timer period.
|
||||
ReaProject* const projA = reinterpret_cast<ReaProject*>(1);
|
||||
ReaProject* const projB = reinterpret_cast<ReaProject*>(2);
|
||||
|
||||
ViewModeModel appliedA;
|
||||
appliedA.setActiveMode(kDesignModeId);
|
||||
appliedA.storeSnapshot("{A}", captured(1, 1, 1, 1));
|
||||
|
||||
const ViewModeModel appliedB; // untouched — B has nothing driven
|
||||
|
||||
std::map<ReaProject*, view::AppliedState> memo;
|
||||
memo[projA] = view::appliedState(appliedA);
|
||||
memo[projB] = view::appliedState(appliedB); // noted in between; must not clobber A
|
||||
|
||||
const ViewModeModel reloadedA; // A's park record dropped, as an undo would leave it
|
||||
|
||||
const ProjectReconcileResult result =
|
||||
planReconcilePerProject(memo, projA, view::appliedState(reloadedA));
|
||||
|
||||
CHECK(result.ops.size() == 4);
|
||||
CHECK(hasOp(result.ops, "{A}", Flag::MainSend, 1));
|
||||
}
|
||||
|
||||
static void testAProjectWithNoPriorMemoGetsNoRepair() {
|
||||
// "A project we hold no memo for gets no repair" -- the tool has no account of
|
||||
// driving anything there, so it has no opinion (see the header's SCOPE note).
|
||||
ReaProject* const projC = reinterpret_cast<ReaProject*>(3);
|
||||
const std::map<ReaProject*, view::AppliedState> memo; // never noted
|
||||
|
||||
const ViewModeModel reloaded;
|
||||
const ProjectReconcileResult result =
|
||||
planReconcilePerProject(memo, projC, view::appliedState(reloaded));
|
||||
|
||||
CHECK(result.ops.empty());
|
||||
}
|
||||
|
||||
int main() {
|
||||
testAnUndoThatLeftOurAppliedOutputAloneRepairsNothing();
|
||||
testADriftedActiveModeAloneIsNotAReasonToDriveATrack();
|
||||
@@ -153,6 +196,9 @@ int main() {
|
||||
testATrackNeitherStateRecordsIsNeverDrivenAtAll();
|
||||
testASpentFlagHalfIsNotARecordToRepairFrom();
|
||||
|
||||
testASecondProjectNotedInBetweenDoesNotEraseTheFirstProjectsMemo();
|
||||
testAProjectWithNoPriorMemoGetsNoRepair();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user