Reconcile orphaned Design View snapshots on track delete

Add ViewModeModel::reconcile(liveGuids) to prune snapshots of deleted
tracks; wire it into applyMode before planning. Keep membership so
undo-delete (same GUID) preserves the Design tag.
This commit is contained in:
2026-07-23 13:27:42 -04:00
parent dcbe5d7bdc
commit f9af111c7a
4 changed files with 141 additions and 0 deletions
+96
View File
@@ -579,6 +579,99 @@ static void testTaggedLeafBehaviorUnchangedWithUntagged() {
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() {
@@ -756,6 +849,9 @@ int main() {
testUntaggedLeavesManagedByModeSystem();
testTaggedLeafBehaviorUnchangedWithUntagged();
testNestedToggleSnapshotSurvivesRepark();
testReconcilePrunesOrphanedSnapshots();
testReconcileFullLiveSetIsNoOp();
testReconcileThenReparkLifecycleIntact();
testNextModeIdCycles();
if (g_fail == 0) std::printf("All tests passed.\n");