feat(view): D2 Wave 2 — apply managed-lane ops + timer-diff auto-tag new content

Drives fixed-lane C_LANEPLAYS for managed lanes only (name-keyed, ordinal-renumber
safe) in the view shell; diffs live track/item GUIDs on the panel timer to auto-tag
new content to the active mode, first-poll-guarded. Pure guid_diff + lane_keys
modules unit-tested; folds in Wave-1 polish.
This commit is contained in:
2026-07-23 17:32:22 -04:00
parent af5f7ee912
commit af34143107
11 changed files with 773 additions and 4 deletions
+137
View File
@@ -0,0 +1,137 @@
// Standalone tests for reasampler::newGuids + GuidBaseline — no REAPER, no test
// framework. Mirror of test_view_mode_model: iterate the hard logic outside the DAW.
//
// Covers (D2 Wave-2 new-content detection):
// 1. newGuids: current \ previous, empty-GUID filtering, determinism.
// 2. GuidBaseline first-poll guard: the first observe() after open reports NOTHING
// new (pre-existing content stays Arrange) and establishes the baseline.
// 3. Incremental detection: only GUIDs added since the prior observe() are returned.
// 4. Deletion drops from the baseline so a reused GUID is re-detected.
// 5. reset() (project switch) re-arms the first-poll guard: the next observe()
// re-baselines and reports nothing new — never diffs across projects.
#include "../src/guid_diff.h"
#include <cstdio>
#include <set>
#include <string>
#include <vector>
using namespace reasampler;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
static bool has(const std::vector<std::string>& v, const std::string& g) {
for (const auto& e : v) if (e == g) return true;
return false;
}
// -- 1. newGuids set difference ----------------------------------------------
static void testNewGuidsDifference() {
std::set<std::string> prev{"{A}", "{B}"};
std::set<std::string> cur{"{A}", "{B}", "{C}", "{D}"};
auto added = newGuids(prev, cur);
CHECK(added.size() == 2);
CHECK(has(added, "{C}"));
CHECK(has(added, "{D}"));
CHECK(!has(added, "{A}")); // pre-existing, not new
CHECK(!has(added, "{B}"));
// No change ⇒ nothing new.
CHECK(newGuids(cur, cur).empty());
// A removed GUID is not "new" (it is absent from current).
std::set<std::string> shrunk{"{A}"};
CHECK(newGuids(prev, shrunk).empty());
// Determinism: ascending set order.
std::set<std::string> p2;
std::set<std::string> c2{"{Z}", "{A}", "{M}"};
auto ordered = newGuids(p2, c2);
CHECK(ordered.size() == 3);
CHECK(ordered[0] == "{A}" && ordered[1] == "{M}" && ordered[2] == "{Z}");
}
static void testNewGuidsIgnoresEmpty() {
std::set<std::string> prev{"{A}"};
std::set<std::string> cur{"", "{A}", "{B}"}; // empty ⇒ a GUID-read failure
auto added = newGuids(prev, cur);
CHECK(added.size() == 1);
CHECK(has(added, "{B}"));
CHECK(!has(added, "")); // never tag an empty GUID
}
// -- 2. First-poll guard -----------------------------------------------------
static void testBaselineFirstPollReportsNothing() {
GuidBaseline b;
CHECK(!b.primed());
// First observe after open: pre-existing content must NOT be tagged.
auto first = b.observe({"{A}", "{B}", "{C}"});
CHECK(first.empty()); // nothing new at open
CHECK(b.primed());
}
// -- 3. Incremental detection ------------------------------------------------
static void testBaselineIncremental() {
GuidBaseline b;
b.observe({"{A}", "{B}"}); // baseline
auto t1 = b.observe({"{A}", "{B}", "{C}"});
CHECK(t1.size() == 1 && has(t1, "{C}")); // only the newly-added GUID
// Next tick with a further addition — earlier-added {C} is now baseline.
auto t2 = b.observe({"{A}", "{B}", "{C}", "{D}"});
CHECK(t2.size() == 1 && has(t2, "{D}"));
CHECK(!has(t2, "{C}"));
// A steady state reports nothing new.
CHECK(b.observe({"{A}", "{B}", "{C}", "{D}"}).empty());
}
// -- 4. Deletion drops from baseline; reused GUID re-detected ----------------
static void testBaselineDeletionReDetect() {
GuidBaseline b;
b.observe({"{A}", "{B}"});
// Delete {B}: not "new", and drops out of the baseline.
CHECK(b.observe({"{A}"}).empty());
// {B} reappears (REAPER reused the GUID or the user re-added) ⇒ detected again.
auto again = b.observe({"{A}", "{B}"});
CHECK(again.size() == 1 && has(again, "{B}"));
}
// -- 5. reset() re-arms the first-poll guard (project switch) -----------------
static void testResetReBaselines() {
GuidBaseline b;
b.observe({"{A}"}); // project 1 baseline
b.observe({"{A}", "{B}"}); // {B} detected in project 1
b.reset();
CHECK(!b.primed());
// Switching to project 2: its pre-existing content must NOT be mass-tagged even
// though those GUIDs were never seen before reset.
auto afterSwitch = b.observe({"{X}", "{Y}", "{Z}"});
CHECK(afterSwitch.empty()); // re-baselined, nothing new
CHECK(b.primed());
// Content created in project 2 after the switch IS detected.
auto p2new = b.observe({"{X}", "{Y}", "{Z}", "{W}"});
CHECK(p2new.size() == 1 && has(p2new, "{W}"));
}
int main() {
testNewGuidsDifference();
testNewGuidsIgnoresEmpty();
testBaselineFirstPollReportsNothing();
testBaselineIncremental();
testBaselineDeletionReDetect();
testResetReBaselines();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}
+69
View File
@@ -0,0 +1,69 @@
// Standalone tests for reasampler::lane_keys — no REAPER, no test framework. The pure
// managed/manual lane-name heuristic that resolves D2 design points #1 (auto-tag
// exemption) and #2 (durable lane identity vs ordinal renumber).
//
// Covers:
// 1. isManagedLaneName: only the "reasampler:" prefix is managed; everything else
// (empty, user comp names, near-miss prefixes) is manual.
// 2. managedLaneKey: managed name -> its durable key; manual/unnamed -> nullopt.
// 3. Round-trip: managedLaneKey(laneNameForMode(m)) == "reasampler:" + m, so the
// Wave-3 minting path and the read path cannot drift.
#include "../src/lane_keys.h"
#include <cstdio>
#include <string>
using namespace reasampler;
static int g_fail = 0;
#define CHECK(cond) do { if(!(cond)) { \
std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0)
static void testIsManagedLaneName() {
// Tool-minted managed names.
CHECK(isManagedLaneName("reasampler:design"));
CHECK(isManagedLaneName("reasampler:arrange"));
CHECK(isManagedLaneName("reasampler:")); // prefix alone still ours (odd but managed)
// Manual / user lanes are never managed.
CHECK(!isManagedLaneName("")); // unnamed lane ⇒ manual
CHECK(!isManagedLaneName("Comp 1")); // user comp lane
CHECK(!isManagedLaneName("Lead vocal"));
CHECK(!isManagedLaneName("reasample")); // near-miss, no colon ⇒ not ours
CHECK(!isManagedLaneName("Reasampler:design")); // case-sensitive prefix
CHECK(!isManagedLaneName(" reasampler:x")); // leading space ⇒ not a prefix match
}
static void testManagedLaneKey() {
// Managed lane: the durable name IS the key.
auto k = managedLaneKey("reasampler:design");
CHECK(k.has_value() && *k == "reasampler:design");
// Manual / unnamed lanes have no managed key (⇒ treated as manual, never driven).
CHECK(!managedLaneKey("").has_value());
CHECK(!managedLaneKey("Comp 1").has_value());
CHECK(!managedLaneKey("guitar-double").has_value());
}
static void testRoundTrip() {
// Minting then reading must agree: managedLaneKey(laneNameForMode(m)) recovers the
// prefixed name for every mode id.
for (const std::string mode : {std::string("arrange"), std::string("design"),
std::string("mixdown")}) {
const std::string name = laneNameForMode(mode);
CHECK(name == "reasampler:" + mode);
CHECK(isManagedLaneName(name));
auto key = managedLaneKey(name);
CHECK(key.has_value() && *key == "reasampler:" + mode);
}
}
int main() {
testIsManagedLaneName();
testManagedLaneKey();
testRoundTrip();
if (g_fail == 0) std::printf("All tests passed.\n");
return g_fail ? 1 : 0;
}
+35
View File
@@ -940,6 +940,40 @@ static void testLaneOwnershipIndex() {
CHECK(!idx.remove("{T}", "l0"));
}
// -- D2.2b Last-writer-wins ownership replace (round-trip) --------------------
//
// setManual then setManaged on the SAME (guid, laneKey) must leave EXACTLY ONE
// managed entry — the ownership record is replaced, not accumulated. Guards the
// "retag a lane the tool now owns" contract and its persistence: the replace must
// survive a serialize/deserialize round-trip with no stray manual duplicate.
static void testLaneOwnershipLastWriterWins() {
ViewModeModel vm;
// Manual first, then managed on the same lane — the managed write replaces.
CHECK(vm.lanes().setManual("{T}", "l0"));
CHECK(vm.lanes().setManaged("{T}", "l0", kDesignModeId));
CHECK(vm.lanes().size() == 1); // one entry, not two
const LaneOwnership* o = vm.lanes().query("{T}", "l0");
CHECK(o && o->isManaged() && *o->managedMode == kDesignModeId);
// The reverse also replaces: managed -> manual leaves exactly one manual entry.
CHECK(vm.lanes().setManual("{T}", "l0"));
CHECK(vm.lanes().size() == 1);
const LaneOwnership* m = vm.lanes().query("{T}", "l0");
CHECK(m && m->isManual());
// Back to managed, then round-trip: exactly one managed entry survives, no stray
// manual duplicate resurrected by (de)serialization.
CHECK(vm.lanes().setManaged("{T}", "l0", kArrangeModeId));
auto back = ViewModeModel::deserialize(vm.serialize());
CHECK(back.has_value());
if (back) {
CHECK(back->lanes().size() == 1);
const LaneOwnership* r = back->lanes().query("{T}", "l0");
CHECK(r && r->isManaged() && *r->managedMode == kArrangeModeId);
}
}
// -- D2.3/D2.4 Managed-only: planner + query never emit a manual lane --------
//
// Required case: a track with a manual lane + managed mode lanes — neither the planner
@@ -1124,6 +1158,7 @@ int main() {
// D2 two-canvas lane extension
testLaneModeStateAndPlayValues();
testLaneOwnershipIndex();
testLaneOwnershipLastWriterWins();
testManagedOnlyPlannerAndQuery();
testAutoTagDecision();
testLaneJsonRoundTrip();