tracking: read the ledger's version, not just write it; clear owned on any block; channel-correct prune recovery

This commit is contained in:
2026-07-30 20:11:05 -04:00
parent 7f70d94228
commit 45b87dc2ff
27 changed files with 432 additions and 168 deletions
+45 -4
View File
@@ -148,8 +148,44 @@ static void testUnreadableUsageBlocksPruneAndNamesIt() {
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
@@ -178,12 +214,19 @@ static void testUnreadableLedgerBlocksAndYieldsNoOrphans() {
// Both blockers at once must both be reported — the operator needs to fix both.
static void testBothBlockersReported() {
const OriginLedger empty;
const UsageFoldResult fold = foldLive({unreadable("rsusage_BROKEN")}, {"{T1}"});
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
@@ -260,9 +303,6 @@ static void testSoleHolderExcludingItselfAnswersNo() {
CHECK(tiedUsageExists(state, "bank/src.wav", "rsusage_ME") == Answer::No);
}
// A `unioned` record carries more than one incarnation's holds, so it can never be
// attributed to a single owner — excluding it could hide a sibling's tie, which is
// the under-protecting direction.
static void testUnionedRecordIsNeverExcludedAsOwn() {
OriginLedger ledger;
ledger.record(originOf("bank/src.wav", OriginKind::Capture, "S-src"));
@@ -404,6 +444,7 @@ int main() {
testLiveHoldProtectsDeReferencedCapture();
testUnreadableUsageBlocksPruneAndNamesIt();
testUnreadableLedgerBlocksAndYieldsNoOrphans();
testFutureVersionLedgerBlocksAndIsReportedSeparately();
testBothBlockersReported();
testZeroIdentifiedInstancesStillProtects();
testUnreadableStateNeverAnswersNo();