fix: classify future-version ledgers with changed record shape correctly, not as corrupt
Version-check now runs on the parseLedger failure path too, so a v3 blob whose record shape actually changed reports FutureVersion instead of Unreadable, avoiding the corrupt-blob "clear it" advice. Also closes the six minor findings.
This commit is contained in:
@@ -258,6 +258,17 @@ static void testFutureVersionIsNeitherLoadedNorMalformed() {
|
||||
CHECK(loadLedger("{\"v\":2,\"records\":[]}").status == LedgerStatus::Loaded);
|
||||
CHECK(loadLedger("{\"v\":1,\"owned\":[\"a.wav\"]}").status == LedgerStatus::Loaded);
|
||||
CHECK(loadLedger("{\"v\":-1,\"records\":[]}").status == LedgerStatus::Unreadable);
|
||||
|
||||
// A future version whose record shape actually changed — not just grew — fails
|
||||
// parseRecord under these v2 rules ("kind" retyped from int to string). That must
|
||||
// still classify as FutureVersion, not Unreadable: this is the exact "changed
|
||||
// record shape" case the version ladder comment says v3 means, and Unreadable
|
||||
// would steer the operator into clearing a newer build's ledger.
|
||||
CHECK(loadLedger("{\"v\":3,\"records\":[{\"path\":\"a.wav\",\"kind\":\"loud\"}]}")
|
||||
.status == LedgerStatus::FutureVersion);
|
||||
// Same, with the array itself retyped to an object.
|
||||
CHECK(loadLedger("{\"v\":3,\"records\":{\"path\":\"a.wav\"}}").status ==
|
||||
LedgerStatus::FutureVersion);
|
||||
}
|
||||
|
||||
// A hand-edited or corrupt blob cannot smuggle an absolute or duplicate path past
|
||||
@@ -315,8 +326,11 @@ static void testLegacyPathOnlyManifestLiftsIn() {
|
||||
|
||||
// A recapture regenerates the audio behind ONE bank id under a NEW file name
|
||||
// (makeUniqueTag guarantees it), so the ledger legitimately ends up holding two
|
||||
// records with the same sampleId and different paths. The original's lineage survives
|
||||
// untouched — the Sample was rewritten in place, the birth record was not.
|
||||
// records with the same sampleId and different paths. This proves that shape and
|
||||
// that take-1's record is untouched by take-2's insert (record() never overwrites —
|
||||
// see testLineageIsNeverBackfilled for the same-path case). It does NOT exercise the
|
||||
// ledger-wins-over-the-mutable-Sample claim: nothing here reads Sample.provenance, and
|
||||
// no production code reads OriginRecord.parentSampleId today.
|
||||
static void testRecaptureAddsSecondRecordUnderTheSameSampleId() {
|
||||
OriginLedger l;
|
||||
l.record(rec("bank/take-1.wav", OriginKind::Capture, "S-1", "S-parent"));
|
||||
@@ -326,8 +340,6 @@ static void testRecaptureAddsSecondRecordUnderTheSameSampleId() {
|
||||
CHECK(l.size() == 2);
|
||||
CHECK(l.find("bank/take-1.wav")->sampleId == "S-1");
|
||||
CHECK(l.find("bank/take-2.wav")->sampleId == "S-1");
|
||||
// The ledger, not the (mutable) Sample, is authoritative for lineage: the
|
||||
// recapture's differing parent did not rewrite the original's.
|
||||
CHECK(l.find("bank/take-1.wav")->parentSampleId == "S-parent");
|
||||
CHECK(l.find("bank/take-1.wav")->kind == OriginKind::Capture);
|
||||
// Both stay owned, so the superseded file is reclaimable rather than foreign.
|
||||
|
||||
@@ -213,12 +213,16 @@ static void testUnreadableLedgerBlocksAndYieldsNoOrphans() {
|
||||
|
||||
// Both blockers at once must both be reported — the operator needs to fix both.
|
||||
static void testBothBlockersReported() {
|
||||
const OriginLedger empty;
|
||||
// Deliberately NON-empty, like testUnreadableLedgerBlocksAndYieldsNoOrphans: an
|
||||
// empty fixture asserts nothing about ownedPaths being withheld, only that it
|
||||
// started empty.
|
||||
OriginLedger populated;
|
||||
populated.record(originOf("bank/would-be-orphan.wav", OriginKind::Capture, "S-1"));
|
||||
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 TrackingState state{LedgerStatus::Unreadable, populated, fold};
|
||||
const ProtectionAnswer answer = pruneProtection(state);
|
||||
CHECK(answer.blocked);
|
||||
CHECK(answer.ledgerUnreadable);
|
||||
@@ -227,6 +231,12 @@ static void testBothBlockersReported() {
|
||||
// what prune protects, so withholding it would be the unsafe direction.
|
||||
CHECK(contains(answer.heldPaths, "bank/held.wav"));
|
||||
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<std::string> orphans = reclaim::pruneOrphans(
|
||||
{"bank/would-be-orphan.wav"}, {}, answer.ownedPaths);
|
||||
CHECK(orphans.empty());
|
||||
}
|
||||
|
||||
// A record that exists but whose track hosts no identified instance still protects
|
||||
|
||||
Reference in New Issue
Block a user