Files
reasampler/tests/test_view_mode_model.cpp
T
daniel 9c234c2e6b Ψ-W1-T2: disjoint per-mode solo surfaces and a playback-gated mode switch
Solo is cached, cleared and replayed per mode on a real switch only; the switch is refused visibly while the transport runs. The footer segment now routes through the activate actions, so a panel switch finally persists.
2026-08-01 19:43:18 -04:00

1937 lines
90 KiB
C++

// 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/core/view/view_mode_model.h"
#include "../src/core/view/lane_keys.h" // laneNameForMode — assert the minting plan's durable keys
#include <algorithm>
#include <cstdio>
#include <string>
using namespace reasampler;
using namespace reasampler::view;
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<std::string>& 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
}
// Golden byte-literal (Q-W1 follow-up): pins the EXACT serialized bytes for the
// default-seeded model (Arrange + Design, no membership), not just self-
// consistent re-serialization — a format drift that both writer and reader
// agree on would slip past the round-trip tests but not this. The format is
// frozen as-shipped; the literal below is the captured current output.
static void testSerializeGoldenLiteral() {
ViewModeModel vm;
CHECK(vm.serialize() ==
"{\"version\":1,\"activeMode\":\"arrange\",\"modes\":[{\"id\":\"arrange\","
"\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
"\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[],"
"\"snapshots\":[],\"lanes\":[],\"soloCache\":[]}");
}
// -- 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<std::string> 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<std::string> 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<std::string> 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<std::string>{}) == 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<std::string>{"{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<std::string, int>;
// 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<LaneRef>& 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<AutoTag>& 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<std::string> tracks{"{NT}"};
std::vector<NewItem> 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<NewItem> 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<std::string> 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));
}
// ADOPTION / STRAND GUARD: a new item dropped onto a track whose pre-existing content
// resolves to a SINGLE mode adopts THAT mode, NOT the (different) active mode — so the
// track never becomes multi-mode and no silencing lane split is triggered. This is the
// exact drop-onto-tagged-track repro at the auto-tag boundary: active mode = Design,
// the track already carries Arrange content ⇒ the drop is tagged Arrange (adopted),
// keeping the pre-existing Arrange items on the visible/playing surface.
{
NewItem dropped{"{DROP}", /*onManualLane=*/false, /*trackModes=*/{kArrangeModeId}};
auto tags = autoTagNewContent({}, {dropped}, kDesignModeId);
CHECK(tags.size() == 1);
CHECK(hasTag(tags, "{DROP}", kArrangeModeId)); // adopted, NOT Design
CHECK(!hasTag(tags, "{DROP}", kDesignModeId));
}
// Adoption is symmetric: pre-existing Design content + active Arrange ⇒ adopt Design.
{
NewItem dropped{"{DROP}", false, {kDesignModeId}};
auto tags = autoTagNewContent({}, {dropped}, kArrangeModeId);
CHECK(tags.size() == 1);
CHECK(hasTag(tags, "{DROP}", kDesignModeId));
}
// No pre-existing content (empty trackModes — a brand-new/empty track) ⇒ the item
// takes the ACTIVE mode (unchanged behaviour; adoption only fires with prior content).
{
NewItem dropped{"{DROP}", false, /*trackModes=*/{}};
auto tags = autoTagNewContent({}, {dropped}, kDesignModeId);
CHECK(tags.size() == 1);
CHECK(hasTag(tags, "{DROP}", kDesignModeId));
}
// Pre-existing content ALREADY spans >1 mode (a deliberate split) ⇒ the new item
// takes the ACTIVE mode and joins the active lane; adoption does not fire (no single
// mode to adopt), and the existing split — with both lanes present — cannot strand.
{
NewItem dropped{"{DROP}", false, {kArrangeModeId, kDesignModeId}};
auto tags = autoTagNewContent({}, {dropped}, kDesignModeId);
CHECK(tags.size() == 1);
CHECK(hasTag(tags, "{DROP}", kDesignModeId)); // active mode, not adopted
}
// Adoption composes with the manual-lane exemption: a manual-lane item is still exempt
// regardless of its track's pre-existing modes (no tag emitted at all).
{
NewItem manual{"{MANUAL}", /*onManualLane=*/true, {kArrangeModeId}};
auto tags = autoTagNewContent({}, {manual}, kDesignModeId);
CHECK(tags.empty());
}
// A new TRACK still takes the active mode — adoption is an ITEM rule only (a track has
// no "pre-existing content on the same track" notion).
{
auto tags = autoTagNewContent({"{NT}"}, {}, kDesignModeId);
CHECK(tags.size() == 1);
CHECK(hasTag(tags, "{NT}", kDesignModeId));
}
}
// -- Drop-onto-tagged-track STRAND repro (end-to-end at the pure-model level) --
//
// The reported bug: a Design-tagged track carries pre-existing (untagged ⇒ Arrange)
// items; while Design is the active mode the user drops a capture onto the track. The
// old auto-tag rule tagged the drop into the ACTIVE mode (Design) even though the
// track's own content was Arrange; the track went multi-mode; planLaneMinting split it;
// planToggle drove the Arrange lane C_LANEPLAYS=0 — stranding the pre-existing,
// previously-visible items on a silenced lane with no user intent.
//
// This test drives the WHOLE decision chain the shell runs on a drop tick — detect the
// new item, resolve its track's pre-existing modes, autoTagNewContent, apply the tag,
// then planLaneMinting + planToggle — and asserts the invariant directly: NO lane
// holding a pre-existing item ends silenced under the active mode.
static void testDropOntoTaggedTrackDoesNotStrand() {
const std::string track = "{T}";
const std::string preA = "{arr-pre-1}"; // pre-existing untagged ⇒ Arrange
const std::string preB = "{arr-pre-2}"; // pre-existing untagged ⇒ Arrange
const std::string drop = "{drop}"; // the capture just dropped onto the track
ViewModeModel vm;
vm.membership().tag(track, kDesignModeId); // the TRACK is tagged Design (leaf tag)
vm.setActiveMode(kDesignModeId); // user is viewing Design when they drop
// Pre-existing items are UNTAGGED (they resolve to Arrange) — never auto-tagged (they
// predate the baseline). Leave them absent from the membership index.
// Shell resolves the drop's track pre-existing modes: both siblings are untagged ⇒
// {arrange}. Exactly one mode ⇒ the adoption guard fires.
NewItem dropped{drop, /*onManualLane=*/false, /*trackModes=*/{kArrangeModeId}};
const std::vector<AutoTag> tags =
autoTagNewContent({}, {dropped}, vm.activeModeId());
for (const AutoTag& t : tags) vm.membership().tag(t.guid, t.modeId);
// FIX ASSERTION 1: the drop adopted Arrange, so the track's OWN items are all one mode.
CHECK(vm.membership().modesOf(drop) == std::set<std::string>{kArrangeModeId});
// Run the lane-minting decision exactly as the shell does after the tag.
FolderTree tree;
tree.nodes.push_back(FolderNode{track, "", false});
std::vector<LaneTrack> tracks{
LaneTrack{track, {
LaneItem{preA, kArrangeModeId, false},
LaneItem{preB, kArrangeModeId, false},
LaneItem{drop, kArrangeModeId, false}, // adopted ⇒ Arrange
}},
};
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
// FIX ASSERTION 2: single-mode track ⇒ NO split at all. Nothing is minted, nothing is
// reassigned, so the pre-existing items stay exactly where they were and visible.
CHECK(plan.empty());
// FIX ASSERTION 3 (the invariant, stated positively): apply whatever lanes the plan
// WOULD mint into the ownership index, then toggle to the active mode and assert NO
// lane carrying a pre-existing item is silenced. With no split the ownership index is
// empty and the toggle emits no silencing op — the pre-existing items cannot be
// stranded. (Belt-and-braces: the same assertion would catch a regression that
// re-introduced the split.)
for (const LaneMint& m : plan.mints)
vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId);
const TogglePlan toggle = vm.planToggle(tree, vm.activeModeId());
// The Arrange lane (if it existed) would be laneNameForMode(kArrangeModeId). Under a
// correct fix it never exists; assert it is not driven to silent either way.
CHECK(lanePlaysFor(toggle, track, laneNameForMode(kArrangeModeId)) != kLaneSilent);
// CONTRAST — the OLD (buggy) behaviour, reproduced by forcing the active-mode tag: if
// the drop had been tagged Design (active) instead of adopting Arrange, the track WOULD
// split and the Arrange lane WOULD be silenced under Design. This proves the test can
// disprove the bug — it is not tautological.
ViewModeModel buggy;
buggy.membership().tag(track, kDesignModeId);
buggy.setActiveMode(kDesignModeId);
buggy.membership().tag(drop, kDesignModeId); // the old active-mode tag
std::vector<LaneTrack> buggyTracks{
LaneTrack{track, {
LaneItem{preA, kArrangeModeId, false},
LaneItem{preB, kArrangeModeId, false},
LaneItem{drop, kDesignModeId, false}, // Design (active) ⇒ 2nd mode
}},
};
const LaneMintPlan buggyPlan = planLaneMinting(buggy, tree, buggyTracks);
CHECK(!buggyPlan.empty()); // the old path DID split
for (const LaneMint& m : buggyPlan.mints)
buggy.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId);
const TogglePlan buggyToggle = buggy.planToggle(tree, kDesignModeId);
CHECK(lanePlaysFor(buggyToggle, track, laneNameForMode(kArrangeModeId)) == kLaneSilent);
}
// -- D2 W3-B item-level mode-move decision -----------------------------------
//
// planItemRetag: the pure decision behind the three item actions. A non-empty target
// tags each eligible selected item into it; an EMPTY target untags (→ Arrange default).
// Manual-lane items are EXEMPT (no op) and empty-GUID items are skipped.
// Find the single op for `guid`, or nullptr.
static const ItemRetagOp* retagOpFor(const std::vector<ItemRetagOp>& ops,
const std::string& guid) {
for (const auto& o : ops)
if (o.guid == guid) return &o;
return nullptr;
}
static void testPlanItemRetag() {
// Move -> Design: a managed-lane / normal item is tagged into Design (untag=false).
{
std::vector<RetagItem> sel{
RetagItem{"{A}", /*onManualLane=*/false},
RetagItem{"{B}", false},
};
auto ops = planItemRetag(sel, kDesignModeId);
CHECK(ops.size() == 2);
const ItemRetagOp* a = retagOpFor(ops, "{A}");
CHECK(a != nullptr);
CHECK(a && !a->untag); // a tag, not an untag
CHECK(a && a->modeId == kDesignModeId); // into Design specifically
const ItemRetagOp* b = retagOpFor(ops, "{B}");
CHECK(b && !b->untag && b->modeId == kDesignModeId);
}
// Empty target ⇒ UNTAG each item (Move -> Arrange / Untag items collapse to this).
// untag must be true and modeId empty — NOT a tag into "arrange".
{
std::vector<RetagItem> sel{ RetagItem{"{A}", false} };
auto ops = planItemRetag(sel, std::string{});
CHECK(ops.size() == 1);
const ItemRetagOp* a = retagOpFor(ops, "{A}");
CHECK(a != nullptr);
CHECK(a && a->untag); // an untag
CHECK(a && a->modeId.empty()); // no target mode carried on an untag
}
// Manual-lane exemption: a manual-lane item yields NO op — not for Move nor for Untag.
{
std::vector<RetagItem> sel{
RetagItem{"{NORMAL}", false},
RetagItem{"{MANUAL}", true}, // on a hand-managed lane ⇒ EXEMPT
};
auto design = planItemRetag(sel, kDesignModeId);
CHECK(design.size() == 1);
CHECK(retagOpFor(design, "{NORMAL}") != nullptr);
CHECK(retagOpFor(design, "{MANUAL}") == nullptr); // exempt — never retagged
auto untag = planItemRetag(sel, std::string{});
CHECK(untag.size() == 1);
CHECK(retagOpFor(untag, "{NORMAL}") != nullptr);
CHECK(retagOpFor(untag, "{MANUAL}") == nullptr); // exempt — never untagged
}
// Empty-GUID items are skipped defensively; empty selection ⇒ no ops.
{
std::vector<RetagItem> sel{ RetagItem{"", false}, RetagItem{"{A}", false} };
auto ops = planItemRetag(sel, kDesignModeId);
CHECK(ops.size() == 1);
CHECK(retagOpFor(ops, "{A}") != nullptr);
CHECK(planItemRetag({}, kDesignModeId).empty());
CHECK(planItemRetag({}, std::string{}).empty());
}
}
// -- D2 W3-B reconcile unregistered-mode guard (pure decision) ---------------
//
// reconcileManagedLanes (shell) recovers a lane's managed ownership from its durable
// name, but must NOT record ownership for a mode the registry no longer knows — a lane
// keyed to an unregistered mode can never become the active mode's lane and would stay
// silenced+hidden forever, orphaning its items. The guard's pure decision is exactly
// modeIdFromLaneName(name) ∈ modes(): this locks that composition so the shell's guard
// (which calls model.modes().contains(*mode)) cannot silently drift.
static void testReconcileUnregisteredModeGuardDecision() {
ViewModeModel vm; // seeds Arrange + Design only
// A managed lane naming a REGISTERED mode: mode decodes and IS contained ⇒ record.
{
const std::string name = laneNameForMode(kDesignModeId);
auto mode = modeIdFromLaneName(name);
CHECK(mode.has_value());
CHECK(vm.modes().contains(*mode)); // guard passes ⇒ shell records ownership
}
// A managed lane naming an UNREGISTERED mode: mode decodes but is NOT contained ⇒
// the guard rejects it and the shell leaves the lane off the index (manual-by-default).
{
const std::string name = laneNameForMode("removed_mode");
auto mode = modeIdFromLaneName(name);
CHECK(mode.has_value());
CHECK(*mode == "removed_mode");
CHECK(!vm.modes().contains(*mode)); // guard fails ⇒ shell must skip
}
}
// -- 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
}
// A plain LEAF track (not a folder) carrying its own items, with no tree derivation:
// an empty model + empty tree means visibleTracks contributes nothing, so the ONLY
// trigger is the track's own-item mode span — exactly the W3-A behavior. These helpers
// keep the W3-A leaf tests reading against a neutral model/tree.
static const ViewModeModel& bareModel() { static ViewModeModel m; return m; }
static const FolderTree& emptyTree() { static FolderTree t; return t; }
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<LaneTrack> tracks{
LaneTrack{"{T}", {
LaneItem{"{i1}", kArrangeModeId, false},
LaneItem{"{i2}", kArrangeModeId, false},
}},
};
const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);
CHECK(plan.empty());
CHECK(splitLaneCount(plan, "{T}") == -1);
// An empty track (no items) is likewise never split.
CHECK(planLaneMinting(bareModel(), emptyTree(), {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<LaneTrack> 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(bareModel(), emptyTree(), 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<LaneTrack> tracks{
LaneTrack{"{T}", {
LaneItem{"{arr}", kArrangeModeId, false},
LaneItem{"{des}", kDesignModeId, false},
LaneItem{"{comp}", kDesignModeId, /*onManualLane=*/true}, // user's comp take
}},
};
const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), 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<LaneTrack> t2{
LaneTrack{"{U}", {
LaneItem{"{a}", kArrangeModeId, false},
LaneItem{"{d}", kDesignModeId, /*onManualLane=*/true}, // only 2nd mode, exempt
}},
};
// managed-eligible content is single-mode ⇒ no split (leaf, no tree derivation).
CHECK(planLaneMinting(bareModel(), emptyTree(), t2).empty());
}
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<LaneTrack> tracks{
LaneTrack{"{T}", {
LaneItem{"{a}", kArrangeModeId, false},
LaneItem{"{d}", kDesignModeId, false},
LaneItem{"{m}", "mixdown", false},
}},
};
// Own items span three modes (leaf; empty tree ⇒ own-item-span is the sole trigger).
const LaneMintPlan plan = planLaneMinting(vm, FolderTree{}, 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);
}
// -- Fix: content-bearing folder derived-visible in >1 mode splits its own media ----
//
// The exact failing case. A folder {F} has descendant leaves in BOTH modes ({LD} Design,
// {LA} Arrange) and carries ONE OWN item ({own}) tagged Design. W3-A's own-item-span test
// alone would NOT split {F} (its own content is single-mode Design), so the item leaked
// into every mode the folder was derived-visible in. The visibility-aware decision splits
// {F} and lanes {own} onto the Design lane — so it hides+silences whenever Arrange is
// active. LAZY-MINT: {F} mints ONLY the Design lane (holding the item), NOT an empty
// reserved Arrange lane — confinement holds via C_LANEPLAYS=0 on the lone Design lane when
// Arrange is active. This is the load-bearing fix; assert it hard.
static void testLaneMintingFolderDerivedVisibleSplitsOwnMedia() {
ViewModeModel vm;
vm.membership().tag("{LD}", kDesignModeId); // a Design leaf under the folder
// {LA} left untagged ⇒ Arrange member; both stances thus live under {F}.
vm.membership().tag("{own}", kDesignModeId); // the folder's OWN dropped item (Design)
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
// Sanity: the folder really is derived-visible in BOTH modes (the precondition the
// W3-A trigger ignored). If this ever stops holding, the fix's premise is gone.
CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{F}") == 1);
CHECK(vm.visibleTracks(tree, kDesignModeId).count("{F}") == 1);
// The folder track {F} carries its own single Design item; its child leaves are the
// separate leaf tracks (not reported as items on {F}).
std::vector<LaneTrack> tracks{
LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}},
};
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
// {F} MUST split even though its own item is single-mode: it is visible in 2 modes.
CHECK(!plan.empty());
// LAZY-MINT: ONE lane only — the Design lane that holds the item. No empty reserved
// Arrange lane is minted, even though {F} is derived-visible in Arrange. The Arrange
// lane appears on demand when an Arrange item first lands on {F}.
CHECK(splitLaneCount(plan, "{F}") == 1); // Design lane only — no reserved lane
CHECK(plan.mints.size() == 1);
CHECK(hasMint(plan, "{F}", kDesignModeId)); // Design lane (holds the item)
CHECK(!hasMint(plan, "{F}", kArrangeModeId)); // NO empty reserved Arrange lane
// The own item is confined to its tagged (Design) lane — the exact hide-in-Arrange fix.
// With only the Design lane present, toggling to Arrange sets its C_LANEPLAYS to 0, so
// the item hides+silences and the track reads as an empty normal track (no leak).
CHECK(plan.assigns.size() == 1);
CHECK(hasAssign(plan, "{own}", "{F}", kDesignModeId));
for (const auto& a : plan.assigns)
CHECK(!(a.itemGuid == "{own}" && a.laneKey == laneNameForMode(kArrangeModeId)));
}
// LAZY-MINT confinement proof. Same single-own-mode / dual-visibility folder, but instead
// of asserting the mint COUNT we prove the FUNCTIONAL confinement the lazy split preserves:
// apply the minted lane's ownership to a live model, then drive the toggle planner and show
// the lone Design lane SILENCES when Arrange is active (C_LANEPLAYS = 0). That is the whole
// point — a single managed lane still hides its item in every other mode, so removing the
// empty reserved Arrange lane costs nothing functionally. Fails if the split ever leaves the
// Design item audible in Arrange (the leak the D2 fix closed) or mints a spurious lane.
static void testLaneMintingLazySingleLaneStillConfines() {
ViewModeModel vm;
vm.membership().tag("{LD}", kDesignModeId); // Design leaf ⇒ folder visible in Design
// {LA} untagged ⇒ Arrange member ⇒ folder ALSO derived-visible in Arrange.
vm.membership().tag("{own}", kDesignModeId); // the folder's one own item (Design)
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
std::vector<LaneTrack> tracks{
LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}},
};
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
// Exactly one lane minted (the Design lane) — no empty reserved Arrange lane.
CHECK(plan.mints.size() == 1);
CHECK(hasMint(plan, "{F}", kDesignModeId));
// Apply the mint's ownership exactly as the shell does, then drive the toggle planner.
for (const auto& m : plan.mints)
CHECK(vm.lanes().setManaged(m.trackGuid, m.laneKey, m.modeId));
CHECK(vm.lanes().size() == 1); // one managed lane on {F}, not two
const std::string designLane = laneNameForMode(kDesignModeId);
// Active = Design: the lone Design lane PLAYS (item visible+audible in its own mode).
const auto design = vm.planToggle(tree, kDesignModeId);
CHECK(lanePlaysFor(design, "{F}", designLane) == kLanePlaysExclusive);
// Active = Arrange: the lone Design lane SILENCES — with no lane playing, the track
// reads as an empty normal track and the Design item does NOT leak. This is the
// confinement guarantee that lets us drop the reserved Arrange lane.
const auto arrange = vm.planToggle(tree, kArrangeModeId);
CHECK(lanePlaysFor(arrange, "{F}", designLane) == kLaneSilent);
}
// A folder carrying its OWN items that already span both modes → still split (the two
// triggers OR: own-item span AND derived visibility both point the same way here). Both
// own items separate to their tagged lanes.
static void testLaneMintingFolderOwnItemsSpanBothModes() {
ViewModeModel vm;
vm.membership().tag("{LD}", kDesignModeId);
vm.membership().tag("{d}", kDesignModeId);
// {a} untagged ⇒ Arrange.
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", true});
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false}); // untagged ⇒ Arrange
std::vector<LaneTrack> tracks{
LaneTrack{"{F}", {
LaneItem{"{a}", kArrangeModeId, false},
LaneItem{"{d}", kDesignModeId, false},
}},
};
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
CHECK(splitLaneCount(plan, "{F}") == 2);
CHECK(plan.assigns.size() == 2);
CHECK(hasAssign(plan, "{a}", "{F}", kArrangeModeId));
CHECK(hasAssign(plan, "{d}", "{F}", kDesignModeId));
}
// -- Fix (pd2): item inserted onto an ALREADY-split folder still yields a non-empty plan --
//
// Regression guard for the "inserted item invisible until a manual toggle" bug. When a new
// item lands (via insert/capture) on a folder that is ALREADY lane-split and derived-visible
// in >1 mode, and REAPER placed it on the active mode's currently-playing lane, the shell's
// assignItemToLane sees I_FIXEDLANE unchanged and writes nothing — applyMintPlan reports
// changed==false. The shell must STILL treat this tick as "content landed on a managed track"
// and refresh the arrange (so the item draws immediately, no toggle). The pure signal the
// shell keys on is: planLaneMinting returns a NON-EMPTY plan carrying an assign for the new
// item. This test locks that signal; if planLaneMinting ever went empty here, the shell would
// have nothing to refresh on and the bug would return.
static void testLaneMintingNewItemOnAlreadySplitFolderYieldsPlan() {
ViewModeModel vm;
vm.membership().tag("{LD}", kDesignModeId); // Design leaf ⇒ folder derived-visible Design
// {LA} untagged ⇒ Arrange ⇒ folder ALSO derived-visible in Arrange (dual-visible).
vm.membership().tag("{own}", kDesignModeId); // the pre-existing own Design item
vm.membership().tag("{new}", kDesignModeId); // the JUST-INSERTED item (auto-tagged Design)
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", /*isParent=*/true});
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
// The folder is already split for Design (its lane exists + is owned). This mirrors the
// live "already auto-split" track the bug reproduces on.
CHECK(vm.lanes().setManaged("{F}", laneNameForMode(kDesignModeId), kDesignModeId));
// {F} now carries its original own item PLUS the freshly-inserted one, both Design.
std::vector<LaneTrack> tracks{
LaneTrack{"{F}", {
LaneItem{"{own}", kDesignModeId, false},
LaneItem{"{new}", kDesignModeId, false},
}},
};
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
// The plan is NON-EMPTY (folder is dual-visible ⇒ splits) and carries an assign for the
// new item onto the Design lane. In the shell this is the exact branch that must force a
// redraw even when the assign is an idempotent no-op (item already on the playing lane).
CHECK(!plan.empty());
CHECK(hasAssign(plan, "{new}", "{F}", kDesignModeId));
CHECK(hasAssign(plan, "{own}", "{F}", kDesignModeId));
}
// SHOW-BOTH escape hatch: a show-both track carrying its own items is visible in every
// mode ON PURPOSE and must NOT be force-split — its content stays cross-mode-visible.
// Even with own items that would otherwise span modes, the decision skips it entirely.
static void testLaneMintingShowBothNotForceSplit() {
ViewModeModel vm;
vm.membership().setShowBoth("{SB}", true);
// A show-both track whose OWN items even span two modes — the W3-A own-span trigger
// would fire, but show-both must override it (its items are meant to play everywhere).
std::vector<LaneTrack> tracks{
LaneTrack{"{SB}", {
LaneItem{"{a}", kArrangeModeId, false},
LaneItem{"{d}", kDesignModeId, false},
}},
};
const LaneMintPlan plan = planLaneMinting(vm, FolderTree{}, tracks);
CHECK(plan.empty()); // NOT split — the escape hatch holds
CHECK(splitLaneCount(plan, "{SB}") == -1);
// And a show-both FOLDER derived-visible in both modes carrying an own item: still not
// split. Visibility is the deliberate point of show-both.
ViewModeModel vm2;
vm2.membership().setShowBoth("{F}", true);
vm2.membership().tag("{LD}", kDesignModeId);
vm2.membership().tag("{own}", kDesignModeId);
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", true});
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
std::vector<LaneTrack> t2{LaneTrack{"{F}", {LaneItem{"{own}", kDesignModeId, false}}}};
CHECK(planLaneMinting(vm2, tree, t2).empty());
}
// A single-mode LEAF visible in exactly one mode is still never split — the D1 whole-track
// parking case. A leaf under a folder, tagged Design, whose sibling is also Design: the
// leaf is visible in one mode only, carries its own Design item, and must NOT lane-split.
static void testLaneMintingSingleModeLeafVisibleOnceNoSplit() {
ViewModeModel vm;
vm.membership().tag("{L}", kDesignModeId);
vm.membership().tag("{own}", kDesignModeId);
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", true});
tree.nodes.push_back(FolderNode{"{L}", "{F}", false}); // the leaf under test
// The leaf {L} is visible only in Design (its one tagged mode).
CHECK(vm.visibleTracks(tree, kDesignModeId).count("{L}") == 1);
CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{L}") == 0);
std::vector<LaneTrack> tracks{
LaneTrack{"{L}", {LaneItem{"{own}", kDesignModeId, false}}},
};
const LaneMintPlan plan = planLaneMinting(vm, tree, tracks);
CHECK(plan.empty()); // single-mode, visible once ⇒ D1 whole-track parking, no split
}
// A content-EMPTY folder derived-visible in many modes carries NO own media, so there is
// nothing to lane-separate: it stays visibility-only (D1 parent handling), never split.
static void testLaneMintingEmptyFolderNotSplit() {
ViewModeModel vm;
vm.membership().tag("{LD}", kDesignModeId);
// {LA} untagged ⇒ Arrange; folder derived-visible in both modes but holds no own item.
FolderTree tree;
tree.nodes.push_back(FolderNode{"{F}", "", true});
tree.nodes.push_back(FolderNode{"{LD}", "{F}", false});
tree.nodes.push_back(FolderNode{"{LA}", "{F}", false});
CHECK(vm.visibleTracks(tree, kArrangeModeId).count("{F}") == 1);
CHECK(vm.visibleTracks(tree, kDesignModeId).count("{F}") == 1);
std::vector<LaneTrack> tracks{LaneTrack{"{F}", {}}}; // no own media
CHECK(planLaneMinting(vm, tree, tracks).empty());
}
// -- 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());
}
}
// -- Per-mode solo cache: persistence + reconcile participation ---------------
static void testSoloCacheJsonRoundTrip() {
ViewModeModel vm;
CHECK(vm.modes().add(Mode{"mixdown", "Mixdown", 2}));
vm.membership().tag("{T}", kDesignModeId);
vm.storeSnapshot("{T}", TrackSnapshot{1, 1, 1, 1, {0, 1}});
CHECK(vm.lanes().setManaged("{T}", "lane:0", kArrangeModeId));
// Every non-zero I_SOLO variant, across more than one mode, plus a GUID that
// exercises the string escaper.
CHECK(vm.soloCache().store(kArrangeModeId, {{"{A}", 1}, {"{B\"q\"}", 2}}));
CHECK(vm.soloCache().store("mixdown", {{"{C}", 5}, {"{D}", 6}}));
CHECK(vm.setActiveMode(kDesignModeId));
const 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) {
const std::map<std::string, int>* arrange = back->soloCache().query(kArrangeModeId);
CHECK(arrange != nullptr);
CHECK(arrange && arrange->at("{A}") == 1);
CHECK(arrange && arrange->at("{B\"q\"}") == 2);
const std::map<std::string, int>* mix = back->soloCache().query("mixdown");
CHECK(mix != nullptr);
CHECK(mix && mix->at("{C}") == 5);
CHECK(mix && mix->at("{D}") == 6);
CHECK(back->soloCache().query(kDesignModeId) == nullptr);
}
}
static void testBlobWithoutSoloCacheKeyStillParses() {
// The compatibility case both ways: a project saved by a build that predates the
// key parses to an empty cache, and its own output stays readable here.
const char* older =
"{\"version\":1,\"activeMode\":\"design\",\"modes\":[{\"id\":\"arrange\","
"\"displayName\":\"Arrange\",\"ordinal\":0},{\"id\":\"design\","
"\"displayName\":\"Design\",\"ordinal\":1}],\"membership\":[{\"guid\":\"{T}\","
"\"modes\":[\"design\"],\"showBoth\":false}],\"snapshots\":[],\"lanes\":[]}";
auto back = ViewModeModel::deserialize(older);
CHECK(back.has_value());
CHECK(back && back->soloCache().empty());
CHECK(back && back->activeModeId() == kDesignModeId);
CHECK(back && back->membership().query("{T}") != nullptr);
}
static void testSoloCacheMalformedJson() {
const char* bad[] = {
"{\"soloCache\":[{\"tracks\":[{\"guid\":\"{A}\",\"solo\":1}]}]}", // missing mode
"{\"soloCache\":[{\"mode\":\"\",\"tracks\":[{\"guid\":\"{A}\",\"solo\":1}]}]}", // empty mode
"{\"soloCache\":[{\"mode\":\"arrange\"}]}", // missing tracks
"{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[]}]}", // empty tracks
"{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[{\"solo\":1}]}]}", // missing guid
"{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[{\"guid\":\"{A}\"}]}]}", // missing solo
"{\"soloCache\":[{\"mode\":\"arrange\",\"tracks\":[{\"guid\":\"\",\"solo\":1}]}]}", // empty guid
"{\"soloCache\":[", // truncated
};
for (const char* j : bad) {
auto r = ViewModeModel::deserialize(j);
CHECK(!r.has_value());
}
}
static void testReconcilePrunesTheSoloCacheAlongsideSnapshots() {
ViewModeModel vm;
vm.storeSnapshot("{LIVE}", TrackSnapshot{1, 1, 1, 1, {}});
vm.storeSnapshot("{DEAD}", TrackSnapshot{1, 1, 1, 1, {}});
vm.soloCache().store(kDesignModeId, {{"{LIVE}", 1}, {"{DEAD}", 2}});
// The return stays the SNAPSHOT count; the solo cache is pruned by the same call.
CHECK(vm.reconcile({"{LIVE}"}) == 1);
const std::map<std::string, int>* design = vm.soloCache().query(kDesignModeId);
CHECK(design != nullptr);
CHECK(design && design->size() == 1);
CHECK(design && design->count("{LIVE}") == 1);
}
int main() {
testSerializeGoldenLiteral();
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();
testDropOntoTaggedTrackDoesNotStrand();
testPlanItemRetag();
testReconcileUnregisteredModeGuardDecision();
testLaneMintingSingleModeNoSplit();
testLaneMintingMultiModeMintsAndAssignsAll();
testLaneMintingManualLaneExempt();
testLaneMintingThreeModesAndOwnershipKeys();
testLaneMintingFolderDerivedVisibleSplitsOwnMedia();
testLaneMintingLazySingleLaneStillConfines();
testLaneMintingFolderOwnItemsSpanBothModes();
testLaneMintingNewItemOnAlreadySplitFolderYieldsPlan();
testLaneMintingShowBothNotForceSplit();
testLaneMintingSingleModeLeafVisibleOnceNoSplit();
testLaneMintingEmptyFolderNotSplit();
testLaneJsonRoundTrip();
testLaneMalformedJson();
// Per-mode solo cache
testSoloCacheJsonRoundTrip();
testBlobWithoutSoloCacheKeyStillParses();
testSoloCacheMalformedJson();
testReconcilePrunesTheSoloCacheAlongsideSnapshots();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}