Split unidentified from missing FX in Design View drop report, fix message + console pop

Distinguish no-GUID-at-capture from identity-no-longer-live; rewrite the drop
message to state the real recovery step; mark FX-GUID stability [verify — DAW];
guard mismatched fx/fxOffline lengths; the report never force-opens now.
This commit is contained in:
2026-08-02 18:51:41 -04:00
parent 5f6efb7cc3
commit d5ed4f6e53
7 changed files with 109 additions and 31 deletions
+23 -9
View File
@@ -82,19 +82,22 @@ static void testReorderedChainRestoresEachPluginItsOwnState() {
// -- 2. FX deleted while parked ----------------------------------------------
static void testDeletedFxDropsExplicitlyAndTouchesNothingElse() {
// B (true) and C (false) carry OPPOSITE captured states — the discriminator.
// A test where both carried `true` couldn't tell "C's own state followed it
// to slot 1" apart from "B's dropped state leaked onto whatever moved there".
const std::vector<FxOfflineOp> ops =
identityOps({{"{A}", false}, {"{B}", true}, {"{C}", true}});
identityOps({{"{A}", false}, {"{B}", true}, {"{C}", false}});
// B was deleted while parked; A and C closed the gap.
const FxRestoreResolution res = resolveFxRestore(ops, {"{A}", "{C}"});
CHECK(res.writes.size() == 2);
CHECK(res.drops.missingIdentity == 1);
CHECK(res.drops.unidentified == 0);
CHECK(res.drops.slotOutOfRange == 0);
CHECK(hasWrite(res, 0, false)); // A
CHECK(hasWrite(res, 1, true)); // C followed its identity to slot 1
// B's captured `true` did NOT land on whatever moved into slot 1.
CHECK(res.writes.size() == 2);
// C's OWN captured `false` landed at slot 1, not B's dropped `true`.
CHECK(hasWrite(res, 1, false));
// The drop is reportable, not silent.
const std::string msg = describeFxRestoreDrops(res.drops, /*trackCount=*/1);
@@ -107,15 +110,17 @@ static void testDeletedFxDropsExplicitlyAndTouchesNothingElse() {
CHECK(describeFxRestoreDrops(FxRestoreDrops{}, 0).empty());
}
static void testDescribeNamesBothDropKinds() {
static void testDescribeNamesAllThreeDropKinds() {
FxRestoreDrops drops;
drops.missingIdentity = 2;
drops.unidentified = 1;
drops.slotOutOfRange = 3;
CHECK(drops.total() == 5);
CHECK(drops.total() == 6);
const std::string msg = describeFxRestoreDrops(drops, /*trackCount=*/2);
CHECK(msg.find("5 captured FX offline state(s) on 2 track(s)") != std::string::npos);
CHECK(msg.find("6 captured FX offline state(s) on 2 track(s)") != std::string::npos);
CHECK(msg.find("2 FX no longer in the chain") != std::string::npos);
CHECK(msg.find("1 FX REAPER could not identify at capture time") != std::string::npos);
CHECK(msg.find("3 from a project saved before FX identity was recorded")
!= std::string::npos);
}
@@ -180,6 +185,10 @@ static void testSlotKeyedSnapshotRestoresByPositionAndBoundsChecks() {
static void testUnresolvableIdentityNeverFallsBackToItsSlot() {
// An identity-keyed entry with NO identity (REAPER reported none at capture)
// is a drop — the slot it happens to carry must not be used as a substitute.
// Counted as `unidentified`, not `missingIdentity`: unlike a real captured
// identity going missing, this FX may still be sitting right there — we
// simply never had a name for it, and the report must say that, not "no
// longer in the chain".
std::vector<FxOfflineOp> ops = identityOps({{"{A}", false}});
ops.push_back(FxOfflineOp{"{TRACK}", FxKeying::Identity, "", /*slot=*/1, /*offline=*/true});
@@ -187,7 +196,12 @@ static void testUnresolvableIdentityNeverFallsBackToItsSlot() {
CHECK(res.writes.size() == 1);
CHECK(hasWrite(res, 0, false));
CHECK(!writesTouch(res, 1)); // {B} would have been the slot-1 victim
CHECK(res.drops.missingIdentity == 1);
CHECK(res.drops.missingIdentity == 0);
CHECK(res.drops.unidentified == 1);
const std::string msg = describeFxRestoreDrops(res.drops, /*trackCount=*/1);
CHECK(msg.find("1 FX REAPER could not identify at capture time") != std::string::npos);
CHECK(msg.find("no longer in the chain") == std::string::npos); // not this FX's story
}
static void testLiveFxWithNoIdentityIsNeverARestoreTarget() {
@@ -225,7 +239,7 @@ static void testDuplicateLiveIdentityResolvesToTheFirstSlotOnly() {
int main() {
testReorderedChainRestoresEachPluginItsOwnState();
testDeletedFxDropsExplicitlyAndTouchesNothingElse();
testDescribeNamesBothDropKinds();
testDescribeNamesAllThreeDropKinds();
testAddedFxIsNotTouched();
testTwoInstancesOfOnePluginKeyIndependently();
testSlotKeyedSnapshotRestoresByPositionAndBoundsChecks();