test(run): the store rules, stacks and memory, and the review fixes, check by check

StoreRules (23 checks), StacksMemory (24) and ReviewFixes (19), each a
test of its own.

Run and SlotsPlan now have seams that start as the real ones: appReady,
probe, query, bypassesRls, migrationsAtHead, migrationsDrift, treeDrift,
promptFor, runPass, resumable, instrumentPreflight, and a Machine for
SlotsPlan. MigrationsAtHead is split out of MigrationsDrift for it; the
HEAD side is still worked out once per tree and base.

The Postgres stand-in and the raw broker call move to tests/Common, shared
by PgWire and ReviewFixes. ReviewFixes runs this binary as `sql`, and a
real server that resets every connection makes the startup message either
fail to go out or never get its answer - a DbError either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 09:39:53 +01:00
commit dd0929b705
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
8 changed files with 1328 additions and 185 deletions

View file

@ -215,7 +215,8 @@ namespace Crafter::Review::Run {
}
}
std::pair<std::int64_t, std::vector<std::string>> SlotsPlan(const Json& cfg, const fs::path& repo, const std::optional<fs::path>& artifact, const std::optional<fs::path>& scratchRoot, const Log& given) {
std::pair<std::int64_t, std::vector<std::string>> SlotsPlan(const Json& cfg, const fs::path& repo, const std::optional<fs::path>& artifact, const std::optional<fs::path>& scratchRoot, const Log& given,
const Machine& machine) {
const Log log = given ? given : Log([](const std::string& line) { std::println("{}", line); });
const Json& sl = cfg.at("slots");
if (!Dyn::Truthy(Dyn::Get(sl, "dir"))) {
@ -244,8 +245,8 @@ namespace Crafter::Review::Run {
if (!best || Number(cost.megabytes) > Number(best->megabytes)) best = std::move(cost);
}
const double passMb = Number(best->megabytes);
const std::optional<std::int64_t> avail = Memory::Available();
const std::int64_t reserve = Memory::Reserve();
const std::optional<std::int64_t> avail = machine.available ? machine.available() : Memory::Available();
const std::int64_t reserve = machine.reserve ? machine.reserve() : Memory::Reserve();
const auto upCount = static_cast<std::int64_t>(up.size());
if (!avail) {
log("slots: no /proc/meminfo - cannot size the slots by memory");
@ -308,6 +309,19 @@ namespace Crafter::Review::Run {
// ---- the Run ---------------------------------------------------------------------------
Run::Run() : repo(fs::current_path()), t0(Platform::Now()) {
log = [this](const std::string& message) { Message(message); };
appReady = [](std::string_view url, std::string_view path) { return AppReady(url, path); };
probe = [](std::string_view url) { return PgWire::Probe(url); };
query = [](std::string_view url, std::string_view sql) { return PgWire::Query(url, sql); };
bypassesRls = [](std::string_view url) { return PgWire::BypassesRls(url); };
migrationsAtHead = [this] { return MigrationsAtHead(); };
migrationsDrift = [this](const std::string& url) { return MigrationsDrift(url); };
treeDrift = [this] { return TreeDrift(); };
promptFor = [this](const Json& job) { return PromptFor(job); };
runPass = [this](const Json& job, const std::string& prompt, const fs::path& systemPath, std::string_view schema, std::int64_t maxTurns, std::int64_t timeoutMin, bool persist) {
return runner->Run(job, prompt, systemPath, schema, trees, maxTurns, timeoutMin, persist);
};
resumable = [this](const std::string& name, const std::string& lensFile, std::string_view title) { return Resumable(name, lensFile, title); };
instrumentPreflight = [this](std::span<const std::string> lensFiles) { InstrumentPreflight(lensFiles); };
}
Run::Run(Arguments given, Json config) : Run() {
@ -368,7 +382,7 @@ namespace Crafter::Review::Run {
for (const std::string& t : trees) {
if (!Artifact::IsToplevel(t)) Die(std::format("--trees: {} is not the toplevel of a git checkout", t));
}
if (const std::optional<std::string> drift = TreeDrift()) Die(std::format("{} - re-freeze, or check out the frozen worktree (freeze --worktree)", *drift));
if (const std::optional<std::string> drift = treeDrift()) Die(std::format("{} - re-freeze, or check out the frozen worktree (freeze --worktree)", *drift));
gateDir = Dyn::JoinPath(a.artifact, "gate-state");
fs::create_directories(gateDir);
Inventory::Built built;
@ -738,41 +752,45 @@ namespace Crafter::Review::Run {
return Dyn::Split(base, "_", 1).front();
}
MigrationsHead Run::MigrationsAtHead() {
constexpr std::string_view M = "supabase/migrations/";
auto git = [&](std::vector<std::string> args) {
std::vector<std::string> argv = {"git", "-C", trees.front()};
std::ranges::move(args, std::back_inserter(argv));
return Process::Run(argv).out;
};
std::map<std::string, std::string> atHead;
for (const std::string& f : Dyn::Split(git({"ls-tree", "--name-only", "HEAD", std::string(M)}))) {
if (const std::optional<std::string> v = MigrationVersion(f)) atHead[*v] = f;
}
std::vector<std::string> inRange;
if (!a.base.empty() && !atHead.empty()) {
for (const std::string& f : Dyn::Split(git({"diff", "--name-only", "--diff-filter=AM", std::format("{}..HEAD", a.base), "--", std::string(M)}))) {
if (MigrationVersion(f)) inRange.push_back(f);
}
}
std::map<std::string, std::string> headFp;
for (const std::string& f : inRange) headFp[f] = PgWire::Fingerprint(git({"show", std::format("HEAD:{}", f)}));
return {atHead, inRange, headFp};
}
std::pair<std::optional<std::string>, std::optional<std::string>> Run::MigrationsDrift(const std::string& url) {
// The HEAD side is the same for every stack: worked out once per tree
// and base.
// and base (a tree's HEAD does not move during a run - TreeDrift
// aborts it first).
constexpr std::string_view M = "supabase/migrations/";
const std::pair<std::string, std::string> key = {trees.front(), a.base};
if (!migCache.contains(key)) {
auto git = [&](std::vector<std::string> args) {
std::vector<std::string> argv = {"git", "-C", trees.front()};
std::ranges::move(args, std::back_inserter(argv));
return Process::Run(argv).out;
};
std::map<std::string, std::string> atHead;
for (const std::string& f : Dyn::Split(git({"ls-tree", "--name-only", "HEAD", std::string(M)}))) {
if (const std::optional<std::string> v = MigrationVersion(f)) atHead[*v] = f;
}
std::vector<std::string> inRange;
if (!a.base.empty() && !atHead.empty()) {
for (const std::string& f : Dyn::Split(git({"diff", "--name-only", "--diff-filter=AM", std::format("{}..HEAD", a.base), "--", std::string(M)}))) {
if (MigrationVersion(f)) inRange.push_back(f);
}
}
std::map<std::string, std::string> headFp;
for (const std::string& f : inRange) headFp[f] = PgWire::Fingerprint(git({"show", std::format("HEAD:{}", f)}));
migCache[key] = {atHead, inRange, headFp};
}
if (!migCache.contains(key)) migCache[key] = migrationsAtHead();
const auto& [atHead, inRange, headFp] = migCache.at(key);
if (atHead.empty()) return {std::nullopt, std::format("no {} at the range HEAD - applied migrations not compared", M)};
std::vector<std::vector<std::optional<std::string>>> rows;
try {
try {
rows = PgWire::Query(url, "select version, array_to_string(statements, ' ') from supabase_migrations.schema_migrations").second;
rows = query(url, "select version, array_to_string(statements, ' ') from supabase_migrations.schema_migrations").second;
} catch (const PgWire::SqlError& e) {
// No statements column (an old CLI): versions alone.
if (!e.fields.contains('C') || e.fields.at('C') != "42703") throw;
rows = PgWire::Query(url, "select version, null from supabase_migrations.schema_migrations").second;
rows = query(url, "select version, null from supabase_migrations.schema_migrations").second;
}
} catch (const PgWire::SqlError& e) {
const std::string code = e.fields.contains('C') ? e.fields.at('C') : std::string();
@ -912,7 +930,7 @@ namespace Crafter::Review::Run {
if (!app.empty()) apps = stacks->All();
for (const Json& s : apps) {
const Json& readyPath = Dyn::Get(fcfg, "readyPath");
const auto [ok, detail] = AppReady(Dyn::Str(s.at("appUrl")), Dyn::Truthy(readyPath) ? Dyn::Str(readyPath) : std::string("/"));
const auto [ok, detail] = appReady(Dyn::Str(s.at("appUrl")), Dyn::Truthy(readyPath) ? Dyn::Str(readyPath) : std::string("/"));
if (!ok) {
const Json& how = Dyn::Truthy(Dyn::Get(s, "slot")) ? Dyn::Get(sl, "start") : Dyn::Get(fcfg, "start");
Die(std::format("stacks ({}): {}: the application does not answer ({}) - start it from the frozen tree first{}", who, Dyn::Str(s.at("id")), detail,
@ -924,18 +942,18 @@ namespace Crafter::Review::Run {
std::optional<std::string> note;
for (const Json& s : dbs) {
const std::string url = Dyn::Str(s.at("dbUrl"));
const auto [ok, detail] = PgWire::Probe(url);
const auto [ok, detail] = probe(url);
if (!ok) {
const Json& supabaseStart = Dyn::Get(cfg.at("supabase"), "start");
const Json& how = Dyn::Truthy(Dyn::Get(s, "slot")) ? Dyn::Get(sl, "start") : (Dyn::Truthy(supabaseStart) ? supabaseStart : Dyn::Get(fcfg, "start"));
Die(std::format("stacks ({}): {}: the database does not answer ({}) - start the stack from the frozen tree first{}", who, Dyn::Str(s.at("id")), detail,
Dyn::Truthy(how) ? std::format(" ({})", Dyn::Str(how)) : std::string()));
}
const auto [drift, n] = MigrationsDrift(url);
const auto [drift, n] = migrationsDrift(url);
if (drift) Die(std::format("stacks ({}): {}: the database: {}", who, Dyn::Str(s.at("id")), *drift));
if (!firstDetail || firstDetail->empty()) firstDetail = detail;
if (!note || note->empty()) note = n;
bypass.insert(PgWire::BypassesRls(url));
bypass.insert(bypassesRls(url));
}
if (!dbs.empty()) {
dbChecked = dbs.size() == 1 ? std::format("it answers ({}); {}", firstDetail.value_or("None"), note.value_or("None"))
@ -978,7 +996,7 @@ namespace Crafter::Review::Run {
}
if (email.empty() || PgWire::CheckUrl(Dyn::Get(stack, "dbUrl"))) return std::nullopt;
try {
const auto [columns, rows] = PgWire::Query(Dyn::Str(stack.at("dbUrl")), std::format("select 1 from auth.users where lower(email) = '{}'", Dyn::Replace(email, "'", "''")));
const auto [columns, rows] = query(Dyn::Str(stack.at("dbUrl")), std::format("select 1 from auth.users where lower(email) = '{}'", Dyn::Replace(email, "'", "''")));
return rows.empty() ? std::optional<std::string>(email) : std::nullopt;
} catch (const PgWire::SqlError&) {
return std::nullopt;
@ -1283,14 +1301,14 @@ namespace Crafter::Review::Run {
abortPool(std::format("artifact changed during the run ({} != {})", now.substr(0, 8), h8));
return {{{"label", job.at("label")}, {"status", "error"}, {"reason", "artifact changed during the run - aborted"}}, nullptr};
}
if (const std::optional<std::string> drift = TreeDrift()) {
if (const std::optional<std::string> drift = treeDrift()) {
abortPool(std::format("evidence drift during the run ({})", *drift));
return {{{"label", job.at("label")}, {"status", "error"}, {"reason", std::format("{} - aborted", *drift)}}, nullptr};
}
const auto [systemPath, prompt] = PromptFor(job);
const auto [systemPath, prompt] = promptFor(job);
const bool whole = job.at("kind") == "whole";
auto [rec, raw] = runner->Run(job, prompt, systemPath, schemas.at(whole ? "whole" : "block"), trees, Dyn::Has(job, "maxTurns") ? Int(job.at("maxTurns")) : 20,
Dyn::Has(job, "timeoutMin") ? Int(job.at("timeoutMin")) : 20, whole);
auto [rec, raw] = runPass(job, prompt, systemPath, schemas.at(whole ? "whole" : "block"), Dyn::Has(job, "maxTurns") ? Int(job.at("maxTurns")) : 20,
Dyn::Has(job, "timeoutMin") ? Int(job.at("timeoutMin")) : 20, whole);
if (Dyn::Truthy(Dyn::Get(job, "stack"))) rec["stack"] = job.at("stack").at("id");
if (Dyn::Truthy(Dyn::Get(rec, "memPeakMb"))) {
const Json& configured = Dyn::Get(environments, Dyn::Str(job.at("lensFile")));
@ -1346,7 +1364,7 @@ namespace Crafter::Review::Run {
std::vector<Json> resumed;
std::vector<std::string> live;
for (const std::string& lf : lensFiles) {
if (std::optional<Json> r = Resumable(Plan::LensName(lf), lf, title)) {
if (std::optional<Json> r = resumable(Plan::LensName(lf), lf, title)) {
resumed.push_back(std::move(*r));
} else {
live.push_back(lf);
@ -1356,7 +1374,7 @@ namespace Crafter::Review::Run {
// Nothing runs: no stack, database or application is needed for a
// result already bought.
if (lensFiles.empty()) return resumed;
InstrumentPreflight(lensFiles);
instrumentPreflight(lensFiles);
std::map<std::string, std::vector<Json>> parts;
std::map<std::string, std::set<std::string>> missing;
std::map<std::string, std::set<std::string>> planned;