fix: bake-answer messages stop asserting causes the classifier can't see
Rewords Cleared/Unanswered/ForeignRequest to name only observed facts and every live hypothesis instead of picking a winner; adds a guarded answeredOutcome accessor against a future unhandled BakeAnswerKind; folds an unreachable BakeScanKey/Context test state; fixes generation-stamp timing.
This commit is contained in:
@@ -259,9 +259,16 @@ int main() {
|
||||
CHECK(classifyBakeAnswer(std::optional<std::string>(encodeBakeRequest(sibling)), sent)
|
||||
.kind == BakeAnswerKind::ForeignRequest);
|
||||
|
||||
CHECK(classifyBakeAnswer(std::nullopt, sent).kind == BakeAnswerKind::Cleared);
|
||||
CHECK(classifyBakeAnswer(std::optional<std::string>(""), sent).kind ==
|
||||
BakeAnswerKind::Cleared);
|
||||
// Cleared folds two genuinely different bridge outcomes -- key absent, and key
|
||||
// present but empty -- into ONE kind, because the bridge cannot label which
|
||||
// occurred. Both must land on the identical kind: a caller's message may claim
|
||||
// no more than what the fold actually preserves.
|
||||
const BakeAnswer clearedFromAbsent = classifyBakeAnswer(std::nullopt, sent);
|
||||
const BakeAnswer clearedFromEmpty =
|
||||
classifyBakeAnswer(std::optional<std::string>(""), sent);
|
||||
CHECK(clearedFromAbsent.kind == BakeAnswerKind::Cleared);
|
||||
CHECK(clearedFromEmpty.kind == BakeAnswerKind::Cleared);
|
||||
CHECK(clearedFromAbsent.kind == clearedFromEmpty.kind);
|
||||
CHECK(classifyBakeAnswer(std::optional<std::string>("rsbakeout9 whatever"), sent)
|
||||
.kind == BakeAnswerKind::Undecodable);
|
||||
|
||||
@@ -273,12 +280,58 @@ int main() {
|
||||
.outcome.has_value());
|
||||
}
|
||||
|
||||
// --- answeredOutcome: the guarded read, not switch exhaustiveness alone -------------
|
||||
{
|
||||
BakeRequest sent;
|
||||
sent.instanceGuid = "abcd";
|
||||
sent.generation = 42;
|
||||
|
||||
BakeOutcome landed;
|
||||
landed.status = BakeStatus::Ok;
|
||||
landed.sampleId = "bake-1";
|
||||
landed.generation = sent.generation;
|
||||
|
||||
// The one state that may yield a non-null pointer, and it points at the SAME
|
||||
// outcome classifyBakeAnswer decoded.
|
||||
const BakeAnswer answered =
|
||||
classifyBakeAnswer(std::optional<std::string>(encodeBakeOutcome(landed)), sent);
|
||||
const BakeOutcome* ptr = answeredOutcome(answered);
|
||||
CHECK(ptr != nullptr);
|
||||
CHECK(ptr->sampleId == "bake-1");
|
||||
|
||||
// Every other real classification nulls out.
|
||||
CHECK(answeredOutcome(classifyBakeAnswer(std::nullopt, sent)) == nullptr);
|
||||
CHECK(answeredOutcome(classifyBakeAnswer(
|
||||
std::optional<std::string>(encodeBakeRequest(sent)), sent)) == nullptr);
|
||||
CHECK(answeredOutcome(classifyBakeAnswer(std::optional<std::string>("garbage"), sent))
|
||||
== nullptr);
|
||||
BakeOutcome foreign = landed;
|
||||
foreign.generation = sent.generation + 1;
|
||||
CHECK(answeredOutcome(classifyBakeAnswer(
|
||||
std::optional<std::string>(encodeBakeOutcome(foreign)), sent)) == nullptr);
|
||||
|
||||
// The defensive arm: a hand-built Answered with no outcome set (what a future
|
||||
// BakeAnswerKind enumerator falling through an un-updated switch would look like
|
||||
// to this accessor) fails closed rather than being dereferenced.
|
||||
BakeAnswer malformed;
|
||||
malformed.kind = BakeAnswerKind::Answered;
|
||||
CHECK(!malformed.outcome.has_value());
|
||||
CHECK(answeredOutcome(malformed) == nullptr);
|
||||
}
|
||||
|
||||
// --- kMaxRequestAgeSeconds: a regression pin on the bound itself --------------------
|
||||
// The bound's meaning depends on the instrument stamping `generation` immediately
|
||||
// before publishing (AFTER staging the WAV) -- a shell-side ordering fix this pure
|
||||
// suite cannot observe directly. Pinning the literal at least catches a silent
|
||||
// widen/narrow of the budget itself.
|
||||
CHECK(kMaxRequestAgeSeconds == 30);
|
||||
|
||||
// --- The extension's per-key verdict over the open tabs -----------------------------
|
||||
{
|
||||
const std::int64_t now = 1893456000;
|
||||
// The session has polled a project and that project is REAPER's active tab — the
|
||||
// steady state a project opened from disk reaches on the next timer tick.
|
||||
const BakeScanContext loadedAndActive{true, true};
|
||||
const BakeScanContext loadedAndActive{true};
|
||||
|
||||
BakeScanKey own{true, now, true};
|
||||
CHECK(classifyBakeScan(loadedAndActive, own, now) == BakeScanVerdict::Land);
|
||||
@@ -292,11 +345,13 @@ int main() {
|
||||
|
||||
// The loaded tab is no longer the active one, so a persist would write elsewhere:
|
||||
// even the loaded tab's own request is refused rather than half-landed.
|
||||
CHECK(classifyBakeScan(BakeScanContext{true, false}, own, now) ==
|
||||
CHECK(classifyBakeScan(BakeScanContext{false}, own, now) ==
|
||||
BakeScanVerdict::RefuseWrongProject);
|
||||
// The window between opening a project from disk and the first poll: no book is
|
||||
// loaded yet, so nothing may land.
|
||||
CHECK(classifyBakeScan(BakeScanContext{false, false}, own, now) ==
|
||||
// The window before any project is loaded: matchesLoadedProject cannot be true for
|
||||
// ANY key here — a key can only match a project that is loaded — so this is the
|
||||
// shell-reachable stand-in for "no book yet", not `own` (which the shell could
|
||||
// never actually pair with an unloaded session).
|
||||
CHECK(classifyBakeScan(BakeScanContext{false}, BakeScanKey{true, now, false}, now) ==
|
||||
BakeScanVerdict::RefuseWrongProject);
|
||||
|
||||
// Stale in EITHER direction (a clock that moved backwards counts too).
|
||||
@@ -318,7 +373,7 @@ int main() {
|
||||
BakeScanKey notARequest{false, 0, true};
|
||||
CHECK(classifyBakeScan(loadedAndActive, notARequest, now) ==
|
||||
BakeScanVerdict::Ignore);
|
||||
CHECK(classifyBakeScan(BakeScanContext{false, false}, notARequest, now) ==
|
||||
CHECK(classifyBakeScan(BakeScanContext{false}, notARequest, now) ==
|
||||
BakeScanVerdict::Ignore);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user