Design View: expose a park-shaped no-record track visible instead of adopting or refusing it
Adopting silently wipes hand repairs or hides untouched tracks forever; refusing was rejected too. EXPOSE hands the track back and self-heals on the next apply.
This commit is contained in:
@@ -821,6 +821,13 @@ using LiveFlags = std::map<std::string, int>;
|
||||
// The FX half of the two-half record is never exercised here (nothing sets fxHeld
|
||||
// false), so it stays held throughout and never blocks a re-capture the way
|
||||
// flagsHeld does — the one place this harness is narrower than the real record.
|
||||
//
|
||||
// `live[guid] == 0` doubles as this harness's single-flag stand-in for
|
||||
// parkTrack's real chainReadsParked (all four driven flags at their parked
|
||||
// value) — the harness tracks one representative flag, not four, so "already at
|
||||
// its parked value" is the whole predicate here. `vm.snapshot(guid) == nullptr`
|
||||
// mirrors parkTrack's own gate for "no record of any kind" — see that function's
|
||||
// header doc for why an FX-held-only record must not reach EXPOSE.
|
||||
void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tree,
|
||||
const std::string& target, bool guard) {
|
||||
TogglePlan plan = vm.planToggle(tree, target);
|
||||
@@ -831,6 +838,13 @@ void simulateApplyMode(ViewModeModel& vm, LiveFlags& live, const FolderTree& tre
|
||||
const std::string& guid = tp.flags.front().guid;
|
||||
if (guard) {
|
||||
if (!vm.holdsSnapshotFlags(guid)) {
|
||||
if (!vm.snapshot(guid) && live[guid] == 0) {
|
||||
// EXPOSE: hand it back visible rather than adopt the
|
||||
// park-shaped read as intent. No snapshot stored; this cycle
|
||||
// does not park the track.
|
||||
live[guid] = 1;
|
||||
continue;
|
||||
}
|
||||
TrackSnapshot snap;
|
||||
snap.showInTcp = live[guid];
|
||||
vm.storeSnapshot(guid, snap);
|
||||
@@ -941,17 +955,14 @@ static void testNestedToggleSnapshotSurvivesRepark() {
|
||||
// A model replaced without the project rolling back with it — an unreadable
|
||||
// view_state, a snapshot reconciled away while its track was deleted, a hand or
|
||||
// script edit — leaves tracks sitting at parked values with nothing recording what
|
||||
// they were before. The park refusal that once existed for this pair (view_fx_park's
|
||||
// decidePark) was retired by the Wave-2 shell restructure: view_park.h now states
|
||||
// plainly that nothing infers a park from a live value any more, so a parked-looking
|
||||
// chain with no record is treated as the user's own state and captured, not refused.
|
||||
// This test documents the CURRENT consequence — it is not asserting the consequence
|
||||
// is desirable: a hand recovery made before the next reapply survives, but one made
|
||||
// after it (the reapply has already re-captured the still-hidden value as "the
|
||||
// user's") is overwritten by the next restore, converging with the always-recapture
|
||||
// defect test 9's unguarded block pins. Guarded and unguarded no longer differ here.
|
||||
// they were before. Daniel's ruling: hand the track back rather than either
|
||||
// adopting the parked read as intent (silently wipes a hand repair on the very
|
||||
// next restore, or hides an untouched track forever) or refusing outright (the
|
||||
// retired `decidePark`). EXPOSE does neither: no snapshot stored, the flags
|
||||
// driven visible, and the very next reapply — no user action needed — finds an
|
||||
// honest chain and captures/parks it normally.
|
||||
|
||||
static void testReloadedModelWithParkedTracksIsCapturedNotRefused() {
|
||||
static void testReloadedModelWithParkedTracksIsExposedNotAdopted() {
|
||||
const FolderTree tree = nestedTree();
|
||||
|
||||
ViewModeModel vm;
|
||||
@@ -963,22 +974,29 @@ static void testReloadedModelWithParkedTracksIsCapturedNotRefused() {
|
||||
CHECK(vm.snapshot("{L2}") != nullptr);
|
||||
|
||||
// The reload: the model comes back with no snapshots while the project's
|
||||
// tracks are still parked.
|
||||
// tracks are still parked -- park-shaped, no record.
|
||||
vm.clearSnapshot("{L2}");
|
||||
vm.clearSnapshot("{L3}");
|
||||
|
||||
// The load tick reapplies the saved active mode. Both leaves are captured
|
||||
// fresh from their still-hidden live value — not refused.
|
||||
// The load-tick reapply hits the no-record/park-shaped pair on both leaves.
|
||||
// EXPOSE fires for each: handed back visible, no snapshot stored -- neither
|
||||
// leaf needed a hand recovery first, and {L3} (never touched by hand in the
|
||||
// old test) comes back on its own exactly like {L2}.
|
||||
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
|
||||
CHECK(vm.snapshot("{L2}") != nullptr && vm.snapshot("{L3}") != nullptr);
|
||||
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
|
||||
CHECK(live["{L2}"] == 1 && live["{L3}"] == 1);
|
||||
CHECK(vm.snapshot("{L2}") == nullptr && vm.snapshot("{L3}") == nullptr);
|
||||
|
||||
// A hand recovery made NOW, after that recapture, is not what the next restore
|
||||
// reads back — the fresh (hidden) capture just above is, so it is lost.
|
||||
live["{L2}"] = 1;
|
||||
// Self-healing: the chain no longer reads park-shaped, so the very next apply
|
||||
// of the same target takes the normal path -- captures the (now-visible)
|
||||
// truth and parks it correctly.
|
||||
simulateApplyMode(vm, live, tree, kDesignModeId, /*guard=*/true);
|
||||
CHECK(live["{L2}"] == 0 && live["{L3}"] == 0);
|
||||
CHECK(vm.snapshot("{L2}") != nullptr && vm.snapshot("{L3}") != nullptr);
|
||||
|
||||
// A normal round trip back to Arrange restores the captured (visible) truth --
|
||||
// the wipe the old adopt-based behavior produced is gone.
|
||||
simulateApplyMode(vm, live, tree, kArrangeModeId, /*guard=*/true);
|
||||
CHECK(live["{L2}"] == 0); // wiped by the restore
|
||||
CHECK(live["{L3}"] == 0); // never recovered
|
||||
CHECK(live["{L2}"] == 1 && live["{L3}"] == 1);
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
@@ -2166,7 +2184,7 @@ int main() {
|
||||
testUntaggedLeavesManagedByModeSystem();
|
||||
testTaggedLeafBehaviorUnchangedWithUntagged();
|
||||
testNestedToggleSnapshotSurvivesRepark();
|
||||
testReloadedModelWithParkedTracksIsCapturedNotRefused();
|
||||
testReloadedModelWithParkedTracksIsExposedNotAdopted();
|
||||
testReconcilePrunesOrphanedSnapshots();
|
||||
testReconcileFullLiveSetIsNoOp();
|
||||
testReconcileThenReparkLifecycleIntact();
|
||||
|
||||
@@ -132,6 +132,85 @@ static void testAPluginRemovedWhileRestoredStaysInTheUnionForResolveFxRestoreToD
|
||||
CHECK(foundRemoved);
|
||||
}
|
||||
|
||||
// -- chainReadsParked: the EXPOSE trigger, flags only -------------------------
|
||||
//
|
||||
// The live values the fold reads, keyed by Flag -- so an assertion names the flag
|
||||
// it varies rather than a position in makeParkPlan's op order.
|
||||
struct FlagValues {
|
||||
int showInTcp = 0, showInMixer = 0, mainSend = 0, fxEnable = 0;
|
||||
|
||||
int operator()(Flag f) const {
|
||||
switch (f) {
|
||||
case Flag::ShowInTcp: return showInTcp;
|
||||
case Flag::ShowInMixer: return showInMixer;
|
||||
case Flag::MainSend: return mainSend;
|
||||
case Flag::FxEnable: return fxEnable;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
};
|
||||
|
||||
static void testAChainSittingAtEveryParkedValueReadsAsParkShaped() {
|
||||
const TrackPlan park = makeParkPlan("{A}");
|
||||
CHECK(chainReadsParked(park.flags, FlagValues{0, 0, 0, 0}));
|
||||
}
|
||||
|
||||
static void testOneFlagStillAtTheUsersValueMeansNotParkShaped() {
|
||||
const TrackPlan park = makeParkPlan("{A}");
|
||||
|
||||
// Each case varies exactly one flag, so together they also prove all four are
|
||||
// in the plan: a missing op makes its case read parked and fail here.
|
||||
CHECK(!chainReadsParked(park.flags, FlagValues{1, 0, 0, 0}));
|
||||
CHECK(!chainReadsParked(park.flags, FlagValues{0, 1, 0, 0}));
|
||||
CHECK(!chainReadsParked(park.flags, FlagValues{0, 0, 1, 0}));
|
||||
CHECK(!chainReadsParked(park.flags, FlagValues{0, 0, 0, 1}));
|
||||
}
|
||||
|
||||
static void testAnEmptyPlanProvesNothing() {
|
||||
CHECK(!chainReadsParked(std::vector<TrackFlagOp>{}, FlagValues{0, 0, 0, 0}));
|
||||
}
|
||||
|
||||
static void testFxOfflineAloneIsNotFoldedIntoTheParkedRead() {
|
||||
// chainReadsParked takes no FX argument at all: an FX-only-offline chain
|
||||
// cannot make it read parked, by construction -- there is nothing here it
|
||||
// could read. That is the resolved design question (see view_park.h):
|
||||
// flags-fine/FX-offline stays on the ordinary capture path, never EXPOSE.
|
||||
const TrackPlan park = makeParkPlan("{A}");
|
||||
CHECK(!chainReadsParked(park.flags, FlagValues{1, 1, 1, 1})); // flags all fine
|
||||
}
|
||||
|
||||
// -- the EXPOSE console report -------------------------------------------------
|
||||
|
||||
static void testExposedTracksAreNamedAndEmptyListSaysNothing() {
|
||||
CHECK(describeExposedTracks({}).empty());
|
||||
|
||||
const std::string one = describeExposedTracks({"Bass"});
|
||||
CHECK(one.find("1 track ") != std::string::npos);
|
||||
CHECK(one.find("Bass") != std::string::npos);
|
||||
|
||||
const std::string two = describeExposedTracks({"Bass", "Drum bus"});
|
||||
CHECK(two.find("2 tracks ") != std::string::npos);
|
||||
CHECK(two.find("Bass") != std::string::npos);
|
||||
CHECK(two.find("Drum bus") != std::string::npos);
|
||||
}
|
||||
|
||||
static void testExposedReportNeverUsesRefusalWording() {
|
||||
// Daniel's ruling was EXPOSE, not refuse -- the wording must never suggest the
|
||||
// tool declined to act.
|
||||
const std::string msg = describeExposedTracks({"Bass"});
|
||||
CHECK(msg.find("refus") == std::string::npos);
|
||||
}
|
||||
|
||||
static void testShouldReportSkipsAnExactRepeatOnly() {
|
||||
const std::vector<std::string> a{"Bass"};
|
||||
const std::vector<std::string> b{"Bass", "Drum bus"};
|
||||
|
||||
CHECK(!shouldReport(/*sameOwnerAsLast=*/true, a, a)); // exact repeat: silent
|
||||
CHECK(shouldReport(/*sameOwnerAsLast=*/true, a, b)); // set changed: reprint
|
||||
CHECK(shouldReport(/*sameOwnerAsLast=*/false, a, a)); // owner (project) changed: reprint
|
||||
CHECK(shouldReport(/*sameOwnerAsLast=*/true, a, {})); // healed to none: reprint (resets memo)
|
||||
}
|
||||
|
||||
int main() {
|
||||
testEachDrivenFlagAddressesItsOwnReaperParameter();
|
||||
testNoDrivenFlagCanEverAddressMuteOrSolo();
|
||||
@@ -144,6 +223,15 @@ int main() {
|
||||
testAPluginAddedWhileRestoredJoinsTheUnionRatherThanBeingDropped();
|
||||
testAPluginRemovedWhileRestoredStaysInTheUnionForResolveFxRestoreToDrop();
|
||||
|
||||
testAChainSittingAtEveryParkedValueReadsAsParkShaped();
|
||||
testOneFlagStillAtTheUsersValueMeansNotParkShaped();
|
||||
testAnEmptyPlanProvesNothing();
|
||||
testFxOfflineAloneIsNotFoldedIntoTheParkedRead();
|
||||
|
||||
testExposedTracksAreNamedAndEmptyListSaysNothing();
|
||||
testExposedReportNeverUsesRefusalWording();
|
||||
testShouldReportSkipsAnExactRepeatOnly();
|
||||
|
||||
if (g_fail == 0) std::printf("All tests passed.\n");
|
||||
return g_fail ? 1 : 0;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user