// Standalone tests for reasampler::tracking's consolidated answers — no REAPER, no // framework. This is the safety-critical file in the territory: it proves that the // prune's protected set and the resample's replace-vs-add decision come out of ONE // state, that neither ever answers destructively from ambiguity, and that the // replace-vs-add universe is a strict subset of the prune-protection universe. // // The prune half is composed end-to-end against the real pure core // (prune_reconcile + BankBook) rather than a mock, so "a tied usage can never reach // the orphan set" is proven at the layer that actually deletes. #include "../src/core/tracking/tracking_authority.h" #include #include #include #include #include #include "../src/core/model/bank_book.h" #include "../src/core/reclaim/prune_reconcile.h" using namespace reasampler; using namespace reasampler::tracking; using reasampler::wire::DecodedUsage; using reasampler::wire::UsageFoldResult; using reasampler::wire::UsageHold; using reasampler::wire::UsageRecord; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // -- fixtures ---------------------------------------------------------------- static OriginRecord originOf(const std::string& path, OriginKind kind, const std::string& id = "", const std::string& parent = "") { OriginRecord r; r.relativePath = path; r.kind = kind; r.sampleId = id; r.parentSampleId = parent; return r; } static DecodedUsage usage(const std::string& key, const std::string& trackGuid, const std::vector& holds, bool unioned = false) { UsageRecord rec; rec.trackGuid = trackGuid; rec.ownerNonce = key + "-nonce"; rec.unioned = unioned; rec.holds = holds; return DecodedUsage{key, rec}; } static DecodedUsage unreadable(const std::string& key) { return DecodedUsage{key, std::nullopt}; } static UsageFoldResult foldLive(const std::vector& decoded, const std::vector& liveTracks) { const std::unordered_set live(liveTracks.begin(), liveTracks.end()); return wire::foldUsageRecords(decoded, live, !live.empty()); } static bool contains(const std::vector& v, const std::string& s) { return std::find(v.begin(), v.end(), s) != v.end(); } // Adds a bank entry so the path lands in BankBook::referencedPaths(). static void addToBank(BankBook& book, const std::string& id, const std::string& path) { model::Sample s; s.id = id; s.displayName = id; s.relativePath = path; book.activeIndex().add(s); } // -- 1. the prune protected set ---------------------------------------------- // Live-held protected; project-referenced protected; foreign untouchable; // system-owned orphan reclaimable — all four from one computation. static void testPruneProtectedSet() { // On disk: a referenced capture, a live-held capture, a foreign file, and a // system-owned orphan. const std::vector present = { "bank/referenced.wav", "bank/held.wav", "bank/foreign.wav", "bank/orphan.wav"}; BankBook book; addToBank(book, "S-ref", "bank/referenced.wav"); OriginLedger ledger; ledger.record(originOf("bank/referenced.wav", OriginKind::Capture, "S-ref")); ledger.record(originOf("bank/held.wav", OriginKind::Capture, "S-held")); ledger.record(originOf("bank/orphan.wav", OriginKind::Capture, "S-orphan")); // "bank/foreign.wav" is deliberately absent — the system did not create it. const UsageFoldResult fold = foldLive( {usage("rsusage_A", "{T1}", {UsageHold{"S-held", "bank/held.wav"}})}, {"{T1}"}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; const ProtectionAnswer answer = pruneProtection(state); CHECK(!answer.blocked); const std::vector orphans = reclaim::pruneOrphans( present, reclaim::mergeReferenced(book.referencedPaths(), answer.heldPaths), answer.ownedPaths); CHECK(orphans.size() == 1); CHECK(contains(orphans, "bank/orphan.wav")); // system-owned, unreferenced CHECK(!contains(orphans, "bank/referenced.wav")); // a bank references it CHECK(!contains(orphans, "bank/held.wav")); // a live instance holds it CHECK(!contains(orphans, "bank/foreign.wav")); // never system-created } // A capture whose bank entry was REMOVED while an instance kept playing it is still // protected — the held-path union, not the index, is what saves it. static void testLiveHoldProtectsDeReferencedCapture() { const std::vector present = {"bank/held.wav"}; BankBook book; // no entry at all OriginLedger ledger; ledger.record(originOf("bank/held.wav", OriginKind::Capture, "S-held")); const UsageFoldResult fold = foldLive( {usage("rsusage_A", "{T1}", {UsageHold{"S-held", "bank/held.wav"}})}, {"{T1}"}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; const ProtectionAnswer answer = pruneProtection(state); const std::vector orphans = reclaim::pruneOrphans( present, reclaim::mergeReferenced(book.referencedPaths(), answer.heldPaths), answer.ownedPaths); CHECK(orphans.empty()); } // -- 2. unreadable tracking state blocks the prune --------------------------- static void testUnreadableUsageBlocksPruneAndNamesIt() { OriginLedger ledger; ledger.record(originOf("bank/orphan.wav", OriginKind::Capture, "S-1")); const UsageFoldResult fold = foldLive( {usage("rsusage_A", "{T1}", {UsageHold{"S-x", "bank/x.wav"}}), unreadable("rsusage_BROKEN")}, {"{T1}"}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; const ProtectionAnswer answer = pruneProtection(state); CHECK(answer.blocked); CHECK(!answer.ledgerUnreadable); CHECK(!answer.ledgerFutureVersion); CHECK(answer.unreadableUsageKeys.size() == 1); CHECK(answer.unreadableUsageKeys[0] == "rsusage_BROKEN"); // The belt-and-braces guard is symmetric: a usage-only block ALSO withholds // ownedPaths, even though the ledger itself read fine, so a caller that ignored // `blocked` computes an empty orphan set rather than deleting with degraded // protection. CHECK(answer.ownedPaths.empty()); CHECK(reclaim::pruneOrphans({"bank/orphan.wav"}, {}, answer.ownedPaths).empty()); // heldPaths is the one list that stays populated on a block — it only ever widens // the protected set, so withholding it would be the unsafe direction. CHECK(contains(answer.heldPaths, "bank/x.wav")); } // A ledger written by a NEWER build blocks exactly like a corrupt one, but is // reported separately: the operator advice differs (clearing a corrupt blob is // repair; clearing a newer build's is destruction). static void testFutureVersionLedgerBlocksAndIsReportedSeparately() { const LedgerLoad load = loadLedger("{\"v\":99,\"records\":[]}"); CHECK(load.status == LedgerStatus::FutureVersion); OriginLedger populated; populated.record(originOf("bank/would-be-orphan.wav", OriginKind::Capture, "S-1")); const UsageFoldResult fold = foldLive({}, {}); const TrackingState state{load.status, populated, fold}; const ProtectionAnswer answer = pruneProtection(state); CHECK(answer.blocked); CHECK(answer.ledgerFutureVersion); CHECK(!answer.ledgerUnreadable); CHECK(answer.ownedPaths.empty()); CHECK(reclaim::pruneOrphans({"bank/would-be-orphan.wav"}, {}, answer.ownedPaths).empty()); // And the replace-vs-add question abstains rather than allowing a replace. CHECK(tiedUsageExists(state, "bank/would-be-orphan.wav", "") == Answer::Indeterminate); } // An unreadable ledger blocks too, AND leaves ownedPaths empty — so a caller that // ignored `blocked` still computes an empty orphan set rather than deleting. static void testUnreadableLedgerBlocksAndYieldsNoOrphans() { // Deliberately NON-empty: loadLedger() hands back an empty ledger on Unreadable, // so an empty fixture here would assert nothing. The guard must suppress records // it was given, not merely pass an empty vector through. OriginLedger populated; populated.record(originOf("bank/would-be-orphan.wav", OriginKind::Capture, "S-1")); const UsageFoldResult fold = foldLive({}, {}); const TrackingState state{LedgerStatus::Unreadable, populated, fold}; const ProtectionAnswer answer = pruneProtection(state); CHECK(answer.blocked); CHECK(answer.ledgerUnreadable); CHECK(answer.ownedPaths.empty()); // Belt-and-braces: the orphan set computed from this answer is empty even // though the file is present, unreferenced, and recorded as owned. const std::vector orphans = reclaim::pruneOrphans( {"bank/would-be-orphan.wav"}, {}, answer.ownedPaths); CHECK(orphans.empty()); } // Both blockers at once must both be reported — the operator needs to fix both. static void testBothBlockersReported() { const OriginLedger empty; const UsageFoldResult fold = foldLive( {usage("rsusage_A", "{T1}", {UsageHold{"S-held", "bank/held.wav"}}), unreadable("rsusage_BROKEN")}, {"{T1}"}); const TrackingState state{LedgerStatus::Unreadable, empty, fold}; const ProtectionAnswer answer = pruneProtection(state); CHECK(answer.blocked); CHECK(answer.ledgerUnreadable); CHECK(answer.unreadableUsageKeys.size() == 1); // heldPaths survives a double block: the fold's protect-all set only ever widens // what prune protects, so withholding it would be the unsafe direction. CHECK(contains(answer.heldPaths, "bank/held.wav")); CHECK(answer.ownedPaths.empty()); } // A record that exists but whose track hosts no identified instance still protects // everything (the zero-identified net) — the prune runs, but reclaims nothing held. static void testZeroIdentifiedInstancesStillProtects() { OriginLedger ledger; ledger.record(originOf("bank/held.wav", OriginKind::Capture, "S-held")); const UsageFoldResult fold = foldLive( {usage("rsusage_A", "{T-GONE}", {UsageHold{"S-held", "bank/held.wav"}})}, {}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; const ProtectionAnswer answer = pruneProtection(state); CHECK(!answer.blocked); CHECK(contains(answer.heldPaths, "bank/held.wav")); const std::vector orphans = reclaim::pruneOrphans({"bank/held.wav"}, reclaim::mergeReferenced({}, answer.heldPaths), answer.ownedPaths); CHECK(orphans.empty()); } // -- 3. unreadable state never takes the replace branch ---------------------- static void testUnreadableStateNeverAnswersNo() { OriginLedger ledger; ledger.record(originOf("bank/a.wav", OriginKind::Capture, "S-a")); // Unreadable usage record. const UsageFoldResult brokenUsage = foldLive({unreadable("rsusage_BROKEN")}, {"{T1}"}); const TrackingState usageBad{LedgerStatus::Loaded, ledger, brokenUsage}; CHECK(tiedUsageExists(usageBad, "bank/a.wav", "") == Answer::Indeterminate); // Unreadable ledger. const OriginLedger empty; const UsageFoldResult okUsage = foldLive({}, {}); const TrackingState ledgerBad{LedgerStatus::Unreadable, empty, okUsage}; CHECK(tiedUsageExists(ledgerBad, "bank/a.wav", "") == Answer::Indeterminate); // An empty capture path is a caller error, not a licence to replace. const TrackingState good{LedgerStatus::Loaded, ledger, okUsage}; CHECK(tiedUsageExists(good, "", "") == Answer::Indeterminate); } // -- 4. the tie itself -------------------------------------------------------- static void testTiedUsageYesNoAndSelfExclusion() { OriginLedger ledger; ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src")); ledger.record(originOf("bank/lonely.wav", OriginKind::Capture, "S-lonely")); const UsageFoldResult fold = foldLive( {usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}}), usage("rsusage_OTHER", "{T2}", {UsageHold{"S-src", "bank/src.wav"}})}, {"{T1}", "{T2}"}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; // Counting every holder: a tie exists. CHECK(tiedUsageExists(state, "bank/src.wav", "") == Answer::Yes); // Excluding my own key still leaves the other instance's tie. CHECK(tiedUsageExists(state, "bank/src.wav", "rsusage_ME") == Answer::Yes); // Nobody holds this one. CHECK(tiedUsageExists(state, "bank/lonely.wav", "") == Answer::No); } // Sole holder excluding itself: definitively No, so the bake may replace in place. static void testSoleHolderExcludingItselfAnswersNo() { OriginLedger ledger; ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src")); const UsageFoldResult fold = foldLive( {usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}})}, {"{T1}"}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; CHECK(tiedUsageExists(state, "bank/src.wav", "rsusage_ME") == Answer::No); } static void testUnionedRecordIsNeverExcludedAsOwn() { OriginLedger ledger; ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src")); const UsageFoldResult fold = foldLive( {usage("rsusage_ME", "{T1}", {UsageHold{"S-src", "bank/src.wav"}}, /*unioned=*/true)}, {"{T1}"}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; CHECK(tiedUsageExists(state, "bank/src.wav", "rsusage_ME") == Answer::Yes); } // -- 5. no silent gaps -------------------------------------------------------- // A system-created file simulated through the same recordCreated path is tracked // the instant it exists: an immediately-following prune sees it as owned (and so // reclaimable, not foreign), and an immediately-following second creation is // unaffected. Its lineage is queryable with no intervening save/load. static void testCreateThenImmediatePruneAndSecondCreate() { OriginLedger ledger; const UsageFoldResult noUsage = foldLive({}, {}); // Create #1 — a resample carrying lineage from birth. ledger.record(originOf("bank/bake-1.wav", OriginKind::Resample, "S-1", "S-src")); { const TrackingState state{LedgerStatus::Loaded, ledger, noUsage}; const ProtectionAnswer answer = pruneProtection(state); CHECK(!answer.blocked); // Tracked instantly: it is owned, so it is reclaimable rather than foreign. CHECK(contains(answer.ownedPaths, "bank/bake-1.wav")); const std::vector orphans = reclaim::pruneOrphans({"bank/bake-1.wav"}, {}, answer.ownedPaths); CHECK(orphans.size() == 1); // Lineage present with nothing in between. CHECK(ledger.find("bank/bake-1.wav")->parentSampleId == "S-src"); } // Create #2 immediately after — both records intact, neither disturbed. ledger.record(originOf("bank/bake-2.wav", OriginKind::Resample, "S-2", "S-1")); { const TrackingState state{LedgerStatus::Loaded, ledger, noUsage}; const ProtectionAnswer answer = pruneProtection(state); CHECK(contains(answer.ownedPaths, "bank/bake-1.wav")); CHECK(contains(answer.ownedPaths, "bank/bake-2.wav")); CHECK(ledger.find("bank/bake-1.wav")->parentSampleId == "S-src"); CHECK(ledger.find("bank/bake-2.wav")->parentSampleId == "S-1"); } } // -- 6. the pre-existing bank ------------------------------------------------- // Lifted from a legacy path-only manifest: protections intact, no spurious lineage, // and the tie query gives a definite answer rather than abstaining. static void testPreExistingBankKeepsProtectionsAndAnswersDefinitely() { const LedgerLoad load = loadLedger("{\"owned\":[\"bank/legacy.wav\"]}"); CHECK(load.status == LedgerStatus::Loaded); CHECK(load.ledger.find("bank/legacy.wav")->parentSampleId.empty()); // Foreign files stay untouchable and the legacy file is still reclaimable when // nothing references it — exactly the protections it had before the lift. const UsageFoldResult noUsage = foldLive({}, {}); const TrackingState state{load.status, load.ledger, noUsage}; const ProtectionAnswer answer = pruneProtection(state); CHECK(!answer.blocked); const std::vector orphans = reclaim::pruneOrphans( {"bank/legacy.wav", "bank/handdropped.wav"}, {}, answer.ownedPaths); CHECK(orphans.size() == 1); CHECK(contains(orphans, "bank/legacy.wav")); CHECK(!contains(orphans, "bank/handdropped.wav")); // Never-recorded is decidable, not indeterminate. CHECK(tiedUsageExists(state, "bank/legacy.wav", "") == Answer::No); CHECK(tiedUsageExists(state, "bank/handdropped.wav", "") == Answer::No); // And a live hold on the legacy file still ties, lineage record or not. const UsageFoldResult held = foldLive( {usage("rsusage_A", "{T1}", {UsageHold{"S-legacy", "bank/legacy.wav"}})}, {"{T1}"}); const TrackingState heldState{load.status, load.ledger, held}; CHECK(tiedUsageExists(heldState, "bank/legacy.wav", "") == Answer::Yes); } // -- 7. the consumers cannot disagree ---------------------------------------- // Every Yes from the tie query is a path the prune protects, over a constructed // record set that mixes live, dead, self-held and unheld paths. The reverse does // NOT hold — that asymmetry is the point, so it is asserted too. static void testTiedUniverseIsStrictSubsetOfProtectedUniverse() { BankBook book; addToBank(book, "S-ref", "bank/bank-only.wav"); OriginLedger ledger; for (const char* p : {"bank/live-a.wav", "bank/live-b.wav", "bank/self.wav", "bank/dead.wav", "bank/bank-only.wav", "bank/unused.wav"}) ledger.record(originOf(p, OriginKind::Capture, std::string("S") + p)); const UsageFoldResult fold = foldLive( {usage("rsusage_A", "{T1}", {UsageHold{"S1", "bank/live-a.wav"}, UsageHold{"S2", "bank/live-b.wav"}}), usage("rsusage_ME", "{T1}", {UsageHold{"S3", "bank/self.wav"}}), usage("rsusage_DEAD", "{T-GONE}", {UsageHold{"S4", "bank/dead.wav"}})}, {"{T1}"}); const TrackingState state{LedgerStatus::Loaded, ledger, fold}; const ProtectionAnswer answer = pruneProtection(state); CHECK(!answer.blocked); const std::vector referenced = reclaim::mergeReferenced(book.referencedPaths(), answer.heldPaths); const std::vector universe = { "bank/live-a.wav", "bank/live-b.wav", "bank/self.wav", "bank/dead.wav", "bank/bank-only.wav", "bank/unused.wav"}; std::size_t tiedCount = 0; for (const std::string& path : universe) { const Answer tied = tiedUsageExists(state, path, "rsusage_ME"); if (tied != Answer::Yes) continue; ++tiedCount; // Yes => protected: the path is in the referenced union, so pruneOrphans // cannot emit it even though it is present and owned. CHECK(contains(referenced, path)); CHECK(reclaim::pruneOrphans({path}, referenced, answer.ownedPaths).empty()); } CHECK(tiedCount == 2); // live-a, live-b — self is excluded, dead is not live // Strictly narrower: bank-only.wav is protected (a bank references it) and // self.wav is protected (a live instance holds it), yet neither is a tie. CHECK(contains(referenced, "bank/bank-only.wav")); CHECK(tiedUsageExists(state, "bank/bank-only.wav", "rsusage_ME") == Answer::No); CHECK(contains(referenced, "bank/self.wav")); CHECK(tiedUsageExists(state, "bank/self.wav", "rsusage_ME") == Answer::No); } int main() { testPruneProtectedSet(); testLiveHoldProtectsDeReferencedCapture(); testUnreadableUsageBlocksPruneAndNamesIt(); testUnreadableLedgerBlocksAndYieldsNoOrphans(); testFutureVersionLedgerBlocksAndIsReportedSeparately(); testBothBlockersReported(); testZeroIdentifiedInstancesStillProtects(); testUnreadableStateNeverAnswersNo(); testTiedUsageYesNoAndSelfExclusion(); testSoleHolderExcludingItselfAnswersNo(); testUnionedRecordIsNeverExcludedAsOwn(); testCreateThenImmediatePruneAndSecondCreate(); testPreExistingBankKeepsProtectionsAndAnswersDefinitely(); testTiedUniverseIsStrictSubsetOfProtectedUniverse(); if (g_fail == 0) std::printf("tracking_authority: all tests passed\n"); return g_fail == 0 ? 0 : 1; }