Ψ-W1-T2 review remediation: solo restore drops on visible-in-target, not parked; N-mode segments read dead when unroutable

Fixes a hidden-parent solo replay that could silence the mix. Also closes the N-mode segment silent no-op, amends the invariant comment, trims view.cpp under 600 lines, hedges two SDK inferences, drops a dead null-check.
This commit is contained in:
2026-08-01 20:13:10 -04:00
parent 9c234c2e6b
commit f2cdf676f3
17 changed files with 137 additions and 73 deletions
+1 -1
View File
@@ -95,7 +95,7 @@ L7 sub-pass, 2026-07-27):
- `theme` — pure palette module: role→color mapping, REAPER-grey neutral ladder + the pastel accent system, the keyboard strip's spectral ramp, WCAG contrast-floor helpers + `compositeOver` (the effective color of a translucent fill, so alpha overlays are testable). Only the ramp's MID stop is its own constant; lo/hi are still aliases of `accent/primary`/`accent/tertiary`, so a categorical accent move CAN still reorder the ramp — `testSpectralRampLuminanceIsMonotonic` is the build-time catch, not the structure.
- `component_geometry` — pure button/slider/list-row geometry + hover hit-test helpers.
- `action_bar` — pure task-grouped action-bar layout/hit-test: clusters (Capture / Placement / Maintenance / Tagging / Switching).
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement, plus `modeSegmentEnabled` — the mode segment's live/dead predicate under the playback gate (the transport bool is passed IN, so this stays REAPER-free).
- `footer_bar` — pure footer layout/hit-test: `[Arrange|Design]` mode-toggle geometry, Tail button, and Prune placement, plus `modeSegmentEnabled` — the mode segment's live/dead predicate under the playback gate AND under whether the shell resolved a routable command id for it (both bools passed IN, so this stays REAPER-free).
- `overflow_menu` — pure overflow-menu-button geometry/reserve/hit-test for the top-toolbar More (⋯) button.
- `mode_enable` — pure opposite-mode enablement predicate: given the active mode, computes per-button live/disabled state for the four Item/Track × Arrange/Design tag buttons.
- `tooltip` — pure tooltip placement + prefix-strip: strips the `ReaSampler:` display prefix from the registered action phrase; width clamped to the client rect.
+2 -2
View File
@@ -64,8 +64,8 @@ FooterHit hitTestFooterBar(int px, int py, const FooterBarLayout& layout) {
return FooterHit::None;
}
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning) {
return isActiveSegment || !transportRunning;
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning, bool routable) {
return isActiveSegment || (!transportRunning && routable);
}
} // namespace reasampler::ui
+6 -2
View File
@@ -65,7 +65,11 @@ FooterHit hitTestFooterBar(int px, int py, const FooterBarLayout& layout);
// plays or records (shell/view::transportBlocksModeSwitch), so an unreachable segment must READ
// dead before the click, not merely refuse on it. The ACTIVE segment stays live regardless: it
// fires a reapply, which is never gated, and dimming the mode you are already in would read as
// "this mode is unavailable" rather than "you cannot leave it right now".
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning);
// "this mode is unavailable" rather than "you cannot leave it right now". `routable` is whether
// the shell resolved a live command id for this segment (the model is N-mode, the UI ships two
// seeded ids — a third registered mode has no action to route through, so its segment must read
// dead rather than paint live and silently no-op on click). Passed IN so this predicate — and
// `core/ui` — stays REAPER-free, exactly as `transportRunning` is.
bool modeSegmentEnabled(bool isActiveSegment, bool transportRunning, bool routable);
} // namespace reasampler::ui
+1 -1
View File
@@ -92,7 +92,7 @@ settled 2026-07-23):
## Modules
- `view_mode_model` — Design View mode system: mode registry, GUID-keyed membership, folder-tree-aware visibility derivation, snapshot-based park/restore planner, the per-mode `SoloCache` it owns, JSON round-trip.
- `solo_cache` — the per-mode solo surface: `SoloCache` (mode id → GUID → raw `I_SOLO`), the soloed-subset filter, and `planSoloRestore`, whose two drop rules (dead GUID, track parked in the incoming mode) and their reasoning live in its header.
- `solo_cache` — the per-mode solo surface: `SoloCache` (mode id → GUID → raw `I_SOLO`), the soloed-subset filter, and `planSoloRestore`, whose two drop rules (dead GUID, not visible in the incoming mode) and their reasoning live in its header.
- `view_tree` — pure `I_FOLDERDEPTH`→FolderTree helper for the Design View shell.
- `mode_switch` — REAPER-free segment layout + hit-test for the bank_panel's Design View mode switch.
- `guid_diff` — the pure, REAPER-free core of the D2 Wave-2 new-content detection: `newGuids(previous, current)` computes the GUIDs present in `current` but absent from `previous` (empty GUIDs ignored); `GuidBaseline` tracks the live GUID set across polls for one project, implementing the first-poll-after-open guard (the first `observe()` after construction/`reset()` records a baseline and reports nothing new, so pre-existing content is never mass-tagged) and re-arms via `reset()` on a detected project switch so detection never diffs across two unrelated projects.
+2 -2
View File
@@ -53,11 +53,11 @@ std::size_t SoloCache::reconcile(const std::set<std::string>& liveGuids) {
std::vector<SoloOp> planSoloRestore(const std::map<std::string, int>& cached,
const std::set<std::string>& liveGuids,
const std::set<std::string>& parkedGuids) {
const std::set<std::string>& visibleGuids) {
std::vector<SoloOp> ops;
for (const auto& [guid, value] : cached) {
if (liveGuids.count(guid) == 0) continue;
if (parkedGuids.count(guid) != 0) continue;
if (visibleGuids.count(guid) == 0) continue;
ops.push_back(SoloOp{guid, value});
}
return ops;
+9 -5
View File
@@ -73,14 +73,18 @@ private:
// The incoming mode's restore writes, in GUID order. Two kinds of entry are dropped
// rather than written:
// * a GUID absent from `liveGuids` — the track is gone (same prune rule as above);
// * a GUID parked in the incoming mode — a parked track is hidden and carries
// B_MAINSEND=0, so soloing it would silence the whole mix while contributing
// nothing audible, and the user would have no visible control to undo it.
// * a GUID absent from `visibleGuids` — not visible in the incoming mode, whether
// because the leaf is parked or because it is a folder parent that is itself
// derived-invisible there. Either way the track is hidden and (for a parked
// leaf) carries B_MAINSEND=0, so soloing it would silence the whole mix while
// contributing nothing audible, and the user would have no visible control to
// undo it. A show-both leaf is a member of every mode's visible set, so it is
// never dropped by this rule.
// The caller consumes the whole mode entry regardless (clear-on-restore), so a
// dropped entry does not linger as zombie state waiting on a track that may never
// come back unparked.
// become visible again.
std::vector<SoloOp> planSoloRestore(const std::map<std::string, int>& cached,
const std::set<std::string>& liveGuids,
const std::set<std::string>& parkedGuids);
const std::set<std::string>& visibleGuids);
} // namespace reasampler::view
+4 -2
View File
@@ -162,8 +162,10 @@ void persistViewState() {
}
// The footer segment already reads disabled while the transport runs, but an action can
// be fired with the panel closed — so the refusal also lands as a status line. Guarded on
// the transport because applyMode's other refusal is an unregistered mode id.
// be fired with the panel closed — so the refusal also goes through Help_Set (SDK
// documents only the signature, not its display surface; inferred to be visible
// feedback by name, not confirmed). Guarded on the transport because applyMode's
// other refusal is an unregistered mode id.
void reportModeSwitchRefused() {
if (transportBlocksModeSwitch(nullptr) && Help_Set)
Help_Set("ReaSampler: stop the transport to switch view mode", true);
+18 -17
View File
@@ -44,6 +44,20 @@ TailSetting currentTail() {
return g_panel.session ? g_panel.session->tail() : TailSetting{};
}
// The registered activate action behind a footer mode segment, or 0 if the mode has none.
// Routing the segment through the SAME action the Actions list fires is what gives a
// panel-initiated switch the persist + repaint it used to skip; a mode with no such
// action (the model is N-mode, the UI ships two) resolves to 0 here, which
// modeSegmentEnabled reads as unroutable so the segment paints dead rather than live-
// but-inert. Non-anonymous: panel_render.cpp resolves the same id to compute that bool.
int modeActivateCommandId(const std::string& modeId) {
ActionBarRow row{};
if (modeId == kArrangeModeId) row.suffix = "VIEW_ACTIVATE_ARRANGE";
else if (modeId == kDesignModeId) row.suffix = "VIEW_ACTIVATE_DESIGN";
else return 0;
return resolveBarCommandId(row);
}
namespace {
// Commits the current tail setting to ext state and marks the active project dirty so the
@@ -76,18 +90,6 @@ bool handleToolbarClick(int x, int y, const ActionBarRect& bar,
return true;
}
// The registered activate action behind a footer mode segment. Routing the segment
// through the SAME action the Actions list fires is what gives a panel-initiated switch
// the persist + repaint it used to skip; a mode with no such action (the model is
// N-mode, the UI ships two) is unreachable from the footer rather than routed around it.
int modeActivateCommandId(const std::string& modeId) {
ActionBarRow row{};
if (modeId == kArrangeModeId) row.suffix = "VIEW_ACTIVATE_ARRANGE";
else if (modeId == kDesignModeId) row.suffix = "VIEW_ACTIVATE_DESIGN";
else return 0;
return resolveBarCommandId(row);
}
// True iff `tr` has I_FREEMODE==2 (fixed lanes enabled). Value verified in view.cpp;
// reproduced locally so this file stays self-contained.
constexpr int kFreeModeFixedLanes = 2;
@@ -316,11 +318,10 @@ void handleClick(int x, int y) {
if (seg < static_cast<int>(modes.size())) {
const std::string& id = modes[static_cast<std::size_t>(seg)].id;
const bool isActive = id == view.activeModeId();
// Disabled while the transport runs: claimed but inert, the same shape a
// disabled toolbar row takes — a dead segment reads inert, never absent,
// and never falls through to the grid.
if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked)) {
const int cmd = modeActivateCommandId(id);
const int cmd = modeActivateCommandId(id);
// Disabled/dead rationale: core/ui/footer_bar.h. Claimed but inert, the
// same shape a disabled toolbar row takes — never falls through to the grid.
if (modeSegmentEnabled(isActive, g_panel.modeSwitchBlocked, cmd != 0)) {
if (cmd != 0 && Main_OnCommand) Main_OnCommand(cmd, 0);
}
}
+3 -3
View File
@@ -112,9 +112,9 @@ void drawFooter(LICE_IBitmap* bmp, int w, int h) {
const SegmentRect& s = segs[static_cast<std::size_t>(i)];
const Mode& mode = modes[static_cast<std::size_t>(i)];
const bool active = mode.id == activeId;
// A switch is refused while the transport runs, so an unreachable segment
// draws dead rather than inviting a click that would silently do nothing.
const bool live = modeSegmentEnabled(active, g_panel.modeSwitchBlocked);
// Disabled/dead rationale: core/ui/footer_bar.h.
const bool live = modeSegmentEnabled(active, g_panel.modeSwitchBlocked,
modeActivateCommandId(mode.id) != 0);
const InteractionState state =
active ? InteractionState::Active
: (live ? hoverState(g_panel.hovered, HoverKind::ModeSegment, i)
+7 -4
View File
@@ -292,10 +292,10 @@ struct PanelState {
unsigned int hoverSinceTick = 0;
bool tooltipShown = false;
// Polled on the OnTimer tick (REAPER exposes no transport-change callback): a mode
// switch is refused while the transport runs, so the footer's [Arrange|Design]
// segments paint disabled. Cached rather than read per paint AND per click so the
// pixel the user saw and the click they made cannot disagree within a tick.
// Polled on the OnTimer tick (REAPER exposes no transport-change callback). Feeds
// modeSegmentEnabled (core/ui/footer_bar.h). Cached rather than read per paint AND
// per click so the pixel the user saw and the click they made cannot disagree
// within a tick.
bool modeSwitchBlocked = false;
BankPanelFullHeight fullHeight = BankPanelFullHeight::Split;
@@ -485,6 +485,9 @@ void startAudition(int idx);
// panel_input.cpp — click/wheel/key routing, accelerator, new-content detection,
// and the session-tail read/mutate helpers.
TailSetting currentTail();
// The registered activate action behind a footer mode segment, or 0 if unroutable.
// Shared with panel_render.cpp so paint and click resolve the same id.
int modeActivateCommandId(const std::string& modeId);
void handleClick(int x, int y);
bool handleWheel(int x, int y, int delta);
void registerAccel();
+7 -1
View File
@@ -83,7 +83,7 @@ applies the resulting lane state to live tracks.
## Modules
- `view` — Design View shell: snapshots flag values before parking, drives hide + CPU-park on inactive-mode leaves (`B_SHOWINTCP`/`B_SHOWINMIXER`/`B_MAINSEND`/`I_FXEN` + per-FX offline), restores from snapshot. Owns the one discriminator (`target != active`) that separates a real switch from a reapply, and with it both the playback gate (`transportBlocksModeSwitch`) and the solo cache/clear/restore seams. **Never touches master or `B_MUTE`.**
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
- `view_solo` — the `I_SOLO` read/write pair behind the per-mode solo surface, plus `clearTrackSolos`/`restoreTrackSolos`, the outgoing-clear and incoming-replay entry points `view` drives them through. Holds no policy: what to cache, clear, or replay is `core/view/solo_cache`.
## Gotchas
@@ -94,3 +94,9 @@ applies the resulting lane state to live tracks.
(fixed-lane mechanics, mode-aware capture placement, hidden-AND-silenced) are
reflected in Invariants above; the membership/lane-ownership model concepts
it also covers live in `core/view`'s Invariants.
- REAPER's own undo restores live `I_SOLO` but not the model's solo cache or
`activeModeId` — neither rolls back with a Ctrl-Z. An undo after a mode switch
leaves the two out of step, and the next switch banks the undo-restored solos
under whatever mode id is active at that point, not the one the user undid back
to. Pre-existing: `snapshots_` already carries this same model-vs-undo split;
the solo cache inherits it rather than introducing it. Not fixed here.
+6 -18
View File
@@ -60,15 +60,6 @@ constexpr int kFreeModeFixedLanes = 2;
// deliberately excluded, nothing is moving there.
constexpr int kTransportMoving = 1 | 4;
// GUIDs the plan parks in the mode being entered — the set a solo restore skips.
std::set<std::string> parkedGuidsOf(const TogglePlan& plan) {
std::set<std::string> parked;
for (const TrackPlan& tp : plan.park) {
if (!tp.flags.empty()) parked.insert(tp.flags.front().guid);
}
return parked;
}
// C_LANESCOLLAPSED=2: render a tool-split track like a normal single-lane
// track showing only the playing lane (SDK: 1=collapsed, 2=hidden-lanes-exist
// but displays as non-fixed-lane).
@@ -394,7 +385,6 @@ bool applyMintPlan(ViewModeModel& model, const LaneMintPlan& plan,
} // namespace
bool transportBlocksModeSwitch(ReaProject* proj) {
if (!GetPlayStateEx) return false; // fail-open: never gate on a missing API pointer
return (GetPlayStateEx(proj) & kTransportMoving) != 0;
}
@@ -439,11 +429,7 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
// ride the existing undo block — one mode toggle stays one Ctrl-Z.
if (realSwitch) {
model.soloCache().store(outgoingModeId, outgoingSolo);
std::vector<view::SoloOp> clearOps;
clearOps.reserve(outgoingSolo.size());
for (const auto& entry : outgoingSolo)
clearOps.push_back(view::SoloOp{entry.first, 0});
applySoloOps(handleByGuid, clearOps);
clearTrackSolos(handleByGuid, outgoingSolo);
}
// PARK: snapshot before mutating, store into the model, then apply.
@@ -502,11 +488,13 @@ bool applyMode(ViewModeModel& model, const std::string& targetModeId, ReaProject
model.setActiveMode(targetModeId);
// Replay + consume, before the single relayout below picks the change up.
// Replay + consume, before the single relayout below picks the change up. Dropped
// against `visible` (computed above for parent visibility), not the park plan: a
// folder parent can be hidden without being parked, and a hidden track must not
// receive a replayed solo it carries no visible control to undo.
if (realSwitch) {
if (const std::map<std::string, int>* cached = model.soloCache().query(targetModeId)) {
applySoloOps(handleByGuid,
view::planSoloRestore(*cached, liveGuids, parkedGuidsOf(plan)));
restoreTrackSolos(handleByGuid, *cached, liveGuids, visible);
model.soloCache().clear(targetModeId);
}
}
+5 -2
View File
@@ -3,7 +3,8 @@
// ViewModeModel's pure planner, and applies the resulting flag / per-FX /
// lane writes. The .cpp is the sole REAPER-facing TU here (CLAUDE.md contract:
// only main.cpp defines the API pointers). See src/shell/view/CLAUDE.md for
// the enforced invariants (never touch master/mute/solo, snapshot-based restore).
// the enforced invariants (never touch master/mute, solo cached per mode and
// never lost, snapshot-based restore).
#include <string>
@@ -17,7 +18,9 @@ namespace reasampler {
// True while `proj`'s transport is playing or recording — the condition under which
// applyMode refuses a real mode switch. Exposed so the panel can paint the footer's
// [Arrange|Design] segments disabled BEFORE the click rather than only refusing on
// it. `proj` == nullptr means the current project.
// it. `proj` is passed through to GetPlayStateEx as-is; the SDK documents proj=0
// meaning "current project" for GetTrack but is silent on GetPlayStateEx, so
// `nullptr` meaning the current project here is inferred by analogy, not confirmed.
bool transportBlocksModeSwitch(ReaProject* proj);
// Snapshots each about-to-park track's flags into `model`, caches/clears the
+14
View File
@@ -44,4 +44,18 @@ void applySoloOps(const TrackHandles& handles, const std::vector<view::SoloOp>&
}
}
void clearTrackSolos(const TrackHandles& handles, const std::map<std::string, int>& soloed) {
std::vector<view::SoloOp> ops;
ops.reserve(soloed.size());
for (const auto& entry : soloed) ops.push_back(view::SoloOp{entry.first, 0});
applySoloOps(handles, ops);
}
void restoreTrackSolos(const TrackHandles& handles,
const std::map<std::string, int>& cached,
const std::set<std::string>& liveGuids,
const std::set<std::string>& visibleGuids) {
applySoloOps(handles, view::planSoloRestore(cached, liveGuids, visibleGuids));
}
} // namespace reasampler
+13
View File
@@ -3,6 +3,8 @@
// to cache, clear, or replay is core/view/solo_cache; this pair only moves I_SOLO
// between the live tracks and that pure plan.
#include <map>
#include <set>
#include <string>
#include <utility>
#include <vector>
@@ -25,4 +27,15 @@ std::vector<view::TrackSolo> readTrackSolos(const TrackHandles& handles);
// absent from the enumeration is skipped (stale/deleted track).
void applySoloOps(const TrackHandles& handles, const std::vector<view::SoloOp>& ops);
// The outgoing-mode clear: zeroes I_SOLO for every GUID in `soloed` (the just-banked
// values applyMode is about to cache).
void clearTrackSolos(const TrackHandles& handles, const std::map<std::string, int>& soloed);
// The incoming-mode replay: applies `cached` filtered through planSoloRestore's drop
// rules (dead GUID, not visible in the incoming mode).
void restoreTrackSolos(const TrackHandles& handles,
const std::map<std::string, int>& cached,
const std::set<std::string>& liveGuids,
const std::set<std::string>& visibleGuids);
} // namespace reasampler
+18 -4
View File
@@ -231,20 +231,32 @@ static void testResizeSweepNoOverlap() {
// --- Mode-segment enablement (the playback gate's visible half) ---------------
static void testModeSegmentsAreLiveWithTheTransportStopped() {
CHECK(modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false));
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false));
CHECK(modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/true));
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/true));
}
static void testInactiveSegmentGoesDeadWhileTheTransportRuns() {
// The one that would fire a real switch — refused while playing/recording, so it
// must read dead rather than invite a click that silently does nothing.
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/true));
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/true, /*routable=*/true));
}
static void testActiveSegmentStaysLiveWhileTheTransportRuns() {
// Clicking the mode you are already in is a reapply, which is never gated;
// dimming it would read as "this mode is unavailable".
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/true));
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/true, /*routable=*/false));
}
static void testInactiveSegmentGoesDeadWhenUnroutable() {
// A third registered mode with no seeded command id (the model is N-mode, the UI
// ships two ids) must read dead, not live-but-silently-inert on click.
CHECK(!modeSegmentEnabled(/*isActiveSegment=*/false, /*transportRunning=*/false, /*routable=*/false));
}
static void testActiveSegmentStaysLiveEvenWhenUnroutable() {
// The active segment always fires a reapply through its own already-resolved id in
// practice, but the predicate's active bypass does not consult routable either way.
CHECK(modeSegmentEnabled(/*isActiveSegment=*/true, /*transportRunning=*/false, /*routable=*/false));
}
int main() {
@@ -263,6 +275,8 @@ int main() {
testModeSegmentsAreLiveWithTheTransportStopped();
testInactiveSegmentGoesDeadWhileTheTransportRuns();
testActiveSegmentStaysLiveWhileTheTransportRuns();
testInactiveSegmentGoesDeadWhenUnroutable();
testActiveSegmentStaysLiveEvenWhenUnroutable();
if (g_fail == 0) std::printf("footer_bar: all tests passed\n");
else std::printf("footer_bar: %d CHECK(s) FAILED\n", g_fail);
+21 -9
View File
@@ -2,8 +2,8 @@
//
// Covers: the soloed-subset filter (zeros and empty GUIDs dropped, raw values kept),
// the cache's store/query/clear lifecycle including the empty-set-removes rule, GUID
// pruning on reconcile, and the restore plan's two drop rules (dead GUID, parked
// track).
// pruning on reconcile, and the restore plan's two drop rules (dead GUID, not visible
// in the incoming mode — covering both a parked leaf and a derived-invisible parent).
#include "../src/core/view/solo_cache.h"
@@ -120,9 +120,9 @@ static void testReconcileWithEveryGuidLiveRemovesNothing() {
// --- planSoloRestore ---------------------------------------------------------
static void testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim() {
static void testRestorePlanReplaysEveryLiveVisibleEntryVerbatim() {
const std::vector<SoloOp> ops =
planSoloRestore({{"{A}", 1}, {"{B}", 2}}, {"{A}", "{B}"}, {});
planSoloRestore({{"{A}", 1}, {"{B}", 2}}, {"{A}", "{B}"}, {"{A}", "{B}"});
CHECK(ops.size() == 2);
CHECK(ops[0] == (SoloOp{"{A}", 1}));
@@ -131,23 +131,34 @@ static void testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim() {
static void testRestorePlanSkipsAGuidThatNoLongerExists() {
const std::vector<SoloOp> ops =
planSoloRestore({{"{GONE}", 1}, {"{HERE}", 1}}, {"{HERE}"}, {});
planSoloRestore({{"{GONE}", 1}, {"{HERE}", 1}}, {"{HERE}"}, {"{HERE}"});
CHECK(ops.size() == 1);
CHECK(ops[0].guid == "{HERE}");
}
static void testRestorePlanSkipsATrackParkedInTheIncomingMode() {
static void testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode() {
// Soloing a parked track would silence the mix while contributing nothing
// audible, and the track is hidden, so the user could not undo it.
const std::vector<SoloOp> ops =
planSoloRestore({{"{PARKED}", 1}, {"{VISIBLE}", 2}}, {"{PARKED}", "{VISIBLE}"},
{"{PARKED}"});
{"{VISIBLE}"});
CHECK(ops.size() == 1);
CHECK(ops[0] == (SoloOp{"{VISIBLE}", 2}));
}
static void testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode() {
// The parent is never parked (parents are never parked — see planToggle), but a
// derived-invisible parent is hidden all the same: replaying its cached solo
// would silence the mix onto a track the user cannot see or unsolo. `visibleGuids`
// (not the park plan) is the drop set precisely so this case is caught too.
const std::vector<SoloOp> ops =
planSoloRestore({{"{HIDDEN_PARENT}", 1}}, {"{HIDDEN_PARENT}"}, {});
CHECK(ops.empty());
}
static void testRestorePlanOfAnEmptyCacheWritesNothing() {
CHECK(planSoloRestore({}, {"{A}"}, {}).empty());
}
@@ -166,9 +177,10 @@ int main() {
testReconcileDropsDeadGuidsAcrossEveryMode();
testReconcileWithEveryGuidLiveRemovesNothing();
testRestorePlanReplaysEveryLiveUnparkedEntryVerbatim();
testRestorePlanReplaysEveryLiveVisibleEntryVerbatim();
testRestorePlanSkipsAGuidThatNoLongerExists();
testRestorePlanSkipsATrackParkedInTheIncomingMode();
testRestorePlanSkipsAParkedLeafNotVisibleInTheIncomingMode();
testRestorePlanSkipsAFolderParentHiddenInTheIncomingMode();
testRestorePlanOfAnEmptyCacheWritesNothing();
if (g_fail == 0) std::printf("solo_cache: all tests passed\n");