fix(design-view): adopt pre-existing track mode on drop to prevent lane strand
A bank-panel capture dropped onto a track with pre-existing content was auto-tagged into the active mode, forcing a multi-mode lane split whose toggle silenced the pre-existing items. New items now adopt the single mode of their track's prior content; deliberate splits stay on the explicit item-move path.
This commit is contained in:
@@ -1072,6 +1072,150 @@ static void testAutoTagDecision() {
|
||||
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 -----------------------------------
|
||||
@@ -1666,6 +1810,7 @@ int main() {
|
||||
testLaneOwnershipLastWriterWins();
|
||||
testManagedOnlyPlannerAndQuery();
|
||||
testAutoTagDecision();
|
||||
testDropOntoTaggedTrackDoesNotStrand();
|
||||
testPlanItemRetag();
|
||||
testReconcileUnregisteredModeGuardDecision();
|
||||
testLaneMintingSingleModeNoSplit();
|
||||
|
||||
Reference in New Issue
Block a user