Take the read-back back out of the persist; a write verdict belongs only where evidence crosses the plugin boundary

Its false gated six undo points, so an unverified byte-equality assumption could have silently removed Ctrl-Z for a bank mutation that landed.
This commit is contained in:
2026-08-02 13:42:51 -04:00
parent 25390d5253
commit 1c8709e82d
12 changed files with 181 additions and 127 deletions
+18 -62
View File
@@ -9,8 +9,8 @@
// degrading to Failed rather than to Ok; the action lookup name's leading underscore and
// its channel fork; the two key classifiers each end reads the shared key through; the
// write-back verdict, driven by a modelled key store that accepts or drops the write; and
// the persist/upgrade state machine that is the ONLY route to a Banked landing, including
// the reachability of its Unpersisted limb at both persist outcomes.
// the persist/upgrade state machine that is the ONLY route to a Banked landing, at both
// persist outcomes.
#include "../src/core/wire/bake_wire.h"
@@ -33,8 +33,8 @@ static int g_fail = 0;
// here and used by both blocks below that need it, so the test suite does not become a
// third place the sentence lives.
static const std::string kUnpersistedAnswer =
"the bake reached the bank in memory, but this pass's persist did not confirm its bank "
"write, so this answer cannot promise a reload will find it";
"the bake reached the bank in memory, but this pass's persist did not report success, "
"so this answer cannot promise a reload will find it";
int main() {
// --- The exact bytes on the wire -------------------------------------------------
@@ -554,8 +554,10 @@ int main() {
// rather than a keyed verdict and an unkeyed reason the reader has to pair up.
CHECK(ok.find("(added as a distinct capture)") != std::string::npos);
// The sentence names the OBSERVATION behind the flag (see BakeLanding) and stops
// there — it may not claim the .rpp on disk already holds the entry.
CHECK(ok.find("read back as exactly what this pass wrote") != std::string::npos);
// there — the write was ISSUED into a saved project. It may not claim REAPER took
// the value, nor that the .rpp on disk already holds the entry.
CHECK(ok.find("issued its bank write into the saved project") != std::string::npos);
CHECK(ok.find("read back") == std::string::npos);
CHECK(ok.find(".rpp") == std::string::npos);
CHECK(ok.find("carries it") == std::string::npos);
@@ -585,11 +587,11 @@ int main() {
unpersisted.landing = BakeLanding::Unpersisted;
const std::string memoryOnly = describeBakeKey(key, unpersisted);
CHECK(memoryOnly.find("IN MEMORY ONLY") != std::string::npos);
CHECK(memoryOnly.find("persist did not confirm its bank write") != std::string::npos);
CHECK(memoryOnly.find("persist did not report success") != std::string::npos);
CHECK(memoryOnly != ok);
// It may NOT claim what the project's saved state holds: the persist can fail
// before writing anything or throw part-way, and a dedup hit's target may have
// been in the project since long before this pass.
// It may NOT claim what the project's saved state holds: the persist may never
// have run at all, and a dedup hit's target may have been in the project since
// long before this pass.
CHECK(memoryOnly.find("does not carry it") == std::string::npos);
BakeKeyOutcome partial = landed;
@@ -686,6 +688,10 @@ int main() {
// to be answered Banked directly, on the grounds that it changed nothing — false
// exactly when the entry it deduped against was one the SAME pass had just added and
// then failed to persist, which answers Ok for an entry the project does not carry.
//
// Both limbs are live in the shell, not just in this table: it assigns the landing
// inside a guarded scan and sets its `persisted` local only in the block after it, so a
// throw between the two reaches Unpersisted with a real landing behind it.
{
// A dedup hit and a fresh add are INDISTINGUISHABLE here, by construction: the shell
// assigns Unpersisted to both, so both need the same observation to be promoted.
@@ -741,8 +747,8 @@ int main() {
// Judging this by SetProjExtState's return made `writeFailed` unproducible on the
// landing path and its sentence dead code (extStateWriteLanded owns why). The verdict is
// the read-back, which a store that drops the write does produce. The store is modelled
// here; the three shells bind these same two calls to SetProjExtState and the
// grow-loop read.
// here; the two shells that bind the verdict make these same two calls, to
// SetProjExtState and the grow-loop read.
{
struct FakeKeyStore {
std::map<std::string, std::string> values;
@@ -837,56 +843,6 @@ int main() {
CHECK(!extStateWriteLanded("", std::optional<std::string>("leftover")));
}
// --- The Unpersisted limb is REACHABLE, through the same predicate ------------------
// `persisted` used to be structurally true wherever a landing could observe it: the
// session reported it from SetProjExtState's unread return, and by the time a landing
// exists the only two conditions that return could reflect (no active project, an
// unsaved one) are already excluded by the Land verdict. So the Unpersisted limb, the
// wire answer for it and its console clause were all dead. The session now proves the
// `banks` key the same way the landing proves its own — modelled below at both
// outcomes, so neither branch is a constant.
{
const std::string banksJson = R"({"banks":[{"id":"pool","samples":[]}]})";
// The write REJECTED: the project still holds whatever it held before, which is a
// book without the entry this pass just landed in memory.
const std::optional<std::string> stale{R"({"banks":[]})"};
const bool persistedAfterDrop = extStateWriteLanded(banksJson, stale);
CHECK(!persistedAfterDrop);
BakeKeyOutcome entry;
entry.verdict = BakeScanVerdict::Land;
entry.landing =
bakeLandingAfterPersist(BakeLanding::Unpersisted, persistedAfterDrop);
entry.proof = BakeWriteProof::Confirmed; // the ANSWER wrote fine; the persist did not
CHECK(entry.landing == BakeLanding::Unpersisted);
const std::string line = describeBakeKey("rsbake_0123abcd", entry);
CHECK(line.find("IN MEMORY ONLY") != std::string::npos);
CHECK(line.find("The answer was written back") != std::string::npos);
// ...and that landing is answered as a FAILURE on the wire, so no instance adopts
// an entry a reload would not find.
BakeOutcome answered;
answered.status = BakeStatus::Failed;
answered.message = kUnpersistedAnswer;
answered.generation = 1893456000;
const auto back = decodeBakeOutcome(encodeBakeOutcome(answered));
CHECK(back.has_value() && back->status == BakeStatus::Failed);
CHECK(back.has_value() && back->message == kUnpersistedAnswer);
// The write ACCEPTED, same predicate, same inputs but for the read-back: the limb
// above is a real branch, not a constant.
const bool persistedAfterTake =
extStateWriteLanded(banksJson, std::optional<std::string>(banksJson));
CHECK(persistedAfterTake);
CHECK(bakeLandingAfterPersist(BakeLanding::Unpersisted, persistedAfterTake) ==
BakeLanding::Banked);
// An unreadable read-back must NOT be folded in as an absence-and-therefore-a-clear:
// the session routes Overflow to false before it ever reaches this predicate, and a
// non-empty write against an absent key is false here regardless.
CHECK(!extStateWriteLanded(banksJson, std::nullopt));
}
// --- Every outcome bake_land actually emits survives the key round trip -------------
// The landing writes these and the instrument reads them back; a field the encoder and
// the decoder disagreed about would strand exactly the bake that produced it.