Files
reasampler/tests/test_view_mode_model.cpp
T
daniel 6e901c198c feat(view_mode_model): add D2 pure lane model (ownership index, lane ops, auto-tag)
Extends the REAPER-free view_mode_model for per-item mode membership via fixed
lanes: lane->mode C_LANEPLAYS mapping, managed-vs-manual lane-ownership index,
managed-only item-lane ops in the toggle planner, "which lanes may this toggle
touch" query, auto-tag decision (manual-lane items exempt), and JSON round-trip
of the lane index. D1 behavior and tests unchanged.
2026-07-23 16:56:36 -04:00

1135 lines
50 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/view_mode_model.h"
#include <algorithm>
#include <cstdio>
#include <string>
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<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
}
// -- 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.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));
}
}
// -- 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();
testManagedOnlyPlannerAndQuery();
testAutoTagDecision();
testLaneJsonRoundTrip();
testLaneMalformedJson();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}