// 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 "../src/lane_keys.h" // laneNameForMode — assert the minting plan's durable keys #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: the Design leaf {L3} stays hidden (leaf visibility unchanged), but // the untagged parents {F2}/{G} are Arrange members by their own default, so they // are visible in Arrange under the corrected own-membership OR derived rule. // (See testParentOwnMembershipVisibility for the full case matrix.) auto a2 = vm2.visibleTracks(nested, kArrangeModeId); CHECK(!visibleHas(a2, "{L3}")); CHECK(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}")); } // -- 2b. Parent own-membership visibility (untagged folder + own FX/media) --- // // Corrected rule: a parent is visible in mode M if EITHER a descendant leaf is // visible in M (existing derived rule) OR the parent belongs to M by its OWN // membership (leafBelongsToMode on the parent's own GUID; untagged ⇒ Arrange). // The reported case: an untagged folder whose leaves are all Design vanished in // Arrange despite carrying its own FX/media. It must now show in Arrange (own // default) AND Design (derived from children). Parents remain never parked. static void testParentOwnMembershipVisibility() { // Case A [reported]: untagged folder {F}, all leaves Design ⇒ folder visible in // BOTH Arrange (own default) and Design (derived from children). { ViewModeModel vm; 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("{L1}", kDesignModeId); vm.membership().tag("{L2}", kDesignModeId); // {F} itself untagged ⇒ Arrange member by default. auto arrange = vm.visibleTracks(tree, kArrangeModeId); auto design = vm.visibleTracks(tree, kDesignModeId); CHECK(visibleHas(arrange, "{F}")); // own default (would FAIL under derived-only rule) CHECK(visibleHas(design, "{F}")); // derived from Design children // Leaves appear only in Design; neither is visible in Arrange. CHECK(visibleHas(design, "{L1}") && visibleHas(design, "{L2}")); CHECK(!visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}")); // Still never parked, in either mode. CHECK(!parkTargets(vm.planToggle(tree, kArrangeModeId), "{F}")); CHECK(!parkTargets(vm.planToggle(tree, kDesignModeId), "{F}")); } // Case B: untagged folder, all leaves Arrange ⇒ folder visible in Arrange only. { ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{F}", "", true}); tree.nodes.push_back(FolderNode{"{L1}", "{F}", false}); // untagged ⇒ Arrange tree.nodes.push_back(FolderNode{"{L2}", "{F}", false}); // untagged ⇒ Arrange CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{F}")); CHECK(!visibleHas(vm.visibleTracks(tree, kDesignModeId), "{F}")); } // Case C: untagged folder, mixed Arrange + Design leaves ⇒ visible in both. { ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{F}", "", true}); tree.nodes.push_back(FolderNode{"{LA}", "{F}", false}); // untagged ⇒ Arrange tree.nodes.push_back(FolderNode{"{LD}", "{F}", false}); vm.membership().tag("{LD}", kDesignModeId); CHECK(visibleHas(vm.visibleTracks(tree, kArrangeModeId), "{F}")); CHECK(visibleHas(vm.visibleTracks(tree, kDesignModeId), "{F}")); } // Case D: nested untagged grandparent {G} > untagged parent {F} > Design leaves. // Both intermediate folders visible in BOTH modes: own-default Arrange (they are // untagged), and derived Design (a Design leaf lives under each). { ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{G}", "", true}); tree.nodes.push_back(FolderNode{"{F}", "{G}", true}); tree.nodes.push_back(FolderNode{"{L1}", "{F}", false}); tree.nodes.push_back(FolderNode{"{L2}", "{F}", false}); vm.membership().tag("{L1}", kDesignModeId); vm.membership().tag("{L2}", kDesignModeId); auto arrange = vm.visibleTracks(tree, kArrangeModeId); auto design = vm.visibleTracks(tree, kDesignModeId); CHECK(visibleHas(arrange, "{G}") && visibleHas(arrange, "{F}")); // own default CHECK(visibleHas(design, "{G}") && visibleHas(design, "{F}")); // derived // The Design leaves stay Design-only; not visible in Arrange. CHECK(!visibleHas(arrange, "{L1}") && !visibleHas(arrange, "{L2}")); } // Case E: existing behavior unchanged — a TAGGED-Design folder holding a visible // Design leaf still shows in Design; and a folder made visible only by a visible // tagged-Design descendant (own membership Arrange) still derives into Design. // Leaf visibility itself is unchanged: a Design leaf is Design-only. { ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{F}", "", true}); tree.nodes.push_back(FolderNode{"{LD}", "{F}", false}); vm.membership().tag("{F}", kDesignModeId); // folder tagged into Design itself vm.membership().tag("{LD}", kDesignModeId); auto design = vm.visibleTracks(tree, kDesignModeId); CHECK(visibleHas(design, "{F}")); // own Design membership + derived from {LD} CHECK(visibleHas(design, "{LD}")); // A Design-tagged folder is NOT an Arrange member ⇒ not visible in Arrange // unless a child is; here the only child is Design, so folder hidden in Arrange. auto arrange = vm.visibleTracks(tree, kArrangeModeId); CHECK(!visibleHas(arrange, "{F}")); CHECK(!visibleHas(arrange, "{LD}")); // leaf visibility unchanged } } // -- 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}")); } // -- 10. reconcile: prune orphaned snapshots on track delete ----------------- // // Closes the "reconcile on delete/restructure" hardening item. reconcile prunes a // snapshot whose GUID is not in the live set (its track was deleted while parked), // preventing both the slow snapshot leak and an incorrect restore if REAPER reuses // the GUID. Membership is deliberately KEPT (undo-delete restores the same GUID, so // dropping the tag would silently lose it). A full live set is a no-op — this is why // folder RESTRUCTURE, which leaves every GUID live, needs no special handling. static void testReconcilePrunesOrphanedSnapshots() { ViewModeModel vm; // Two parked tracks (both snapshotted + tagged); {DEL} is about to be deleted. vm.membership().tag("{LIVE}", kDesignModeId); vm.membership().tag("{DEL}", kDesignModeId); TrackSnapshot sLive; sLive.showInTcp = 1; sLive.fxOffline = {0, 1}; TrackSnapshot sDel; sDel.showInTcp = 1; sDel.fxEnable = 1; vm.storeSnapshot("{LIVE}", sLive); vm.storeSnapshot("{DEL}", sDel); CHECK(vm.snapshots().size() == 2); // {DEL} is deleted from the project ⇒ absent from the live GUID set. std::set liveGuids{"{LIVE}"}; std::size_t removed = vm.reconcile(liveGuids); // The orphaned snapshot is pruned; the live one is retained verbatim. CHECK(removed == 1); CHECK(vm.snapshot("{DEL}") == nullptr); const TrackSnapshot* kept = vm.snapshot("{LIVE}"); CHECK(kept != nullptr); if (kept) CHECK(kept->showInTcp == 1 && kept->fxOffline.size() == 2); // Membership is NOT pruned — the deleted GUID keeps its Design tag so an // undo-delete (which restores the same GUID) brings the track back correctly // tagged. This is the load-bearing design call. CHECK(vm.membership().query("{DEL}") != nullptr); CHECK(vm.leafBelongsToMode("{DEL}", kDesignModeId)); CHECK(vm.membership().query("{LIVE}") != nullptr); } static void testReconcileFullLiveSetIsNoOp() { // The restructure case: tracks moved between folders but none deleted ⇒ every // GUID stays live ⇒ reconcile prunes nothing. ViewModeModel vm; vm.storeSnapshot("{A}", TrackSnapshot{}); vm.storeSnapshot("{B}", TrackSnapshot{}); vm.storeSnapshot("{C}", TrackSnapshot{}); std::set liveGuids{"{A}", "{B}", "{C}"}; std::size_t removed = vm.reconcile(liveGuids); CHECK(removed == 0); CHECK(vm.snapshots().size() == 3); CHECK(vm.snapshot("{A}") && vm.snapshot("{B}") && vm.snapshot("{C}")); // A superset of live GUIDs (tracks exist that were never parked) is also a no-op: // reconcile only ever removes, never adds. std::set superset{"{A}", "{B}", "{C}", "{NEVER_PARKED}"}; CHECK(vm.reconcile(superset) == 0); CHECK(vm.snapshots().size() == 3); // Empty live set (whole project emptied) prunes everything. CHECK(vm.reconcile(std::set{}) == 3); CHECK(vm.snapshots().empty()); } static void testReconcileThenReparkLifecycleIntact() { // No regression to the park/restore lifecycle: after reconcile prunes a deleted // track's snapshot, a still-live tagged leaf toggled back to its mode still // restores from its retained snapshot, and a re-park recaptures fresh state. ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{DES}", "", false}); vm.membership().tag("{DES}", kDesignModeId); TrackSnapshot snap; snap.showInTcp = 1; snap.mainSend = 0; snap.fxEnable = 1; vm.storeSnapshot("{DES}", snap); // parked while in Arrange vm.storeSnapshot("{ORPHAN}", TrackSnapshot{}); // a since-deleted parked track // Reconcile with {DES} live, {ORPHAN} gone. CHECK(vm.reconcile(std::set{"{DES}"}) == 1); CHECK(vm.snapshot("{ORPHAN}") == nullptr); // Toggle back to Design: {DES} restores from its retained snapshot verbatim. auto toDesign = vm.planToggle(tree, kDesignModeId); const TrackPlan* r = restoreFor(toDesign, "{DES}"); CHECK(r != nullptr); if (r) { CHECK(flagValue(*r, Flag::ShowInTcp) == 1); CHECK(flagValue(*r, Flag::MainSend) == 0); CHECK(flagValue(*r, Flag::FxEnable) == 1); } } // -- 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); } } // =========================================================================== // D2 two-canvas lane extension tests // =========================================================================== // Does the plan emit a lane op for (trackGuid, laneKey)? Returns its lanePlays, or a // sentinel if absent. static int lanePlaysFor(const TogglePlan& plan, const std::string& trackGuid, const std::string& laneKey) { for (const auto& op : plan.lanes) if (op.trackGuid == trackGuid && op.laneKey == laneKey) return op.lanePlays; return -999; // sentinel: no op for this lane } static bool laneTouched(const std::set& s, const std::string& g, const std::string& k) { return s.count(LaneRef{g, k}) > 0; } // -- D2.1 Lane↔mode mapping + C_LANEPLAYS values ----------------------------- // // A managed lane owned by the ACTIVE mode plays exclusively (1); every inactive-mode // managed lane is silenced+hidden (C_LANEPLAYS = 0). Covers the direct laneModeState // decision and the planToggle op values. static void testLaneModeStateAndPlayValues() { // Direct decision: active mode's lane plays exclusively; others silent. CHECK(laneModeState(kArrangeModeId, kArrangeModeId) == kLanePlaysExclusive); CHECK(laneModeState(kDesignModeId, kArrangeModeId) == kLaneSilent); CHECK(laneModeState(kDesignModeId, kDesignModeId) == kLanePlaysExclusive); CHECK(laneModeState(kArrangeModeId, kDesignModeId) == kLaneSilent); CHECK(kLanePlaysExclusive == 1 && kLaneSilent == 0); // SDK C_LANEPLAYS values // Via planToggle: a shared track {T} with an Arrange lane and a Design lane. ViewModeModel vm; CHECK(vm.lanes().setManaged("{T}", "laneA", kArrangeModeId)); CHECK(vm.lanes().setManaged("{T}", "laneD", kDesignModeId)); FolderTree tree; tree.nodes.push_back(FolderNode{"{T}", "", false}); // Toggle to Design: the Design lane plays (1); the Arrange lane is silenced (0). auto design = vm.planToggle(tree, kDesignModeId); CHECK(lanePlaysFor(design, "{T}", "laneD") == kLanePlaysExclusive); CHECK(lanePlaysFor(design, "{T}", "laneA") == kLaneSilent); // Toggle to Arrange: mirror image. auto arrange = vm.planToggle(tree, kArrangeModeId); CHECK(lanePlaysFor(arrange, "{T}", "laneA") == kLanePlaysExclusive); CHECK(lanePlaysFor(arrange, "{T}", "laneD") == kLaneSilent); // A D1-only project (no fixed lanes) emits no lane ops — plan unchanged from before. ViewModeModel plain; FolderTree t2; t2.nodes.push_back(FolderNode{"{L}", "", false}); CHECK(plain.planToggle(t2, kDesignModeId).lanes.empty()); } // -- D2.2 Lane-ownership index: managed vs manual, add/query/remove ----------- static void testLaneOwnershipIndex() { LaneOwnershipIndex idx; CHECK(idx.empty()); // A lane ABSENT from the index is manual-by-default (never minted by the tool). CHECK(idx.query("{T}", "l0") == nullptr); CHECK(!idx.isManaged("{T}", "l0")); // Managed lane names its owning mode. CHECK(idx.setManaged("{T}", "l0", kDesignModeId)); const LaneOwnership* o = idx.query("{T}", "l0"); CHECK(o != nullptr); if (o) { CHECK(o->isManaged() && !o->isManual()); CHECK(o->managedMode && *o->managedMode == kDesignModeId); } CHECK(idx.isManaged("{T}", "l0")); // Manual lane carries no mode. CHECK(idx.setManual("{T}", "l1")); const LaneOwnership* m = idx.query("{T}", "l1"); CHECK(m != nullptr); if (m) CHECK(m->isManual() && !m->isManaged()); CHECK(!idx.isManaged("{T}", "l1")); // (guid, laneKey) is a composite key: same laneKey on a different track is distinct. CHECK(idx.setManaged("{U}", "l0", kArrangeModeId)); CHECK(idx.size() == 3); CHECK(idx.isManaged("{U}", "l0")); // setManaged replaces a prior manual entry (retag a lane the tool now owns). CHECK(idx.setManaged("{T}", "l1", kArrangeModeId)); CHECK(idx.isManaged("{T}", "l1")); // Empty args are rejected without mutation. CHECK(!idx.setManaged("", "l0", kDesignModeId)); CHECK(!idx.setManaged("{T}", "", kDesignModeId)); CHECK(!idx.setManaged("{T}", "l0", "")); CHECK(!idx.setManual("", "l0")); CHECK(!idx.setManual("{T}", "")); CHECK(idx.size() == 3); // remove drops the entry (⇒ manual-by-default again); second remove is a no-op. CHECK(idx.remove("{T}", "l0")); CHECK(idx.query("{T}", "l0") == nullptr); CHECK(!idx.isManaged("{T}", "l0")); CHECK(!idx.remove("{T}", "l0")); } // -- D2.2b Last-writer-wins ownership replace (round-trip) -------------------- // // setManual then setManaged on the SAME (guid, laneKey) must leave EXACTLY ONE // managed entry — the ownership record is replaced, not accumulated. Guards the // "retag a lane the tool now owns" contract and its persistence: the replace must // survive a serialize/deserialize round-trip with no stray manual duplicate. static void testLaneOwnershipLastWriterWins() { ViewModeModel vm; // Manual first, then managed on the same lane — the managed write replaces. CHECK(vm.lanes().setManual("{T}", "l0")); CHECK(vm.lanes().setManaged("{T}", "l0", kDesignModeId)); CHECK(vm.lanes().size() == 1); // one entry, not two const LaneOwnership* o = vm.lanes().query("{T}", "l0"); CHECK(o && o->isManaged() && *o->managedMode == kDesignModeId); // The reverse also replaces: managed -> manual leaves exactly one manual entry. CHECK(vm.lanes().setManual("{T}", "l0")); CHECK(vm.lanes().size() == 1); const LaneOwnership* m = vm.lanes().query("{T}", "l0"); CHECK(m && m->isManual()); // Back to managed, then round-trip: exactly one managed entry survives, no stray // manual duplicate resurrected by (de)serialization. CHECK(vm.lanes().setManaged("{T}", "l0", kArrangeModeId)); auto back = ViewModeModel::deserialize(vm.serialize()); CHECK(back.has_value()); if (back) { CHECK(back->lanes().size() == 1); const LaneOwnership* r = back->lanes().query("{T}", "l0"); CHECK(r && r->isManaged() && *r->managedMode == kArrangeModeId); } } // -- D2.3/D2.4 Managed-only: planner + query never emit a manual lane -------- // // Required case: a track with a manual lane + managed mode lanes — neither the planner // nor the "which lanes may this toggle touch" query ever emit an op for the manual lane. static void testManagedOnlyPlannerAndQuery() { ViewModeModel vm; FolderTree tree; tree.nodes.push_back(FolderNode{"{T}", "", false}); // Two managed lanes + one manual comp lane on the same track. CHECK(vm.lanes().setManaged("{T}", "arr", kArrangeModeId)); CHECK(vm.lanes().setManaged("{T}", "des", kDesignModeId)); CHECK(vm.lanes().setManual("{T}", "comp")); // user's own comp take // Planner: emits ops for the two managed lanes only; the manual lane is untouched. auto plan = vm.planToggle(tree, kDesignModeId); CHECK(plan.lanes.size() == 2); CHECK(lanePlaysFor(plan, "{T}", "des") == kLanePlaysExclusive); CHECK(lanePlaysFor(plan, "{T}", "arr") == kLaneSilent); CHECK(lanePlaysFor(plan, "{T}", "comp") == -999); // NEVER emitted for a manual lane // Query: managed lanes only; the manual lane is never in the result. auto touched = vm.lanesTouchedByToggle(); CHECK(touched.size() == 2); CHECK(laneTouched(touched, "{T}", "arr")); CHECK(laneTouched(touched, "{T}", "des")); CHECK(!laneTouched(touched, "{T}", "comp")); // The query is target-mode-independent: the SET of touchable lanes is every managed // lane regardless of which mode we would toggle to (the mode only sets the VALUE). auto touchedA = vm.lanesTouchedByToggle(); CHECK(touchedA == touched); // A lane absent from the index entirely is also never touched (manual by default). CHECK(!laneTouched(touched, "{T}", "never-indexed")); } // -- D2.5 Auto-tag decision -------------------------------------------------- // // New track/item GUIDs + active mode ⇒ membership writes; a new item on a manual lane // is EXEMPT (no tag); pre-existing content (not reported new) stays Arrange by default. static bool hasTag(const std::vector& tags, const std::string& guid, const std::string& mode) { for (const auto& t : tags) if (t.guid == guid && t.modeId == mode) return true; return false; } static void testAutoTagDecision() { // A new track + a new item, active mode = Design ⇒ both tagged to Design. { std::vector tracks{"{NT}"}; std::vector items{ NewItem{"{NI}", /*onManualLane=*/false} }; auto tags = autoTagNewContent(tracks, items, kDesignModeId); CHECK(tags.size() == 2); CHECK(hasTag(tags, "{NT}", kDesignModeId)); CHECK(hasTag(tags, "{NI}", kDesignModeId)); } // A new item on a MANUAL lane is exempt — no tag emitted for it. { std::vector items{ NewItem{"{NORMAL}", false}, NewItem{"{MANUAL}", true}, // landed on a hand-managed lane ⇒ exempt }; auto tags = autoTagNewContent({}, items, kDesignModeId); CHECK(tags.size() == 1); CHECK(hasTag(tags, "{NORMAL}", kDesignModeId)); CHECK(!hasTag(tags, "{MANUAL}", kDesignModeId)); // manual-lane exemption } // Active mode = Arrange ⇒ new content is tagged to Arrange (the active-mode rule, // even for the default stance). Empty GUIDs are skipped. { std::vector tracks{"{NT}", ""}; auto tags = autoTagNewContent(tracks, {}, kArrangeModeId); CHECK(tags.size() == 1); CHECK(hasTag(tags, "{NT}", kArrangeModeId)); } // Empty active mode ⇒ no tags at all (nothing to tag into). { auto tags = autoTagNewContent({"{NT}"}, {NewItem{"{NI}", false}}, ""); CHECK(tags.empty()); } // Pre-existing content resolves to Arrange: a GUID the shell does NOT report as new // is never passed here, so it never gets tagged and stays untagged ⇒ Arrange by the // membership default. Prove the default directly on a fresh model. { ViewModeModel vm; CHECK(vm.membership().query("{PREEXISTING}") == nullptr); // absent from index CHECK(vm.leafBelongsToMode("{PREEXISTING}", kArrangeModeId)); // ⇒ Arrange CHECK(!vm.leafBelongsToMode("{PREEXISTING}", kDesignModeId)); } } // -- D2.7 Lane minting decision (Wave 3) ------------------------------------- // // planLaneMinting: a track with content of only ONE mode is NOT split (D1 unchanged); // a track that holds >1 mode's content mints one managed lane per mode and assigns EVERY // managed-eligible item (incl. pre-existing) to its mode's lane; manual-lane items are // exempt (never counted, never reassigned, their lane never minted-over). static bool hasMint(const LaneMintPlan& p, const std::string& track, const std::string& mode) { for (const auto& m : p.mints) if (m.trackGuid == track && m.modeId == mode && m.laneKey == laneNameForMode(mode)) return true; return false; } static bool hasAssign(const LaneMintPlan& p, const std::string& item, const std::string& track, const std::string& mode) { for (const auto& a : p.assigns) if (a.itemGuid == item && a.trackGuid == track && a.laneKey == laneNameForMode(mode)) return true; return false; } static int splitLaneCount(const LaneMintPlan& p, const std::string& track) { for (const auto& s : p.splits) if (s.trackGuid == track) return s.laneCount; return -1; // no split for this track } static void testLaneMintingSingleModeNoSplit() { // A track whose items all belong to ONE mode is NOT lane-split — D1 whole-track // parking still separates the stances. No split, no mint, no assignment. std::vector tracks{ LaneTrack{"{T}", { LaneItem{"{i1}", kArrangeModeId, false}, LaneItem{"{i2}", kArrangeModeId, false}, }}, }; const LaneMintPlan plan = planLaneMinting(tracks); CHECK(plan.empty()); CHECK(splitLaneCount(plan, "{T}") == -1); // An empty track (no items) is likewise never split. CHECK(planLaneMinting({LaneTrack{"{E}", {}}}).empty()); } static void testLaneMintingMultiModeMintsAndAssignsAll() { // A track that gained a second mode's item: it now holds Arrange + Design content. // Both modes get a managed lane; ALL managed-eligible items are assigned — including // the pre-existing Arrange item (retroactive lane assignment), not only the new one. std::vector tracks{ LaneTrack{"{T}", { LaneItem{"{arr1}", kArrangeModeId, false}, // pre-existing single-mode item LaneItem{"{arr2}", kArrangeModeId, false}, // pre-existing single-mode item LaneItem{"{des1}", kDesignModeId, false}, // the newly-added 2nd-mode item }}, }; const LaneMintPlan plan = planLaneMinting(tracks); CHECK(!plan.empty()); // One split with two managed lanes (one per involved mode). CHECK(splitLaneCount(plan, "{T}") == 2); CHECK(plan.mints.size() == 2); CHECK(hasMint(plan, "{T}", kArrangeModeId)); CHECK(hasMint(plan, "{T}", kDesignModeId)); // EVERY managed-eligible item assigned to its mode's lane — pre-existing included. CHECK(plan.assigns.size() == 3); CHECK(hasAssign(plan, "{arr1}", "{T}", kArrangeModeId)); // retroactive CHECK(hasAssign(plan, "{arr2}", "{T}", kArrangeModeId)); // retroactive CHECK(hasAssign(plan, "{des1}", "{T}", kDesignModeId)); // the new item } static void testLaneMintingManualLaneExempt() { // A track with Arrange + Design managed-eligible content AND an item the user placed // on a manual lane: the manual item is EXEMPT — it is not counted, not assigned, and // its lane is never minted-over. The managed split proceeds around it. std::vector tracks{ LaneTrack{"{T}", { LaneItem{"{arr}", kArrangeModeId, false}, LaneItem{"{des}", kDesignModeId, false}, LaneItem{"{comp}", kDesignModeId, /*onManualLane=*/true}, // user's comp take }}, }; const LaneMintPlan plan = planLaneMinting(tracks); // Split for the two managed modes; the manual item never appears in assigns. CHECK(splitLaneCount(plan, "{T}") == 2); CHECK(plan.assigns.size() == 2); CHECK(hasAssign(plan, "{arr}", "{T}", kArrangeModeId)); CHECK(hasAssign(plan, "{des}", "{T}", kDesignModeId)); for (const auto& a : plan.assigns) CHECK(a.itemGuid != "{comp}"); // manual-lane item NEVER reassigned // Manual-lane exemption can also SUPPRESS a split: if the ONLY second mode is // supplied by a manual-lane item, the managed-eligible items are single-mode ⇒ NO // split (the user's manual lane is not a mode the tool separates). std::vector t2{ LaneTrack{"{U}", { LaneItem{"{a}", kArrangeModeId, false}, LaneItem{"{d}", kDesignModeId, /*onManualLane=*/true}, // only 2nd mode, exempt }}, }; CHECK(planLaneMinting(t2).empty()); // managed-eligible content is single-mode ⇒ no split } static void testLaneMintingThreeModesAndOwnershipKeys() { // N-mode proof + the ownership writes the shell will apply: three modes on one track // mint three managed lanes, each keyed by its durable name (== laneNameForMode), each // owning the right mode. Applying the mints to a real ownership index reproduces the // managed classification the toggle planner then gates on. ViewModeModel vm; CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2})); std::vector tracks{ LaneTrack{"{T}", { LaneItem{"{a}", kArrangeModeId, false}, LaneItem{"{d}", kDesignModeId, false}, LaneItem{"{m}", "mixdown", false}, }}, }; const LaneMintPlan plan = planLaneMinting(tracks); CHECK(splitLaneCount(plan, "{T}") == 3); CHECK(plan.mints.size() == 3); // Apply the mints exactly as the shell does — record managed ownership — then assert // the ownership index classifies each lane managed-for-its-mode and the toggle // planner would drive exactly these three lanes (managed-only invariant intact). for (const auto& m : plan.mints) CHECK(vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId)); CHECK(vm.lanes().size() == 3); CHECK(vm.lanes().isManaged("{T}", laneNameForMode(kArrangeModeId))); CHECK(vm.lanes().isManaged("{T}", laneNameForMode(kDesignModeId))); CHECK(vm.lanes().isManaged("{T}", laneNameForMode("mixdown"))); CHECK(vm.lanesTouchedByToggle().size() == 3); // Persist round-trip of the just-minted lane-split project: the ownership index (and // the whole model) survives serialize/deserialize unchanged, so a saved lane-split // project restores its managed classification without re-minting. auto back = ViewModeModel::deserialize(vm.serialize()); CHECK(back.has_value()); CHECK(back && *back == vm); if (back) CHECK(back->lanes().size() == 3); } // -- D2.6 JSON round-trip with lane index + membership ----------------------- static void testLaneJsonRoundTrip() { ViewModeModel vm; CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2})); // Membership populated (item + track GUIDs share the index). vm.membership().tag("{TRACK}", kDesignModeId); vm.membership().tag("{ITEM}", "mixdown"); // Lane ownership: managed lanes for two modes + a manual lane; a lane key with // characters that exercise the string escaper. CHECK(vm.lanes().setManaged("{T}", "lane:0", kArrangeModeId)); CHECK(vm.lanes().setManaged("{T}", "lane\"1\"", kDesignModeId)); CHECK(vm.lanes().setManual("{T}", "comp")); CHECK(vm.lanes().setManaged("{U}", "lane:0", "mixdown")); // same key, other track CHECK(vm.setActiveMode("mixdown")); std::string json = vm.serialize(); auto back = ViewModeModel::deserialize(json); CHECK(back.has_value()); CHECK(back && *back == vm); // deserialize(serialize(x)) == x if (back) CHECK(back->serialize() == json); // stable second round-trip if (back) { CHECK(back->lanes().size() == 4); const LaneOwnership* a = back->lanes().query("{T}", "lane:0"); CHECK(a && a->isManaged() && *a->managedMode == kArrangeModeId); const LaneOwnership* d = back->lanes().query("{T}", "lane\"1\""); CHECK(d && d->isManaged() && *d->managedMode == kDesignModeId); const LaneOwnership* c = back->lanes().query("{T}", "comp"); CHECK(c && c->isManual()); const LaneOwnership* u = back->lanes().query("{U}", "lane:0"); CHECK(u && u->isManaged() && *u->managedMode == "mixdown"); } // A model with an EMPTY lane index still round-trips (D1-only project on D2 code). ViewModeModel d1only; d1only.membership().tag("{X}", kDesignModeId); auto b2 = ViewModeModel::deserialize(d1only.serialize()); CHECK(b2.has_value()); CHECK(b2 && *b2 == d1only); CHECK(b2 && b2->lanes().empty()); } static void testLaneMalformedJson() { const char* bad[] = { "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\"}]}", // missing "managed" "{\"lanes\":[{\"trackGuid\":\"{T}\",\"managed\":true,\"mode\":\"design\"}]}", // missing laneKey "{\"lanes\":[{\"laneKey\":\"l0\",\"managed\":false}]}", // missing trackGuid "{\"lanes\":[{\"trackGuid\":\"\",\"laneKey\":\"l0\",\"managed\":false}]}", // empty trackGuid "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"\",\"managed\":false}]}", // empty laneKey "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\",\"managed\":true}]}", // managed w/o mode "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\",\"managed\":true,\"mode\":\"\"}]}", // managed empty mode "{\"lanes\":[{\"trackGuid\":\"{T}\",\"laneKey\":\"l0\",\"managed\":false,\"mode\":\"design\"}]}", // manual w/ mode "{\"lanes\":[", // truncated }; for (const char* j : bad) { auto r = ViewModeModel::deserialize(j); CHECK(!r.has_value()); } } int main() { testNModeRegistryAndMembership(); testParentDerivationMultiMode(); testParentOwnMembershipVisibility(); testRestoreRoundTripSnapshotValues(); testShowBothNeverParkedVisibleEverywhere(); testStaleGuidTolerated(); testJsonRoundTrip(); testEmptyModelRoundTrip(); testMalformedJson(); testPlanToggleParkHasEmptyFxOffline(); testUntaggedLeavesManagedByModeSystem(); testTaggedLeafBehaviorUnchangedWithUntagged(); testNestedToggleSnapshotSurvivesRepark(); testReconcilePrunesOrphanedSnapshots(); testReconcileFullLiveSetIsNoOp(); testReconcileThenReparkLifecycleIntact(); testNextModeIdCycles(); // D2 two-canvas lane extension testLaneModeStateAndPlayValues(); testLaneOwnershipIndex(); testLaneOwnershipLastWriterWins(); testManagedOnlyPlannerAndQuery(); testAutoTagDecision(); testLaneMintingSingleModeNoSplit(); testLaneMintingMultiModeMintsAndAssignsAll(); testLaneMintingManualLaneExempt(); testLaneMintingThreeModesAndOwnershipKeys(); testLaneJsonRoundTrip(); testLaneMalformedJson(); if (g_fail == 0) std::printf("All tests passed.\n"); return g_fail ? 1 : 0; }