tracking: append OriginKind::PackageImport as value 5

An appended field-vocabulary value, so kLedgerVersion stays 2 — pinned by a
test. Unknown kinds still degrade to Unknown with the ledger Loaded.
This commit is contained in:
2026-08-02 07:17:01 -04:00
parent 09a9ef838f
commit 35b2a3a151
3 changed files with 84 additions and 4 deletions
+80 -4
View File
@@ -2,9 +2,10 @@
//
// The record family behind file tracking. Covers: the relative-paths-only invariant,
// exact-string ownership, dedup, insertion order, the JSON round-trip (incl. golden
// byte literals over every persisted enum value), the no-backfill rule, the legacy
// path-only lift, and the Fresh / Loaded / Unreadable / FutureVersion classification
// that keeps never-recorded apart from the two degraded states.
// byte literals over every persisted enum value), the append-a-kind-without-moving-"v"
// rule and its degrade-don't-block twin, the no-backfill rule, the legacy path-only
// lift, and the Fresh / Loaded / Unreadable / FutureVersion classification that keeps
// never-recorded apart from the two degraded states.
#include "../src/core/tracking/origin_ledger.h"
@@ -162,13 +163,15 @@ static void testSerializeGoldenLiteralPinsEveryPersistedKind() {
l.record(rec("bank/ingest.wav", OriginKind::Ingest, "S-b"));
l.record(rec("bank/recapture.wav", OriginKind::Recapture, "S-c", "S-a"));
l.record(rec("bank/resample.wav", OriginKind::Resample, "S-d", "S-a"));
l.record(rec("bank/import.wav", OriginKind::PackageImport, "S-e"));
const std::string expected =
"{\"v\":2,\"records\":["
"{\"path\":\"bank/unknown.wav\",\"kind\":0,\"sample\":\"\",\"parent\":\"\"},"
"{\"path\":\"bank/capture.wav\",\"kind\":1,\"sample\":\"S-a\",\"parent\":\"\"},"
"{\"path\":\"bank/ingest.wav\",\"kind\":2,\"sample\":\"S-b\",\"parent\":\"\"},"
"{\"path\":\"bank/recapture.wav\",\"kind\":3,\"sample\":\"S-c\",\"parent\":\"S-a\"},"
"{\"path\":\"bank/resample.wav\",\"kind\":4,\"sample\":\"S-d\",\"parent\":\"S-a\"}"
"{\"path\":\"bank/resample.wav\",\"kind\":4,\"sample\":\"S-d\",\"parent\":\"S-a\"},"
"{\"path\":\"bank/import.wav\",\"kind\":5,\"sample\":\"S-e\",\"parent\":\"\"}"
"]}";
CHECK(l.serialize() == expected);
@@ -180,6 +183,76 @@ static void testSerializeGoldenLiteralPinsEveryPersistedKind() {
CHECK(back->find("bank/ingest.wav")->kind == OriginKind::Ingest);
CHECK(back->find("bank/recapture.wav")->kind == OriginKind::Recapture);
CHECK(back->find("bank/resample.wav")->kind == OriginKind::Resample);
CHECK(back->find("bank/import.wav")->kind == OriginKind::PackageImport);
CHECK(*back == l); // every field, not just the kind, survives the trip
}
// Appending a value to the kind vocabulary must NOT move the document version: the
// two rules sit side by side and pull in opposite directions — an unknown kind
// degrades, an unknown "v" blocks. Pinned on the emitted bytes rather than on the
// internal constant, because the byte is what an older build actually reads.
static void testAppendingAKindDoesNotMoveTheDocumentVersion() {
OriginLedger l;
l.record(rec("bank/import.wav", OriginKind::PackageImport, "S-e"));
const std::string json = l.serialize();
CHECK(json.rfind("{\"v\":2,", 0) == 0);
CHECK(loadLedger(json).status == LedgerStatus::Loaded);
// The ceiling did not move with it: v3 is still a future document shape.
CHECK(loadLedger("{\"v\":3,\"records\":[]}").status == LedgerStatus::FutureVersion);
}
// The other half of the append rule: a kind this build does NOT know degrades to
// Unknown while the ledger still loads and the path stays owned. Losing the kind
// detail costs nothing today — no consumer reads it — but an Unreadable here would
// block prune entirely on nothing worse than a vocabulary gap.
static void testUnknownKindDegradesWithoutBlockingTheLedger() {
const std::string blob =
"{\"v\":2,\"records\":["
"{\"path\":\"bank/next.wav\",\"kind\":6,\"sample\":\"S-1\",\"parent\":\"\"},"
"{\"path\":\"bank/far.wav\",\"kind\":99,\"sample\":\"S-2\",\"parent\":\"\"},"
"{\"path\":\"bank/bogus.wav\",\"kind\":-1,\"sample\":\"S-3\",\"parent\":\"\"}]}";
const LedgerLoad load = loadLedger(blob);
CHECK(load.status == LedgerStatus::Loaded);
CHECK(!ledgerDegraded(load.status));
CHECK(load.ledger.size() == 3);
CHECK(load.ledger.find("bank/next.wav")->kind == OriginKind::Unknown);
CHECK(load.ledger.find("bank/far.wav")->kind == OriginKind::Unknown);
CHECK(load.ledger.find("bank/bogus.wav")->kind == OriginKind::Unknown);
// Every path still owned, in order — the protection prune reads is untouched.
const std::vector<std::string> paths = load.ledger.ownedPaths();
CHECK(paths.size() == 3);
CHECK(paths[0] == "bank/next.wav");
CHECK(paths[1] == "bank/far.wav");
CHECK(paths[2] == "bank/bogus.wav");
// The rest of the record survives the degrade; only the kind is lost.
CHECK(load.ledger.find("bank/next.wav")->sampleId == "S-1");
}
// ownedPaths() is the ONLY thing pruneProtection reads out of a ledger, and it is
// kind-blind: two ledgers agreeing on paths and differing on every kind, new value
// included, yield identical protection input. Adding a kind therefore cannot change
// a prune decision for any existing kind.
static void testOwnedPathsAreKindIndependent() {
const OriginKind kinds[] = {OriginKind::Unknown, OriginKind::Capture,
OriginKind::Ingest, OriginKind::Recapture,
OriginKind::Resample, OriginKind::PackageImport};
const std::size_t n = sizeof(kinds) / sizeof(kinds[0]);
OriginLedger baseline;
OriginLedger varied;
for (std::size_t i = 0; i < n; ++i) {
const std::string path = "bank/f" + std::to_string(i) + ".wav";
baseline.record(rec(path, OriginKind::Unknown, "S"));
varied.record(rec(path, kinds[i], "S"));
}
CHECK(baseline.ownedPaths() == varied.ownedPaths());
CHECK(baseline.ownedPaths().size() == n);
CHECK(!(baseline == varied)); // the ledgers really do differ, kind by kind
}
// contains() is an EXACT-string predicate, never a prefix or substring match — the
@@ -383,6 +456,9 @@ int main() {
testRoundTripWithLineage();
testRoundTripWithJsonMetacharacters();
testSerializeGoldenLiteralPinsEveryPersistedKind();
testAppendingAKindDoesNotMoveTheDocumentVersion();
testUnknownKindDegradesWithoutBlockingTheLedger();
testOwnedPathsAreKindIndependent();
testMalformedParsesToNullopt();
testTrailingGarbageIsRejected();
testLegacyShapeTypeErrorsAreRejectedButEmptyIsValid();