fix(view): lane-separate a content-bearing folder's own media when it's derived-visible in >1 mode
planLaneMinting now takes the model + folder tree and splits a track visible in 2+ modes that carries own media (own-item span OR folder derived-visibility), fixing an item dropped on a cross-mode folder leaking into every mode. Show-both tracks are never force-split.
This commit is contained in:
@@ -1105,6 +1105,13 @@ static int splitLaneCount(const LaneMintPlan& p, const std::string& track) {
|
||||
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.
|
||||
@@ -1114,12 +1121,12 @@ static void testLaneMintingSingleModeNoSplit() {
|
||||
LaneItem{"{i2}", kArrangeModeId, false},
|
||||
}},
|
||||
};
|
||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
||||
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({LaneTrack{"{E}", {}}}).empty());
|
||||
CHECK(planLaneMinting(bareModel(), emptyTree(), {LaneTrack{"{E}", {}}}).empty());
|
||||
}
|
||||
|
||||
static void testLaneMintingMultiModeMintsAndAssignsAll() {
|
||||
@@ -1133,7 +1140,7 @@ static void testLaneMintingMultiModeMintsAndAssignsAll() {
|
||||
LaneItem{"{des1}", kDesignModeId, false}, // the newly-added 2nd-mode item
|
||||
}},
|
||||
};
|
||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
||||
const LaneMintPlan plan = planLaneMinting(bareModel(), emptyTree(), tracks);
|
||||
CHECK(!plan.empty());
|
||||
|
||||
// One split with two managed lanes (one per involved mode).
|
||||
@@ -1160,7 +1167,7 @@ static void testLaneMintingManualLaneExempt() {
|
||||
LaneItem{"{comp}", kDesignModeId, /*onManualLane=*/true}, // user's comp take
|
||||
}},
|
||||
};
|
||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
||||
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);
|
||||
@@ -1179,7 +1186,8 @@ static void testLaneMintingManualLaneExempt() {
|
||||
LaneItem{"{d}", kDesignModeId, /*onManualLane=*/true}, // only 2nd mode, exempt
|
||||
}},
|
||||
};
|
||||
CHECK(planLaneMinting(t2).empty()); // managed-eligible content is single-mode ⇒ no split
|
||||
// managed-eligible content is single-mode ⇒ no split (leaf, no tree derivation).
|
||||
CHECK(planLaneMinting(bareModel(), emptyTree(), t2).empty());
|
||||
}
|
||||
|
||||
static void testLaneMintingThreeModesAndOwnershipKeys() {
|
||||
@@ -1197,7 +1205,8 @@ static void testLaneMintingThreeModesAndOwnershipKeys() {
|
||||
LaneItem{"{m}", "mixdown", false},
|
||||
}},
|
||||
};
|
||||
const LaneMintPlan plan = planLaneMinting(tracks);
|
||||
// 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);
|
||||
|
||||
@@ -1221,6 +1230,153 @@ static void testLaneMintingThreeModesAndOwnershipKeys() {
|
||||
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 now
|
||||
// mints managed lanes on {F} and lanes {own} onto the Design lane — so it hides+silences
|
||||
// whenever 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());
|
||||
CHECK(splitLaneCount(plan, "{F}") == 2); // one lane per stance
|
||||
CHECK(hasMint(plan, "{F}", kDesignModeId)); // Design lane (holds the item)
|
||||
CHECK(hasMint(plan, "{F}", kArrangeModeId)); // Arrange lane (reserves the slot)
|
||||
|
||||
// The own item is confined to its tagged (Design) lane — the exact hide-in-Arrange fix.
|
||||
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)));
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
|
||||
// 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() {
|
||||
@@ -1313,6 +1469,11 @@ int main() {
|
||||
testLaneMintingMultiModeMintsAndAssignsAll();
|
||||
testLaneMintingManualLaneExempt();
|
||||
testLaneMintingThreeModesAndOwnershipKeys();
|
||||
testLaneMintingFolderDerivedVisibleSplitsOwnMedia();
|
||||
testLaneMintingFolderOwnItemsSpanBothModes();
|
||||
testLaneMintingShowBothNotForceSplit();
|
||||
testLaneMintingSingleModeLeafVisibleOnceNoSplit();
|
||||
testLaneMintingEmptyFolderNotSplit();
|
||||
testLaneJsonRoundTrip();
|
||||
testLaneMalformedJson();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user