tracking: one ledger, one authority — prune protection and replace-vs-add answered from the same records, fail-safe on unreadable state

This commit is contained in:
2026-07-30 19:44:11 -04:00
parent 7bd911d58b
commit 7f70d94228
40 changed files with 1546 additions and 633 deletions
+50 -17
View File
@@ -52,6 +52,12 @@ UsageRecord makeRecord(const std::string& trackGuid, const std::string& nonce,
return r;
}
// One enumerated key's read result. Keys are synthesized per index because the fold
// only uses them to name an offender; a nullopt record is the unreadable case.
DecodedUsage decodedOf(const std::string& key, std::optional<UsageRecord> rec) {
return DecodedUsage{key, std::move(rec)};
}
bool holdsContainPath(const std::vector<UsageHold>& holds, const std::string& path) {
for (const UsageHold& h : holds)
if (h.relativePath == path) return true;
@@ -349,36 +355,61 @@ static void testTakeFxAttributedRecordIsProtected() {
// delete nothing) — silently reduced protection is the delete direction.
static void testUnreadableRecordAbortsPrune() {
std::vector<std::optional<UsageRecord>> decoded;
decoded.push_back(makeRecord("{T1}", "N1", {UsageHold{"a", "pa.wav"}}));
decoded.push_back(std::nullopt); // one unreadable record among readable ones
std::vector<DecodedUsage> decoded;
decoded.push_back(decodedOf("rsusage_1", makeRecord("{T1}", "N1", {UsageHold{"a", "pa.wav"}})));
decoded.push_back(decodedOf("rsusage_BAD", std::nullopt)); // one unreadable among readable
const UsageFoldResult fold =
foldUsageRecords(decoded, std::unordered_set<std::string>{"{T1}"}, true);
CHECK(fold.abortPrune);
// The offender is named so the action can tell the operator which key to recover.
CHECK(fold.offendingKeys.size() == 1);
CHECK(fold.offendingKeys[0] == "rsusage_BAD");
// Attribution is exactly what an unreadable record destroys — no record may be
// reported as counted while one is unreadable.
CHECK(fold.counted.empty());
// All readable -> no abort, normal liveness fold.
std::vector<std::optional<UsageRecord>> ok;
ok.push_back(makeRecord("{T1}", "N1", {UsageHold{"a", "pa.wav"}}));
// All readable -> no abort, normal liveness fold, and the live record is attributed.
std::vector<DecodedUsage> ok;
ok.push_back(decodedOf("rsusage_1", makeRecord("{T1}", "N1", {UsageHold{"a", "pa.wav"}})));
const UsageFoldResult okFold =
foldUsageRecords(ok, std::unordered_set<std::string>{"{T1}"}, true);
CHECK(!okFold.abortPrune);
CHECK(okFold.offendingKeys.empty());
CHECK(okFold.heldPaths.size() == 1);
CHECK(okFold.heldPaths[0] == "pa.wav");
CHECK(okFold.counted.size() == 1);
CHECK(okFold.counted[0].key == "rsusage_1");
// Empty input (no records enumerated) -> empty, no abort.
const UsageFoldResult empty =
foldUsageRecords({}, std::unordered_set<std::string>{}, false);
CHECK(!empty.abortPrune);
CHECK(empty.heldPaths.empty());
CHECK(empty.counted.empty());
// Readable records + zero identified -> the protect-all net applies through the
// fold too (belt and braces with the abort).
std::vector<std::optional<UsageRecord>> unmatched;
unmatched.push_back(makeRecord("{T9}", "N1", {UsageHold{"a", "pa.wav"}}));
// fold too (belt and braces with the abort), and every record counts.
std::vector<DecodedUsage> unmatched;
unmatched.push_back(decodedOf("rsusage_9", makeRecord("{T9}", "N1", {UsageHold{"a", "pa.wav"}})));
const UsageFoldResult net =
foldUsageRecords(unmatched, std::unordered_set<std::string>{}, false);
CHECK(!net.abortPrune);
CHECK(net.heldPaths.size() == 1);
CHECK(net.counted.size() == 1);
}
// A dead-track record must be excluded from `counted` as well as from heldPaths —
// attribution and protection must name the same records or the two consumers drift.
static void testCountedMirrorsHeldPaths() {
std::vector<DecodedUsage> decoded;
decoded.push_back(decodedOf("rsusage_LIVE", makeRecord("{LIVE}", "N1", {UsageHold{"a", "pa.wav"}})));
decoded.push_back(decodedOf("rsusage_DEAD", makeRecord("{DEAD}", "N2", {UsageHold{"b", "pb.wav"}})));
const UsageFoldResult fold =
foldUsageRecords(decoded, std::unordered_set<std::string>{"{LIVE}"}, true);
CHECK(fold.heldPaths.size() == 1);
CHECK(fold.heldPaths[0] == "pa.wav");
CHECK(fold.counted.size() == 1);
CHECK(fold.counted[0].key == "rsusage_LIVE");
}
// --- abort returns the protect-all set (belt-and-braces) ---------------------------
@@ -388,10 +419,10 @@ static void testUnreadableRecordAbortsPrune() {
// delete-ward).
static void testAbortFoldReturnsProtectAllSet() {
// Two readable records + one unreadable (nullopt) in between.
std::vector<std::optional<UsageRecord>> decoded;
decoded.push_back(makeRecord("{T1}", "N1", {UsageHold{"a", "pa.wav"}}));
decoded.push_back(std::nullopt); // triggers abort
decoded.push_back(makeRecord("{T2}", "N2", {UsageHold{"b", "pb.wav"}}));
std::vector<DecodedUsage> decoded;
decoded.push_back(decodedOf("rsusage_1", makeRecord("{T1}", "N1", {UsageHold{"a", "pa.wav"}})));
decoded.push_back(decodedOf("rsusage_BAD", std::nullopt)); // triggers abort
decoded.push_back(decodedOf("rsusage_2", makeRecord("{T2}", "N2", {UsageHold{"b", "pb.wav"}})));
// Live: only {T1} — so without protect-all, T2's path would be excluded.
const UsageFoldResult fold =
@@ -409,8 +440,8 @@ static void testAbortFoldReturnsProtectAllSet() {
CHECK(hasPB);
// All nullopt (every key unreadable): abort + empty heldPaths (nothing readable).
std::vector<std::optional<UsageRecord>> allNull;
allNull.push_back(std::nullopt);
std::vector<DecodedUsage> allNull;
allNull.push_back(decodedOf("rsusage_BAD", std::nullopt));
const UsageFoldResult allNullFold =
foldUsageRecords(allNull, std::unordered_set<std::string>{}, false);
CHECK(allNullFold.abortPrune);
@@ -445,8 +476,9 @@ static void testTruncatedWalkProtectsAll() {
CHECK(hasB);
// Belt-and-braces: the same scenario through foldUsageRecords also protects all.
std::vector<std::optional<UsageRecord>> decoded;
for (const UsageRecord& r : records) decoded.push_back(r);
std::vector<DecodedUsage> decoded;
for (std::size_t i = 0; i < records.size(); ++i)
decoded.push_back(decodedOf("rsusage_" + std::to_string(i), records[i]));
const UsageFoldResult fold =
foldUsageRecords(decoded, std::unordered_set<std::string>{}, false);
CHECK(!fold.abortPrune);
@@ -561,6 +593,7 @@ int main() {
testHeldPathsDedupAndEmptyPathSkip();
testTakeFxAttributedRecordIsProtected();
testUnreadableRecordAbortsPrune();
testCountedMirrorsHeldPaths();
testAbortFoldReturnsProtectAllSet();
testTruncatedWalkProtectsAll();
testIdentityMatcher();