// Standalone tests for reasampler::ViewModeModel — no REAPER, no test framework. // Mirror of test_bank_model: iterate the hard logic outside the DAW. // // Covers (PLAN.md D1 test cases): // 1. N-mode proven — >=3 modes, membership + derivation still correct. // 2. Parent derivation — a folder with descendant leaves in different modes is // visible in each of those modes. // 3. Restore round-trip — snapshot -> park -> restore returns every driven flag to // its captured value; includes the "flag already at 0 stays 0" (no default). // 4. show-both leaf never appears in a park op-list and is visible in all modes. // 5. Unknown/stale GUID tolerated (ignore-and-prune, no crash). // 6. JSON round-trip lossless: modes + membership + show-both + snapshots + active. // 7. planToggle park path: fxOffline is empty (shell-expands-FX contract). // 9. Nested-folder toggle: the snapshot store/clear lifecycle survives a re-park // (park-while-parked) so untagged leaves return to visible after toggling back; // guards the in-DAW "all leaves hidden after toggling twice" regression. #include "../src/view_mode_model.h" #include #include #include using namespace reasampler; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // -- helpers ----------------------------------------------------------------- static bool visibleHas(const std::set& v, const std::string& g) { return v.count(g) > 0; } // Does any park TrackPlan in the plan target `guid`? static bool parkTargets(const TogglePlan& plan, const std::string& guid) { for (const auto& p : plan.park) for (const auto& f : p.flags) if (f.guid == guid) return true; return false; } // Find the single restore plan for `guid`, or nullptr. static const TrackPlan* restoreFor(const TogglePlan& plan, const std::string& guid) { for (const auto& p : plan.restore) if (!p.flags.empty() && p.flags.front().guid == guid) return &p; return nullptr; } static int flagValue(const TrackPlan& p, Flag f) { for (const auto& op : p.flags) if (op.flag == f) return op.value; return -999; // sentinel: flag absent } // -- 1. N-mode proven -------------------------------------------------------- static void testNModeRegistryAndMembership() { ViewModeModel vm; // Seeded: Arrange + Design. CHECK(vm.modes().size() == 2); CHECK(vm.modes().contains(kArrangeModeId)); CHECK(vm.modes().contains(kDesignModeId)); CHECK(vm.activeModeId() == kArrangeModeId); // Add a third mode — proves N-mode, not boolean. CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2})); CHECK(vm.modes().size() == 3); CHECK(vm.modes().contains("mixdown")); // Duplicate id and empty id are rejected without mutation. CHECK(!vm.modes().add(Mode{"mixdown", "Dup", 5})); CHECK(!vm.modes().add(Mode{"", "Empty", 6})); CHECK(vm.modes().size() == 3); // Membership across three modes. CHECK(vm.membership().tag("{A}", kArrangeModeId)); CHECK(vm.membership().tag("{D}", kDesignModeId)); CHECK(vm.membership().tag("{M}", "mixdown")); // Leaf-belongs rule holds in each mode. CHECK(vm.leafBelongsToMode("{D}", kDesignModeId)); CHECK(!vm.leafBelongsToMode("{D}", kArrangeModeId)); CHECK(vm.leafBelongsToMode("{M}", "mixdown")); CHECK(!vm.leafBelongsToMode("{M}", kDesignModeId)); // Untagged leaf defaults to Arrange, and only Arrange. CHECK(vm.leafBelongsToMode("{UNTAGGED}", kArrangeModeId)); CHECK(!vm.leafBelongsToMode("{UNTAGGED}", kDesignModeId)); // Retag moves the leaf (single-mode semantics). CHECK(vm.membership().tag("{D}", "mixdown")); CHECK(vm.leafBelongsToMode("{D}", "mixdown")); CHECK(!vm.leafBelongsToMode("{D}", kDesignModeId)); // Untag returns to the Arrange default. CHECK(vm.membership().untag("{D}")); CHECK(vm.leafBelongsToMode("{D}", kArrangeModeId)); CHECK(!vm.membership().untag("{D}")); // second untag is a no-op // setActiveMode rejects an unregistered id, accepts a registered one. CHECK(!vm.setActiveMode("nope")); CHECK(vm.activeModeId() == kArrangeModeId); CHECK(vm.setActiveMode("mixdown")); CHECK(vm.activeModeId() == "mixdown"); } // -- 2. Parent derivation ---------------------------------------------------- static void testParentDerivationMultiMode() { ViewModeModel vm; // Folder {F} holds two leaves: {L1} in Arrange (untagged default), {L2} in Design. FolderTree tree; tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true}); tree.nodes.push_back(FolderNode{"{L1}", "{F}", false}); tree.nodes.push_back(FolderNode{"{L2}", "{F}", false}); vm.membership().tag("{L2}", kDesignModeId); // {L1} stays untagged ⇒ Arrange. auto arrange = vm.visibleTracks(tree, kArrangeModeId); auto design = vm.visibleTracks(tree, kDesignModeId); // The parent is visible in BOTH modes because it has a descendant in each. CHECK(visibleHas(arrange, "{F}")); CHECK(visibleHas(design, "{F}")); // Leaves appear only in their own mode. CHECK(visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}")); CHECK(visibleHas(design, "{L2}") && !visibleHas(design, "{L1}")); // Nested folder chain: grandparent {G} > parent {F2} > leaf {L3} (Design). // The whole chain up to the root must be visible in Design. FolderTree nested; nested.nodes.push_back(FolderNode{"{G}", "", true}); nested.nodes.push_back(FolderNode{"{F2}", "{G}", true}); nested.nodes.push_back(FolderNode{"{L3}", "{F2}", false}); ViewModeModel vm2; vm2.membership().tag("{L3}", kDesignModeId); auto d2 = vm2.visibleTracks(nested, kDesignModeId); CHECK(visibleHas(d2, "{L3}")); CHECK(visibleHas(d2, "{F2}")); CHECK(visibleHas(d2, "{G}")); // In Arrange, none of the chain is visible (no Arrange leaf under it). auto a2 = vm2.visibleTracks(nested, kArrangeModeId); CHECK(!visibleHas(a2, "{L3}") && !visibleHas(a2, "{F2}") && !visibleHas(a2, "{G}")); // A parent is NEVER parked, in either mode. auto planD = vm.planToggle(tree, kDesignModeId); auto planA = vm.planToggle(tree, kArrangeModeId); CHECK(!parkTargets(planD, "{F}")); CHECK(!parkTargets(planA, "{F}")); } // -- 3. Restore round-trip (the trust anchor) -------------------------------- static void testRestoreRoundTripSnapshotValues() { // Direct planner check: park is fixed zeros; restore is snapshot verbatim. TrackSnapshot snap; snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 0; // user had it OUT of the mix for their own reason snap.fxEnable = 1; snap.fxOffline = {0, 1, 0}; // slot 1 was already offline before parking TrackPlan park = makeParkPlan("{T}", /*fxCount=*/3); CHECK(flagValue(park, Flag::ShowInTcp) == 0); CHECK(flagValue(park, Flag::ShowInMixer) == 0); CHECK(flagValue(park, Flag::MainSend) == 0); CHECK(flagValue(park, Flag::FxEnable) == 0); CHECK(park.fxOffline.size() == 3); for (const auto& op : park.fxOffline) CHECK(op.offline == true); TrackPlan restore = makeRestorePlan("{T}", snap); // Every flag returns to its CAPTURED value — not a hardcoded "on". CHECK(flagValue(restore, Flag::ShowInTcp) == 1); CHECK(flagValue(restore, Flag::ShowInMixer) == 1); CHECK(flagValue(restore, Flag::MainSend) == 0); // the "already at 0 stays 0" case CHECK(flagValue(restore, Flag::FxEnable) == 1); CHECK(restore.fxOffline.size() == 3); CHECK(restore.fxOffline[0].offline == false); CHECK(restore.fxOffline[1].offline == true); // was offline pre-park ⇒ stays offline CHECK(restore.fxOffline[2].offline == false); // A snapshot entirely at 0 must restore entirely to 0 (no default leaks in). TrackSnapshot zero; // all zeros, empty fxOffline TrackPlan rz = makeRestorePlan("{Z}", zero); CHECK(flagValue(rz, Flag::ShowInTcp) == 0); CHECK(flagValue(rz, Flag::ShowInMixer) == 0); CHECK(flagValue(rz, Flag::MainSend) == 0); CHECK(flagValue(rz, Flag::FxEnable) == 0); CHECK(rz.fxOffline.empty()); // End-to-end via planToggle: a leaf tagged Design, snapshotted, parked while in // Arrange, then restored when we toggle back to Design. ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{DES}", "", false}); vm.membership().tag("{DES}", kDesignModeId); vm.storeSnapshot("{DES}", snap); // Toggle to Arrange: {DES} is inactive ⇒ parked. auto toArrange = vm.planToggle(tree, kArrangeModeId); CHECK(parkTargets(toArrange, "{DES}")); CHECK(restoreFor(toArrange, "{DES}") == nullptr); // not restored while inactive // Toggle to Design: {DES} is active AND has a snapshot ⇒ restored from it. auto toDesign = vm.planToggle(tree, kDesignModeId); CHECK(!parkTargets(toDesign, "{DES}")); const TrackPlan* r = restoreFor(toDesign, "{DES}"); CHECK(r != nullptr); if (r) { CHECK(flagValue(*r, Flag::MainSend) == 0); // captured 0 comes back 0 CHECK(flagValue(*r, Flag::ShowInTcp) == 1); } } // -- 4. show-both leaf -------------------------------------------------------- static void testShowBothNeverParkedVisibleEverywhere() { ViewModeModel vm; CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2})); FolderTree tree; tree.nodes.push_back(FolderNode{"{SB}", "", false}); // Tag into Design, then pin show-both. vm.membership().tag("{SB}", kDesignModeId); CHECK(vm.membership().setShowBoth("{SB}", true)); CHECK(vm.membership().isShowBoth("{SB}")); // Visible in EVERY mode. CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{SB}")); CHECK(visibleHas(vm.visibleTracks(tree, kDesignModeId), "{SB}")); CHECK(visibleHas(vm.visibleTracks(tree, "mixdown"), "{SB}")); // Never parked, in any mode — even a mode it isn't tagged into. CHECK(!parkTargets(vm.planToggle(tree, kArrangeModeId), "{SB}")); CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{SB}")); CHECK(!parkTargets(vm.planToggle(tree, "mixdown"), "{SB}")); // Clearing show-both restores normal one-mode parking: now in Arrange it parks. CHECK(vm.membership().setShowBoth("{SB}", false)); CHECK(parkTargets(vm.planToggle(tree, kArrangeModeId), "{SB}")); CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{SB}")); } // -- 5. Unknown/stale GUID tolerated ----------------------------------------- static void testStaleGuidTolerated() { ViewModeModel vm; // Tag two leaves, but the tree only knows one — the other GUID is stale (its // track was deleted / restructured while parked). vm.membership().tag("{LIVE}", kDesignModeId); vm.membership().tag("{GHOST}", kDesignModeId); vm.storeSnapshot("{GHOST}", TrackSnapshot{}); // stale snapshot too FolderTree tree; tree.nodes.push_back(FolderNode{"{LIVE}", "", false}); // {GHOST} absent from the tree. // No crash; the stale GUID is simply ignored (prune-safe). auto plan = vm.planToggle(tree, kArrangeModeId); CHECK(parkTargets(plan, "{LIVE}")); // live leaf still planned CHECK(!parkTargets(plan, "{GHOST}")); // stale leaf never emitted // Visibility derivation also ignores the stale GUID without incident. auto vis = vm.visibleTracks(tree, kDesignModeId); CHECK(visibleHas(vis, "{LIVE}")); CHECK(!visibleHas(vis, "{GHOST}")); // An empty tree with tagged members: nothing planned, no crash. FolderTree empty; auto emptyPlan = vm.planToggle(empty, kDesignModeId); CHECK(emptyPlan.park.empty() && emptyPlan.restore.empty()); } // -- 6. JSON round-trip lossless --------------------------------------------- static void testJsonRoundTrip() { ViewModeModel vm; // Modes: seeded pair + a third; also an out-of-order ordinal to prove sorting // survives round-trip. CHECK(vm.modes().add(Mode{"print", "Print \"stem\"\n", 5})); CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2})); // Membership: a plain Design leaf, a show-both leaf, an Arrange leaf, and a // leaf carrying multiple modes (representable via restore; exercises the set). vm.membership().tag("{A}", kArrangeModeId); vm.membership().tag("{D}", kDesignModeId); vm.membership().tag("{SB}", "mixdown"); vm.membership().setShowBoth("{SB}", true); Membership multi; multi.modeIds = {kDesignModeId, "mixdown"}; multi.showBoth = false; CHECK(vm.membership().restore("{MULTI}", multi)); // Snapshots: one full, one with a per-FX vector, including the tricky 0-values. TrackSnapshot s1; s1.showInTcp = 1; s1.showInMixer = 0; s1.mainSend = 1; s1.fxEnable = 0; s1.fxOffline = {1, 0, 1, 1}; vm.storeSnapshot("{D}", s1); TrackSnapshot s2; // all zeros, empty fx vector vm.storeSnapshot("{A}", s2); // Active mode set to a non-default. CHECK(vm.setActiveMode("mixdown")); std::string json = vm.serialize(); auto back = ViewModeModel::deserialize(json); CHECK(back.has_value()); CHECK(back && *back == vm); // String form is stable across a second round-trip. if (back) CHECK(back->serialize() == json); // Spot-check the load-bearing bits survived. if (back) { CHECK(back->activeModeId() == "mixdown"); CHECK(back->modes().size() == 4); const Mode* print = back->modes().query("print"); CHECK(print && print->displayName == "Print \"stem\"\n" && print->ordinal == 5); CHECK(back->membership().isShowBoth("{SB}")); const Membership* mm = back->membership().query("{MULTI}"); CHECK(mm && mm->modeIds.size() == 2 && mm->modeIds.count("mixdown")); const TrackSnapshot* snap = back->snapshot("{D}"); CHECK(snap && snap->mainSend == 1 && snap->fxEnable == 0); CHECK(snap && snap->fxOffline.size() == 4 && snap->fxOffline[1] == 0); } } static void testEmptyModelRoundTrip() { ViewModeModel vm; // default: Arrange + Design seeded, active = Arrange, no members std::string json = vm.serialize(); auto back = ViewModeModel::deserialize(json); CHECK(back.has_value()); CHECK(back && *back == vm); // Lenient empty root ⇒ a default-seeded model. auto empty = ViewModeModel::deserialize("{}"); CHECK(empty.has_value()); CHECK(empty && empty->modes().size() == 2); CHECK(empty && empty->activeModeId() == kArrangeModeId); CHECK(empty && empty->membership().empty()); } static void testMalformedJson() { const char* bad[] = { "", "{", "not json", "{\"modes\":[", "{\"modes\":[{\"id\":\"x\"", // truncated mode "{\"activeMode\":\"ghost\"}", // active mode not registered "{\"modes\":[{\"id\":\"a\",\"ordinal\":0},{\"id\":\"a\",\"ordinal\":1}]}", // dup id "{\"membership\":[{\"guid\":\"\"}]}", // empty guid "{\"snapshots\":[{\"showInTcp\":1}]}", // snapshot without guid "{\"snapshots\":[{\"guid\":\"x\",\"fxOffline\":[1,notanumber]}]}", "{\"modes\":[]}trailing", // trailing garbage }; for (const char* j : bad) { auto r = ViewModeModel::deserialize(j); CHECK(!r.has_value()); } } // -- 7. planToggle park path: fxOffline is empty (shell-expands-FX contract) -- // // planToggle calls makeParkPlan(guid, /*fxCount=*/0) for each inactive leaf. // The D2 shell is responsible for expanding per-FX offline ops using // TrackFX_GetCount — the pure model has no access to REAPER FX counts at plan // time. This test pins that contract so a regression that passes a non-zero // count (and emits FX ops prematurely) is caught immediately. // The direct makeParkPlan(guid, 3) path (non-zero fxCount) is covered by // testRestoreRoundTripSnapshotValues above. static void testPlanToggleParkHasEmptyFxOffline() { ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{LEAF}", "", false}); vm.membership().tag("{LEAF}", kDesignModeId); // Toggle to Arrange: {LEAF} is inactive ⇒ park plan emitted. auto plan = vm.planToggle(tree, kArrangeModeId); CHECK(plan.park.size() == 1); // The park plan must have an empty fxOffline — the shell expands FX ops. CHECK(plan.park[0].fxOffline.empty()); // Scalar flags must still be present (the four park zeros). CHECK(plan.park[0].flags.size() == 4); } // -- 7b. Untagged leaves are managed by the mode system ---------------------- // // The core semantic fix: an untagged leaf is an Arrange member. planToggle must // enumerate EVERY leaf in the tree (not just membership_.all()), so an untagged // leaf — absent from the membership index — parks in every non-Arrange mode and // restores in Arrange, identically to a tagged leaf. Parents and show-both leaves // remain never-parked. Tagged-leaf behavior is unchanged. static void testUntaggedLeavesManagedByModeSystem() { ViewModeModel vm; // A tree of leaves NONE of which are in the membership index (all untagged), // plus a parent folder and a show-both leaf to prove they stay untouched. FolderTree tree; tree.nodes.push_back(FolderNode{"{P}", "", /*isParent=*/true}); tree.nodes.push_back(FolderNode{"{U1}", "{P}", false}); // untagged leaf tree.nodes.push_back(FolderNode{"{U2}", "{P}", false}); // untagged leaf tree.nodes.push_back(FolderNode{"{SB}", "", false}); // show-both leaf vm.membership().setShowBoth("{SB}", true); // (iii) Enumeration covers leaves absent from the membership index: {U1}/{U2} // are NOT in membership_.all(), yet the planner reaches them. CHECK(vm.membership().query("{U1}") == nullptr); CHECK(vm.membership().query("{U2}") == nullptr); // (i) Toggling to Design (non-Arrange): every untagged leaf is parked. auto toDesign = vm.planToggle(tree, kDesignModeId); CHECK(parkTargets(toDesign, "{U1}")); CHECK(parkTargets(toDesign, "{U2}")); // (iv) The parent and the show-both leaf are NEVER parked, in either mode. CHECK(!parkTargets(toDesign, "{P}")); CHECK(!parkTargets(toDesign, "{SB}")); // (ii) Toggling to Arrange: the untagged leaves are Arrange members ⇒ active and // NOT parked. (No snapshot stored yet ⇒ no restore op either; just not parked.) auto toArrange = vm.planToggle(tree, kArrangeModeId); CHECK(!parkTargets(toArrange, "{U1}")); CHECK(!parkTargets(toArrange, "{U2}")); CHECK(restoreFor(toArrange, "{U1}") == nullptr); CHECK(!parkTargets(toArrange, "{P}")); CHECK(!parkTargets(toArrange, "{SB}")); // (vi) Restore-from-snapshot fidelity for a previously-parked UNTAGGED leaf: // an untagged leaf parked while in Design carries a snapshot; toggling back to // Arrange restores it from that snapshot verbatim, never a hardcoded default. TrackSnapshot snap; snap.showInTcp = 1; snap.showInMixer = 1; snap.mainSend = 0; snap.fxEnable = 1; snap.fxOffline = {0, 1}; vm.storeSnapshot("{U1}", snap); // as the shell would, before parking it in Design auto backToArrange = vm.planToggle(tree, kArrangeModeId); const TrackPlan* r = restoreFor(backToArrange, "{U1}"); CHECK(r != nullptr); if (r) { CHECK(flagValue(*r, Flag::ShowInTcp) == 1); CHECK(flagValue(*r, Flag::ShowInMixer) == 1); CHECK(flagValue(*r, Flag::MainSend) == 0); // captured 0 comes back 0 CHECK(flagValue(*r, Flag::FxEnable) == 1); CHECK(r->fxOffline.size() == 2); CHECK(r->fxOffline[0].offline == false); CHECK(r->fxOffline[1].offline == true); } } // (v) Tagged-leaf park/restore behavior is unchanged after the untagged fix: a leaf // tagged Design parks in Arrange and is active (not parked) in Design, and a mix of // tagged + untagged leaves each land on the correct side of the toggle. static void testTaggedLeafBehaviorUnchangedWithUntagged() { ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{DES}", "", false}); // tagged into Design tree.nodes.push_back(FolderNode{"{UNT}", "", false}); // untagged ⇒ Arrange vm.membership().tag("{DES}", kDesignModeId); // In Design: {DES} active (not parked); {UNT} inactive ⇒ parked. auto design = vm.planToggle(tree, kDesignModeId); CHECK(!parkTargets(design, "{DES}")); CHECK(parkTargets(design, "{UNT}")); // In Arrange: {DES} inactive ⇒ parked; {UNT} active (not parked). auto arrange = vm.planToggle(tree, kArrangeModeId); CHECK(parkTargets(arrange, "{DES}")); CHECK(!parkTargets(arrange, "{UNT}")); } // -- 8. nextModeId cycle (D4 toggle helper) ---------------------------------- static void testNextModeIdCycles() { ModeRegistry seeded; // Arrange(0) + Design(1) // Two-mode cycle: Arrange -> Design -> Arrange (wraps past the last). CHECK(nextModeId(seeded, kArrangeModeId) == kDesignModeId); CHECK(nextModeId(seeded, kDesignModeId) == kArrangeModeId); // Extends to cycle-through-all with >2 modes, in ordinal order. ModeRegistry three; CHECK(three.add(Mode{"mixdown", "Mixdown", 2})); CHECK(nextModeId(three, kArrangeModeId) == kDesignModeId); CHECK(nextModeId(three, kDesignModeId) == "mixdown"); CHECK(nextModeId(three, "mixdown") == kArrangeModeId); // wraps // Unknown/stale current id -> first mode (a sane home, not ""). CHECK(nextModeId(seeded, "does-not-exist") == kArrangeModeId); // Empty registry -> "" (nothing to cycle to). ModeRegistry empty = ModeRegistry::makeEmpty(); CHECK(nextModeId(empty, kArrangeModeId).empty()); } // -- 9. Nested-folder toggle: snapshot lifecycle survives a re-park ----------- // // Regression for the in-DAW bug: a nested structure (root parent > intermediate // parent > leaves), ONE leaf tagged Design, toggled twice, hid ALL leaves for good. // // Root cause: the D2 shell's park loop stored a fresh snapshot on EVERY park. If a // track is parked again while already parked — which happens on any redundant // same-mode re-apply (a re-activate of the current mode, the segmented switch, a // tag/untag reapply) — the second snapshot captures the track's already-HIDDEN // flags, so a later restore returns it to hidden and the leaf vanishes permanently. // // The pure model can't run the REAPER shell, but the corruption is entirely in the // snapshot store/clear lifecycle, which is model state. This harness mirrors // applyMode's park/restore loops faithfully: it maintains a per-track live "visible" // flag (proxy for B_SHOWINTCP), and for each toggle it runs planToggle, then for // each park op it (a) snapshots the live flag BEFORE parking — GUARDED to only // capture when no snapshot exists yet, exactly like the fixed shell — and (b) hides // the track; for each restore op it writes the snapshot's flag back and clears the // snapshot. Asserting the live flags after the sequence proves the fix; a parallel // UNGUARDED run reproduces the original corruption. namespace { // A tiny stand-in for the shell's live REAPER flag reads/writes: guid -> visible. using LiveFlags = std::map; // Runs one applyMode-equivalent toggle against `vm` + `live`. `guard` selects the // fixed (snapshot-once) 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. void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tree, const std::string& target, bool guard) { TogglePlan plan = vm.planToggle(tree, target); // PARK: snapshot-before-hide (guarded or not), then hide. for (const auto& tp : plan.park) { if (tp.flags.empty()) continue; const std::string& guid = tp.flags.front().guid; if (!guard || vm.snapshot(guid) == nullptr) { TrackSnapshot snap; snap.showInTcp = live[guid]; // capture the LIVE visible flag vm.storeSnapshot(guid, snap); } live[guid] = 0; // park hides it } // RESTORE: write snapshot flag back, then drop the snapshot. 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.setActiveMode(target); } // The nested tree Daniel reported: root parent {R} > intermediate parent {I} > // leaves {L1} (tagged Design) and {L2}, {L3} (untagged => Arrange). FolderTree nestedTree() { FolderTree t; t.nodes.push_back(FolderNode{"{R}", "", /*isParent=*/true}); t.nodes.push_back(FolderNode{"{I}", "{R}", /*isParent=*/true}); t.nodes.push_back(FolderNode{"{L1}", "{I}", false}); t.nodes.push_back(FolderNode{"{L2}", "{I}", false}); t.nodes.push_back(FolderNode{"{L3}", "{I}", false}); return t; } } // namespace static void testNestedToggleSnapshotSurvivesRepark() { const FolderTree tree = nestedTree(); // Structural preconditions: buildFolderTree-shaped 3-level tree is classified // correctly and the intermediate node is a parent (never parked), and its // visibility derives from its descendant leaves. { ViewModeModel probe; probe.membership().tag("{L1}", kDesignModeId); // {I} and {R} are parents => never parked, in either mode. auto pd = probe.planToggle(tree, kDesignModeId); auto pa = probe.planToggle(tree, kArrangeModeId); CHECK(!parkTargets(pd, "{I}") && !parkTargets(pd, "{R}")); CHECK(!parkTargets(pa, "{I}") && !parkTargets(pa, "{R}")); // Design: {L1} (its only Design leaf) is visible => {I} and {R} derive visible. auto vd = probe.visibleTracks(tree, kDesignModeId); CHECK(visibleHas(vd, "{I}") && visibleHas(vd, "{R}")); // Arrange: {L2}/{L3} are Arrange leaves => {I} and {R} still derive visible. auto va = probe.visibleTracks(tree, kArrangeModeId); CHECK(visibleHas(va, "{I}") && visibleHas(va, "{R}")); } // GUARDED (fixed shell): drive the reported sequence and assert every leaf is // returned to its correct visibility. Include a redundant same-mode re-apply // (the real trigger) between toggles to force a park-while-parked. { ViewModeModel vm; vm.membership().tag("{L1}", kDesignModeId); LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}}; // all visible at start simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true); // In Design: {L2}/{L3} parked (hidden), {L1} visible. CHECK(live["{L1}"] == 1 && live["{L2}"] == 0 && live["{L3}"] == 0); // Redundant re-apply of the CURRENT mode (segmented switch / re-activate). // With the guard this must NOT recapture the now-hidden snapshots. simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true); CHECK(live["{L1}"] == 1 && live["{L2}"] == 0 && live["{L3}"] == 0); simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true); // Back in Arrange: {L2}/{L3} RESTORED to visible; {L1} parked. CHECK(live["{L2}"] == 1 && live["{L3}"] == 1 && live["{L1}"] == 0); // A second full round to match "toggle twice" exactly. simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true); CHECK(live["{L1}"] == 1 && live["{L2}"] == 0 && live["{L3}"] == 0); simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true); CHECK(live["{L2}"] == 1 && live["{L3}"] == 1 && live["{L1}"] == 0); } // UNGUARDED (original shell): the same sequence corrupts — the redundant re-apply // recaptures {L2}/{L3}'s hidden flags, so toggling back to Arrange restores them // to HIDDEN and they never return. This pins the exact regression the guard fixes. { ViewModeModel vm; vm.membership().tag("{L1}", kDesignModeId); LiveFlags live{{"{L1}", 1}, {"{L2}", 1}, {"{L3}", 1}}; simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false); simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/false); // re-park corrupts simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/false); // The bug: Arrange leaves stay hidden after returning to Arrange. CHECK(live["{L2}"] == 0 && live["{L3}"] == 0); } } int main() { testNModeRegistryAndMembership(); testParentDerivationMultiMode(); testRestoreRoundTripSnapshotValues(); testShowBothNeverParkedVisibleEverywhere(); testStaleGuidTolerated(); testJsonRoundTrip(); testEmptyModelRoundTrip(); testMalformedJson(); testPlanToggleParkHasEmptyFxOffline(); testUntaggedLeavesManagedByModeSystem(); testTaggedLeafBehaviorUnchangedWithUntagged(); testNestedToggleSnapshotSurvivesRepark(); testNextModeIdCycles(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }