fix(pS-usage): close delete-ward residuals — remint on corrupt, named abort keys, truncation protect-all, abort->protect-all set

This commit is contained in:
2026-07-28 13:54:10 -04:00
parent a4aeb9dcc8
commit ec83f14738
8 changed files with 197 additions and 36 deletions
+92 -9
View File
@@ -7,15 +7,20 @@
//
// Covers: wire round-trip (nonce + unioned flag, empty / adversarial bytes),
// malformed -> nullopt, the publish plan's branches (fresh / clean replace + skip /
// sibling union with the sticky poison flag / cross-track re-mint / undecodable heal),
// sibling union with the sticky poison flag / cross-track re-mint / undecodable remint),
// the SAME-TRACK SIBLING repro (the review's 🔴#1 — byte-identical wire convergence
// must never let one sibling clean-replace the other's still-held paths, including one
// write later via the poison flag), the liveness fold (live, dead-track, empty-guid
// fallback, de-dup), the ZERO-IDENTIFIED protect-all net (🔴#2 — an identity-matcher
// failure must protect everything, not nothing), the UNREADABLE-record abort
// (foldUsageRecords.abortPrune — prune halts, deletes nothing), the pure identity
// matcher (UID hex / module filename base / display name, beta over-protect), and the
// composed pruneOrphans exclusion proof.
// (foldUsageRecords.abortPrune — prune halts, deletes nothing), the UNDECODABLE-EXISTING
// REMINT (corrupt key left in place — prune-side abort keeps firing while sibling holds
// unprotected), the ABORT→PROTECT-ALL belt-and-braces (foldUsageRecords.heldPaths is
// the full protect-all set even when abortPrune is set), the TRUNCATED-WALK→PROTECT-ALL
// proof (FX walk misses a nested instance → anyLive=false → usageHeldPaths protects
// every record — the pure side of the depth-cap + depth-exhaustion-is-container fix),
// the pure identity matcher (UID hex / module filename base / display name, beta
// over-protect), and the composed pruneOrphans exclusion proof.
#include "../src/sample_usage.h"
@@ -254,16 +259,20 @@ static void testPlanOwnRecordAfterTrackMove() {
CHECK(back->trackGuid == "{T2}");
}
static void testPlanUndecodableExisting() {
// An undecodable existing value under MY key is corruption — overwrite with mine
// (the self-heal; the prune side independently aborts while it is unreadable).
static void testPlanUndecodableExistingRemints() {
// An undecodable existing value under MY key must REMINT (not overwrite). Overwriting
// would clear the prune-side abort while a same-key sibling B's holds are unprotected
// until B republishes. Leaving the corrupt key in place keeps the prune-side abort
// (foldUsageRecords.abortPrune) firing so no delete-ward window opens.
const UsageRecord mine = makeRecord("{T1}", "NA", {UsageHold{"a", "pa.wav"}});
const UsagePublishPlan plan = planUsagePublish(std::string("corrupt"), mine);
CHECK(!plan.remint);
CHECK(plan.remint); // fresh key — leave the corrupt key untouched
CHECK(!plan.skipWrite);
// wire carries mine (to be written under the NEW key by the caller)
auto back = decodeUsageRecord(plan.wire);
CHECK(back.has_value());
CHECK(back->holds.size() == 1);
CHECK(!back->unioned); // fresh key, sole writer — un-poisoned
}
// --- liveness fold ---------------------------------------------------------------
@@ -370,6 +379,78 @@ static void testUnreadableRecordAbortsPrune() {
CHECK(net.heldPaths.size() == 1);
}
// --- abort returns the protect-all set (belt-and-braces) ---------------------------
// foldUsageRecords must return heldPaths = EVERY readable record's paths when
// abortPrune is set, so a future caller that forgets to check the flag before using
// heldPaths still gets maximum protection rather than an empty set (which would be
// 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"}}));
// Live: only {T1} — so without protect-all, T2's path would be excluded.
const UsageFoldResult fold =
foldUsageRecords(decoded, std::unordered_set<std::string>{"{T1}"}, true);
CHECK(fold.abortPrune);
// heldPaths must contain BOTH paths (protect-all over all readable records),
// not just {T1}'s path.
CHECK(fold.heldPaths.size() == 2);
bool hasPA = false, hasPB = false;
for (const std::string& p : fold.heldPaths) {
if (p == "pa.wav") hasPA = true;
if (p == "pb.wav") hasPB = true;
}
CHECK(hasPA);
CHECK(hasPB);
// All nullopt (every key unreadable): abort + empty heldPaths (nothing readable).
std::vector<std::optional<UsageRecord>> allNull;
allNull.push_back(std::nullopt);
const UsageFoldResult allNullFold =
foldUsageRecords(allNull, std::unordered_set<std::string>{}, false);
CHECK(allNullFold.abortPrune);
CHECK(allNullFold.heldPaths.empty()); // no readable records to protect
}
// --- truncated enumeration → protect-all -------------------------------------------
// The FX walk may truncate at kMaxContainerDepth, leaving a deeply-nested live instance
// missed. At the PURE layer this is indistinguishable from a genuine identity-matcher
// failure: anyInstanceLive stays false while records exist. The protect-all net in
// usageHeldPaths guarantees this resolves toward PROTECT, never toward delete — the same
// test shape as testZeroIdentifiedProtectsAll, stated here explicitly for the truncation
// failure mode.
static void testTruncatedWalkProtectsAll() {
// Records from two tracks that host instances; the FX walk (shell side) failed to
// identify ANY instance (e.g. truncated at depth, or a future matcher gap).
const std::vector<UsageRecord> records = {
makeRecord("{TRACK-A}", "N1", {UsageHold{"x", "nested-a.wav"}}),
makeRecord("{TRACK-B}", "N2", {UsageHold{"y", "nested-b.wav"}}),
};
// Shell reported anyLive=false (it couldn't identify any instance — truncated walk).
const std::vector<std::string> paths =
usageHeldPaths(records, std::unordered_set<std::string>{}, /*anyInstanceLive=*/false);
// Both paths must be protected — the protect-all net fires.
CHECK(paths.size() == 2);
bool hasA = false, hasB = false;
for (const std::string& p : paths) {
if (p == "nested-a.wav") hasA = true;
if (p == "nested-b.wav") hasB = true;
}
CHECK(hasA);
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);
const UsageFoldResult fold =
foldUsageRecords(decoded, std::unordered_set<std::string>{}, false);
CHECK(!fold.abortPrune);
CHECK(fold.heldPaths.size() == 2);
}
// --- the identity matcher ----------------------------------------------------------
// The common-case shapes: REAPER's fx_ident carries the .vst3 MODULE PATH (matched by
// the output-name needle "REASAMPLER_9000" — the display name, space-separated, can
@@ -472,12 +553,14 @@ int main() {
testPlanEmptyNonceNeverClaimsOwnership();
testPlanCrossTrackRemint();
testPlanOwnRecordAfterTrackMove();
testPlanUndecodableExisting();
testPlanUndecodableExistingRemints();
testHeldPathsLiveness();
testZeroIdentifiedProtectsAll();
testHeldPathsDedupAndEmptyPathSkip();
testTakeFxAttributedRecordIsProtected();
testUnreadableRecordAbortsPrune();
testAbortFoldReturnsProtectAllSet();
testTruncatedWalkProtectsAll();
testIdentityMatcher();
testInstanceHoldMakesPathUnprunable();