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:
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user