fix(bank_panel): drive new-content baseline re-arm from persist's load signal

The detector re-armed its GuidBaseline on a pointer compare (proj != lastProject),
a weaker signal than persist's GUID-primary identity. On a load onto a recycled
ReaProject* the baseline never reset, so the just-loaded project's pre-existing
tracks diffed against the previous project and were mass-tagged into the active
mode — opening a Design-saved project mis-tagged its Arrange tracks. Re-arm now
rides persist's authoritative load signal via bankPanelNotifyProjectLoaded().
This commit is contained in:
2026-07-23 19:26:06 -04:00
parent 938c85a223
commit 02b6350dc9
4 changed files with 120 additions and 16 deletions
+57
View File
@@ -124,6 +124,61 @@ static void testResetReBaselines() {
CHECK(p2new.size() == 1 && has(p2new, "{W}"));
}
// -- 6. Reload-mis-tag regression: a project LOAD must re-baseline before the first
// post-load observe, so the newly-loaded project's PRE-EXISTING content is never
// reported as new. This locks the exact failure behind the reload-mis-tag bug:
// the detector used to re-arm on a `proj != lastProject` pointer compare, which a
// recycled ReaProject* address defeats; the previous project's stale baseline then
// reported the whole just-loaded project as new content and it got mass-tagged into
// the active mode. The fix routes the re-arm through persist's authoritative load
// signal (bankPanelNotifyProjectLoaded -> reset()), modeled here as: on a load,
// reset() runs BEFORE the first observe of the new project's set.
//
// The seam under test is GuidBaseline; the shell wiring (main.cpp notify ->
// bank_panel reset()) is DAW-verified, but the load-then-observe DECISION lives
// here and is what the bug got wrong.
static void testReloadReBaselinesBeforeFirstObserve() {
// Project A is open and settled: its content is the baseline, steady state reports
// nothing new. This is the "extension already running against project A" precondition
// the bug needs (a NON-empty stale baseline to mis-diff the next project against).
GuidBaseline b;
b.observe({"{A1}", "{A2}"}); // A baseline (first-poll guard)
CHECK(b.observe({"{A1}", "{A2}"}).empty()); // steady: nothing new
CHECK(b.primed());
// Daniel opens project B (saved in Design). B's pre-existing tracks are an ENTIRELY
// different GUID set from A. persist raises its load signal; the fix calls reset()
// (via bankPanelNotifyProjectLoaded) BEFORE the first post-load observe.
b.reset();
auto afterLoad = b.observe({"{B1}", "{B2}", "{B3}"});
// The load must tag NOTHING: B's pre-existing content is the baseline, not "new".
// Untagged/Arrange leaves stay Arrange; nothing is mass-tagged into Design.
CHECK(afterLoad.empty());
// And genuine post-load creation in B is still detected (the fix must not deafen the
// detector — only suppress the pre-existing set at the load boundary).
auto createdInB = b.observe({"{B1}", "{B2}", "{B3}", "{B4}"});
CHECK(createdInB.size() == 1 && has(createdInB, "{B4}"));
}
// -- 6b. Negative control: WITHOUT the load re-baseline (the old pointer-miss path where
// reset() never fired), the just-loaded project's pre-existing content IS reported
// as new — i.e. it would be mass-tagged. This proves the assertion in test 6 is
// load-bearing (the reset() is what prevents the mis-tag), not self-affirming.
static void testMissingReBaselineWouldMisTag() {
GuidBaseline b;
b.observe({"{A1}", "{A2}"}); // A baseline
b.observe({"{A1}", "{A2}"}); // settled against A
// Simulate the BUG: no reset() on the load (the pointer compare missed a recycled
// ReaProject*). The next observe diffs B's set against A's stale baseline.
auto misdetected = b.observe({"{B1}", "{B2}", "{B3}"});
// Every one of B's pre-existing tracks looks "new" — exactly the mass-tag that
// parked the Arrange tracks into Design on open. This is the failure the fix removes.
CHECK(misdetected.size() == 3);
CHECK(has(misdetected, "{B1}") && has(misdetected, "{B2}") && has(misdetected, "{B3}"));
}
int main() {
testNewGuidsDifference();
testNewGuidsIgnoresEmpty();
@@ -131,6 +186,8 @@ int main() {
testBaselineIncremental();
testBaselineDeletionReDetect();
testResetReBaselines();
testReloadReBaselinesBeforeFirstObserve();
testMissingReBaselineWouldMisTag();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;