Q-W5: persist → session/ext_state_io/prune_fs (deletion authority concentrated); one GetProjExtState grow-loop in bridge_marshal (T2-04, ×3 rewired); bank_book JSON codec → bank_book_json via private static nameKey; persist.h stays umbrella. 61/61 green.

This commit is contained in:
2026-07-29 12:56:06 -04:00
parent bbbb69ee55
commit 75aa93f913
16 changed files with 1899 additions and 1514 deletions
+95 -1
View File
@@ -6,11 +6,17 @@
// Covers: decodeGetProjExtState hit/absent/zero-return/empty-buffer (the stale-buffer
// guard). The S1 spike's extractJsonStringField string-scan reader was retired in S4
// (the instrument now parses the bank through the shared bank_book JSON path), so its
// cases are gone with it.
// cases are gone with it. Q-W5 (rider T2-04) adds readProjExtStateGrowing — the ONE
// grow-loop retry policy shared by persist / usage_scan / reaper_bridge — covered
// against a fake read: absent, small-fit, grow-then-fit, empty-complete (composed with
// the decode guard), and the 16 MB overflow give-up. The overflow case is
// prune-safety-adjacent (usage_scan folds it to abortPrune), so it is pinned here.
#include "../src/core/instrument/map/bridge_marshal.h"
#include <cstdio>
#include <cstring>
#include <string>
using namespace reasampler;
using namespace reasampler::instrument::map;
@@ -46,12 +52,100 @@ static void testDecodeEmptyBuffer() {
CHECK(!v.has_value());
}
// --- readProjExtStateGrowing (the T2-04 shared grow-loop policy) -------------
// A fake GetProjExtState: honors the buffer contract (writes at most cap-1 chars +
// NUL — REAPER clips to the caller's capacity) and returns the stored value's length.
static int fakeRead(const std::string& stored, char* buf, int cap) {
const std::size_t n =
stored.size() < static_cast<std::size_t>(cap - 1)
? stored.size()
: static_cast<std::size_t>(cap - 1);
std::memcpy(buf, stored.data(), n);
buf[n] = '\0';
return static_cast<int>(stored.size());
}
static void testGrowingAbsent() {
// rv <= 0 on the first attempt: the key holds no value — Absent, no retry.
int calls = 0;
const auto r = readProjExtStateGrowing([&](char*, int) {
++calls;
return 0;
});
CHECK(r.status == GrowingExtStateRead::Status::Absent);
CHECK(r.apiReturn == 0);
CHECK(calls == 1);
}
static void testGrowingSmallValueFitsFirstAttempt() {
const std::string stored = "hello";
int calls = 0;
const auto r = readProjExtStateGrowing([&](char* buf, int cap) {
++calls;
return fakeRead(stored, buf, cap);
});
CHECK(r.status == GrowingExtStateRead::Status::Complete);
CHECK(r.value == stored);
CHECK(r.apiReturn == 5);
CHECK(calls == 1);
}
static void testGrowingRetriesUntilStrictFit() {
// A value whose C string exactly fills the first buffer (size+1 == cap) is
// AMBIGUOUS — it may have been clipped — so the policy must retry at the next
// capacity, where it fits strictly and returns whole.
const std::string stored(static_cast<std::size_t>((1 << 16) - 1), 'x');
int calls = 0;
const auto r = readProjExtStateGrowing([&](char* buf, int cap) {
++calls;
return fakeRead(stored, buf, cap);
});
CHECK(r.status == GrowingExtStateRead::Status::Complete);
CHECK(r.value == stored);
CHECK(calls == 2);
}
static void testGrowingEmptyCompleteComposesWithDecodeGuard() {
// rv > 0 but an empty buffer: the loop reports a Complete empty value, and the
// bridge path's decodeGetProjExtState(apiReturn, value) still rejects it — the
// stale/empty-buffer guard survives the T2-04 rewire unchanged.
const auto r = readProjExtStateGrowing([&](char* buf, int) {
buf[0] = '\0';
return 3;
});
CHECK(r.status == GrowingExtStateRead::Status::Complete);
CHECK(r.value.empty());
CHECK(!decodeGetProjExtState(r.apiReturn, r.value).has_value());
}
static void testGrowingOverflowGivesUpAtCeiling() {
// Every attempt clips (the value never fits under 16 MB): Overflow — which is
// "unreadable WHOLE", never Absent. usage_scan folds this to the prune fail-safe
// abort, so the distinction is load-bearing. Caps run 2^16..2^24 step ×4 = 5 tries.
int calls = 0;
const auto r = readProjExtStateGrowing([&](char* buf, int cap) {
++calls;
std::memset(buf, 'a', static_cast<std::size_t>(cap - 1));
buf[cap - 1] = '\0';
return cap; // reports a length that never strictly fits
});
CHECK(r.status == GrowingExtStateRead::Status::Overflow);
CHECK(calls == 5);
}
int main() {
testDecodeHit();
testDecodeAbsentKey();
testDecodeNegativeReturn();
testDecodeEmptyBuffer();
testGrowingAbsent();
testGrowingSmallValueFitsFirstAttempt();
testGrowingRetriesUntilStrictFit();
testGrowingEmptyCompleteComposesWithDecodeGuard();
testGrowingOverflowGivesUpAtCeiling();
if (g_fail == 0) std::printf("bridge_marshal: all tests passed\n");
return g_fail != 0;
}