343 lines
24 KiB
C++
343 lines
24 KiB
C++
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||
|
|
|
||
|
|
// Store rules: what unit-state stores as reusable, reads per round, the
|
||
|
|
// reads a unit needs, salvage by environment, the environment on the store
|
||
|
|
// side, resume by environment, the migrations preflight and the instrument
|
||
|
|
// preflight. A Run is built part by part.
|
||
|
|
|
||
|
|
#include <stdlib.h>
|
||
|
|
import std;
|
||
|
|
import Crafter.Review;
|
||
|
|
namespace fs = std::filesystem;
|
||
|
|
using namespace Crafter::Review;
|
||
|
|
|
||
|
|
namespace {
|
||
|
|
std::int32_t Failures = 0;
|
||
|
|
const std::string Fn(Plan::FunctionalLens);
|
||
|
|
|
||
|
|
void Check(std::string_view name, bool cond, std::string_view detail = {}) {
|
||
|
|
std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string());
|
||
|
|
if (!cond) ++Failures;
|
||
|
|
}
|
||
|
|
|
||
|
|
void Write(const fs::path& path, std::string_view text) {
|
||
|
|
fs::create_directories(path.parent_path());
|
||
|
|
std::ofstream(path, std::ios::binary) << text;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::set<std::string> Keys(const Json& object) {
|
||
|
|
std::set<std::string> keys;
|
||
|
|
for (auto it = object.begin(); it != object.end(); ++it) keys.insert(it.key());
|
||
|
|
return keys;
|
||
|
|
}
|
||
|
|
|
||
|
|
Json Defaults() {
|
||
|
|
Json out = Json::object();
|
||
|
|
for (const Plan::Lens& l : Plan::Lenses) out[std::string(l.file)] = std::string(l.env);
|
||
|
|
return out;
|
||
|
|
}
|
||
|
|
|
||
|
|
std::string Git(const fs::path& repo, std::vector<std::string> args) {
|
||
|
|
std::vector<std::string> argv = {"git", "-C", repo.string(), "-c", "user.email=t@t", "-c", "user.name=t"};
|
||
|
|
std::ranges::move(args, std::back_inserter(argv));
|
||
|
|
const Process::Captured run = Process::Run(argv);
|
||
|
|
if (run.returnCode != 0) throw std::runtime_error(std::format("{}: {}", Dyn::Join(argv, " "), run.err));
|
||
|
|
return Dyn::Strip(run.out);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int main() {
|
||
|
|
std::println("unit-state: what is stored as reusable; reads per round; reads needed per lens");
|
||
|
|
Platform::SetMode(Platform::Mode::Native);
|
||
|
|
// No git of this test reads a global config.
|
||
|
|
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
||
|
|
setenv("GIT_CONFIG_SYSTEM", "/dev/null", 1);
|
||
|
|
const fs::path td = fs::temp_directory_path() / std::format("crafter-review-store-rules-{}", Platform::PublishedPid());
|
||
|
|
fs::remove_all(td);
|
||
|
|
fs::create_directories(td);
|
||
|
|
auto noLog = [](const std::string&) {};
|
||
|
|
|
||
|
|
auto stored = [&](std::vector<Json> recs, const Json& parts, const Json& coverage, const Json& findings, const Json& before = Json::object(), std::int64_t rnd = 1, bool twice = false) {
|
||
|
|
Run::Run r;
|
||
|
|
r.records = std::move(recs);
|
||
|
|
r.hash = "H";
|
||
|
|
r.h8 = "H";
|
||
|
|
r.round = rnd;
|
||
|
|
r.dumpPath = td / std::format("run-{}.json", rnd);
|
||
|
|
r.repo = td;
|
||
|
|
r.results = {{{"lens", "L"}, {"coverage", coverage}, {"findings", findings}, {"notes", Json::array()}, {"parts", parts}}};
|
||
|
|
r.state = {{"version", 1}, {"entries", before}, {"personaPass", nullptr}};
|
||
|
|
r.statePath = td / "unit-state.json";
|
||
|
|
r.a.date = "2026-10-15";
|
||
|
|
r.log = noLog;
|
||
|
|
// The file is the record the store merges into (2026-10-31).
|
||
|
|
State::SaveWhole(r.statePath, r.state);
|
||
|
|
r.StoreState();
|
||
|
|
if (twice) r.StoreState();
|
||
|
|
return r.state.at("entries");
|
||
|
|
};
|
||
|
|
auto rec = [](const std::string& u, const std::string& k) {
|
||
|
|
return Json {{"label", std::format("L:block:{}", u)}, {"lens", "L"}, {"lensFile", "repo-bug-prover.md"}, {"unit", u}, {"kind", "block"}, {"status", "ok"}, {"reuseKey", k}};
|
||
|
|
};
|
||
|
|
auto part = [](const std::string& u, const std::string& v) {
|
||
|
|
return Json {{"label", std::format("L:block:{}", u)}, {"kind", "block"}, {"unit", u}, {"verdict", v}, {"findings", v != "SEND-READY" ? 1 : 0}};
|
||
|
|
};
|
||
|
|
auto cov = [](const std::string& u, const std::string& v) { return Json {{"item", u}, {"verdict", v}, {"basis", ""}}; };
|
||
|
|
auto finding = [](const std::string& item, bool discharged, const std::optional<std::string>& by = std::nullopt) {
|
||
|
|
Json f = {{"finding", "f"}, {"items", Json::array({item})}, {"discharged", discharged}};
|
||
|
|
if (by) f["dischargedBy"] = *by;
|
||
|
|
return f;
|
||
|
|
};
|
||
|
|
|
||
|
|
Json e = stored({rec("X", "kx"), rec("Y", "ky")}, Json::array({part("X", "SEND-READY"), part("Y", "SEND-READY")}), Json::array({cov("X", "clear"), cov("Y", "clear")}), Json::array());
|
||
|
|
Check("two clean passes -> both stored", Keys(e) == std::set<std::string>{"kx", "ky"} && e.at("kx").at("unit") == "X");
|
||
|
|
e = stored({rec("X", "kx"), rec("Y", "ky")}, Json::array({part("X", "FIX FIRST"), part("Y", "SEND-READY")}), Json::array({cov("X", "clear"), cov("Y", "clear")}), Json::array({finding("Y", false)}),
|
||
|
|
{{"kx", {{"unit", "X"}}}});
|
||
|
|
Check("the pass on X returns a finding that cites only Y -> neither stored, X loses its earlier entry", e.empty(), e.dump());
|
||
|
|
e = stored({rec("X", "kx"), rec("Y", "ky")}, Json::array({part("X", "SEND-READY"), part("Y", "SEND-READY")}), Json::array({cov("X", "defect"), cov("Y", "clear")}), Json::array({finding("X", true, "w")}));
|
||
|
|
Check("a defect verdict whose only citing finding is discharged -> stored", Keys(e) == std::set<std::string>{"kx", "ky"} && e.at("kx").at("coverage").at("verdict") == "defect");
|
||
|
|
e = stored({rec("X", "kx"), rec("Y", "ky")}, Json::array({part("X", "SEND-READY"), part("Y", "SEND-READY")}), Json::array({cov("X", "defect"), cov("Y", "clear")}), Json::array());
|
||
|
|
Check("a defect verdict with no citing finding -> not stored", Keys(e) == std::set<std::string>{"ky"});
|
||
|
|
e = stored({rec("p3:absent", "k"), rec("p4:absent", "k")}, Json::array({part("p3:absent", "SEND-READY"), part("p4:absent", "FIX FIRST")}), Json::array({cov("p3:absent", "clear"), cov("p4:absent", "defect")}),
|
||
|
|
Json::array({finding("p4:absent", false)}), {{"k", {{"unit", "p3:absent"}}}, {"other", {{"unit", "Z"}}}});
|
||
|
|
Check("two units under one key, one defective -> the key is not stored and its earlier entry is dropped", Dyn::Equal(e, {{"other", {{"unit", "Z"}}}}), e.dump());
|
||
|
|
e = stored({rec("p3:x#hdr", "k"), rec("p8:x#hdr", "k")}, Json::array({part("p3:x#hdr", "SEND-READY"), part("p8:x#hdr", "SEND-READY")}), Json::array({cov("p3:x#hdr", "clear"), cov("p8:x#hdr", "n/a")}), Json::array());
|
||
|
|
Check("two units under one key, both clean -> stored once", Keys(e) == std::set<std::string>{"k"});
|
||
|
|
const Json oneRead = Json::array({{{"hash", "H"}, {"round", 1}, {"run", "run-1.json"}}});
|
||
|
|
const Json e1 = stored({rec("X", "kx")}, Json::array({part("X", "SEND-READY")}), Json::array({cov("X", "clear")}), Json::array(), Json::object(), 1, true);
|
||
|
|
Check("reads: a clean pass is one read however often the run stores it",
|
||
|
|
Plan::ReadsOf(e1.at("kx")) == 1 && Dyn::Equal(e1.at("kx").at("reads"), oneRead) && Plan::ReadIn(e1.at("kx"), "H", 1) && !Plan::ReadIn(e1.at("kx"), "H", 2));
|
||
|
|
const Json e2 = stored({rec("X", "kx")}, Json::array({part("X", "SEND-READY")}), Json::array({cov("X", "clear")}), Json::array(), e1, 2);
|
||
|
|
Check("reads: the next round on the same hash is the second read; a relaunch of the same round adds nothing",
|
||
|
|
Plan::ReadsOf(e2.at("kx")) == 2 && Plan::ReadsOf(stored({rec("X", "kx")}, Json::array({part("X", "SEND-READY")}), Json::array({cov("X", "clear")}), Json::array(), e1).at("kx")) == 1);
|
||
|
|
const Json e3 = stored({rec("X", "kx")}, Json::array({part("X", "FIX FIRST")}), Json::array({cov("X", "defect")}), Json::array({finding("X", false)}), e2, 3);
|
||
|
|
Check("reads: a finding on the third read drops the count to nothing", !e3.contains("kx"));
|
||
|
|
auto need = [](const std::string& lf, const std::string& kind = "block", const Json& unitKind = "code", std::int64_t x = 3) {
|
||
|
|
Run::Run r;
|
||
|
|
r.x = x;
|
||
|
|
return r.ReadsNeeded({{"lensFile", lf}, {"kind", kind}, {"unitKind", unitKind}});
|
||
|
|
};
|
||
|
|
Check("reads needed: 3 for a static code unit, 1 for a functional unit, a whole pass, and everything at --reads 1",
|
||
|
|
need("repo-bug-prover.md") == 3 && need("repo-better-solution.md") == 3 && need(Fn) == 1 && need("repo-better-solution.md", "whole", nullptr) == 1 && need("repo-bug-prover.md", "block", "code", 1) == 1 && Plan::Reads == 3);
|
||
|
|
|
||
|
|
// Salvage: an earlier run's output is evidence only if its environment
|
||
|
|
// was there (exercised), whatever classify says.
|
||
|
|
{
|
||
|
|
Run::Run r;
|
||
|
|
const fs::path sd = td / "old-run";
|
||
|
|
fs::create_directories(sd / "out");
|
||
|
|
r.salvageDirs = {sd};
|
||
|
|
r.testMode = false;
|
||
|
|
r.x = 1;
|
||
|
|
r.repo = td;
|
||
|
|
r.environments = {{Fn, "browser"}, {"repo-bug-prover.md", "none"}};
|
||
|
|
r.log = noLog;
|
||
|
|
r.a.dryRun = false;
|
||
|
|
r.a.noSalvage = false;
|
||
|
|
const std::string res = R"({"type":"result","subtype":"success","is_error":false,"structured_output":{"verdict":"SEND-READY","findings":[],"notes":[],"coverage":[]},"usage":{"input_tokens":1,"output_tokens":1}})";
|
||
|
|
const std::string initNo = R"({"type":"system","subtype":"init","model":"m","mcp_servers":[],"tools":["Read"]})";
|
||
|
|
const std::string initOk = R"({"type":"system","subtype":"init","model":"m","mcp_servers":[{"name":"playwright","status":"connected"}],"tools":["mcp__playwright__browser_snapshot"]})";
|
||
|
|
const std::string call = R"({"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t1","name":"mcp__playwright__browser_snapshot","input":{}}]}})"
|
||
|
|
"\n"
|
||
|
|
R"({"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"t1","content":"- page"}]}})";
|
||
|
|
const Json job = {{"label", "repo-functional:whole"}, {"lensFile", Fn}, {"kind", "whole"}, {"model", "m"}};
|
||
|
|
Write(sd / "out" / "repo-functional:whole.jsonl", std::format("{}\n{}\n", initNo, res));
|
||
|
|
Check("salvage: a functional whole pass whose server never connected is not salvaged (no browser = no evidence, salvaged or not)", !r.Salvage(job));
|
||
|
|
Write(sd / "out" / "repo-functional:whole.jsonl", std::format("{}\n{}\n{}\n", initOk, call, res));
|
||
|
|
std::optional<std::pair<Json, Json>> got = r.Salvage(job);
|
||
|
|
Check("...with the server connected and a browser call it is, with its environment on the record",
|
||
|
|
got && got->first.at("environment") == "browser" && Dyn::Truthy(got->first.at("salvaged")) && got->second.at("verdict") == "SEND-READY");
|
||
|
|
Write(sd / "out" / "repo-bug-prover:block:X.jsonl", std::format("{}\n{}\n", initNo, res));
|
||
|
|
const Json bj = {{"label", "repo-bug-prover:block:X"}, {"lensFile", "repo-bug-prover.md"}, {"kind", "block"}, {"model", "m"}};
|
||
|
|
got = r.Salvage(bj);
|
||
|
|
Check("a static pass (environment none) is salvaged as classified", got && got->first.at("environment") == "none");
|
||
|
|
// Across environments: the scratch dir's marker says what its passes
|
||
|
|
// had; no marker = the defaults.
|
||
|
|
r.environments = {{Fn, "browser"}, {"repo-bug-prover.md", "supabase"}};
|
||
|
|
const bool unmarked = r.Salvage(bj).has_value();
|
||
|
|
Write(sd / "environments", R"({"repo-bug-prover": "supabase"})");
|
||
|
|
r.envMarkers.clear();
|
||
|
|
const std::optional<std::pair<Json, Json>> marked = r.Salvage(bj);
|
||
|
|
r.environments = {{Fn, "browser"}, {"repo-bug-prover.md", "none"}};
|
||
|
|
const bool back = r.Salvage(bj).has_value();
|
||
|
|
Check("salvage: an output is reused only in the environment its scratch dir's marker names (a dir without one ran the defaults)",
|
||
|
|
!unmarked && marked && marked->first.at("environment") == "supabase" && !back);
|
||
|
|
}
|
||
|
|
|
||
|
|
// The environment on the STORE side.
|
||
|
|
const fs::path fx = fs::current_path() / "tests" / "Goldens" / "fixture" / "commits";
|
||
|
|
const Inventory::Built built = Inventory::Build("commits", fx.string());
|
||
|
|
Run::Run g;
|
||
|
|
g.a.artifact = fx.string();
|
||
|
|
g.a.dryRun = false;
|
||
|
|
g.a.date = "2026-10-22";
|
||
|
|
g.units = built.out.at("units");
|
||
|
|
g.lines = built.lines;
|
||
|
|
for (const Plan::Lens& l : Plan::Lenses) g.charterSha[std::string(l.file)] = "c";
|
||
|
|
g.state = State::EmptyState();
|
||
|
|
g.x = 1;
|
||
|
|
g.h8 = "H";
|
||
|
|
g.round = 1;
|
||
|
|
g.hash = "H";
|
||
|
|
g.repo = td;
|
||
|
|
g.statePath = td / "us-env.json";
|
||
|
|
g.dumpPath = td / "run-env.json";
|
||
|
|
g.log = noLog;
|
||
|
|
const std::string U = "p2:hello.py#1";
|
||
|
|
auto unitJob = [&] {
|
||
|
|
for (Json j : Plan::PlanLens("repo-bug-prover.md", g.units, g.lines).blocks) {
|
||
|
|
if (j.at("unit") == U) {
|
||
|
|
j["gate"] = "G";
|
||
|
|
return j;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
throw std::runtime_error("no unit job");
|
||
|
|
};
|
||
|
|
// One round of the bug-prover on U in env: PrepareBlocks (reuse or a
|
||
|
|
// job), then - if it ran - a pass with that verdict, stored.
|
||
|
|
auto round = [&](const std::string& env, const std::string& verdict) -> std::pair<std::string, Json> {
|
||
|
|
g.environments = Defaults();
|
||
|
|
g.environments["repo-bug-prover.md"] = env;
|
||
|
|
g.records.clear();
|
||
|
|
g.results.clear();
|
||
|
|
g.wholeParts.clear();
|
||
|
|
std::map<std::string, std::vector<Json>> parts = {{"repo-bug-prover", {}}};
|
||
|
|
const std::vector<Json> jobs = g.PrepareBlocks("repo-bug-prover", {unitJob()}, parts);
|
||
|
|
if (jobs.empty()) return {"reused", nullptr};
|
||
|
|
const Json& j = jobs.front();
|
||
|
|
g.records = {{{"label", j.at("label")}, {"lens", "repo-bug-prover"}, {"lensFile", "repo-bug-prover.md"}, {"unit", U}, {"kind", "block"}, {"status", "ok"}, {"reuseKey", j.at("reuseKey")}, {"otherKeys", j.at("otherKeys")}}};
|
||
|
|
const bool bad = verdict != "SEND-READY";
|
||
|
|
g.results = {{{"lens", "repo-bug-prover"}, {"coverage", Json::array({{{"item", U}, {"verdict", bad ? "defect" : "clear"}, {"basis", ""}}})}, {"notes", Json::array()},
|
||
|
|
{"findings", bad ? Json::array({finding(U, false)}) : Json::array()}, {"parts", Json::array({{{"label", j.at("label")}, {"verdict", verdict}}})}}};
|
||
|
|
g.StoreState();
|
||
|
|
return {"ran", j};
|
||
|
|
};
|
||
|
|
auto inEntries = [&](const Json& key) { return g.state.at("entries").contains(key.get<std::string>()); };
|
||
|
|
const auto [s1, j1] = round("none", "SEND-READY");
|
||
|
|
const auto [s2, j2] = round("supabase", "SEND-READY");
|
||
|
|
Check("store: a clean verdict bought in one environment is not reused in another (none -> supabase runs the unit again); each is stored under its own key",
|
||
|
|
s1 == "ran" && s2 == "ran" && j1.at("reuseKey") != j2.at("reuseKey") && inEntries(j1.at("reuseKey")) && inEntries(j2.at("reuseKey"))
|
||
|
|
&& std::ranges::contains(j2.at("otherKeys"), j1.at("reuseKey")));
|
||
|
|
Check("...and in its own environment it is reused", round("none", "SEND-READY").first == "reused" && round("supabase", "SEND-READY").first == "reused");
|
||
|
|
g.state["entries"].erase(j2.at("reuseKey").get<std::string>());
|
||
|
|
const auto [s3, j3] = round("supabase", "FIX FIRST");
|
||
|
|
Check("store: two environments, one unit, different verdicts - the supabase pass finds a defect the none pass read clean: neither verdict survives, so switching the lens back to none re-reads the unit",
|
||
|
|
s3 == "ran" && !inEntries(j1.at("reuseKey")) && !inEntries(j3.at("reuseKey")) && round("none", "SEND-READY").first == "ran");
|
||
|
|
// The whole pass: its key carries the environment, and a whole pass that
|
||
|
|
// is not clean drops the other environments' whole verdicts.
|
||
|
|
Json wj = Plan::PlanLens("repo-better-solution.md", g.units, g.lines).whole;
|
||
|
|
wj["gate"] = "G";
|
||
|
|
g.environments = Defaults();
|
||
|
|
g.records.clear();
|
||
|
|
g.ReuseWhole("repo-better-solution", wj);
|
||
|
|
const std::string kwNone = wj.at("reuseKey").get<std::string>();
|
||
|
|
g.state["entries"][kwNone] = {{"lens", "repo-better-solution.md"}, {"unit", nullptr}, {"kind", "whole"}, {"hash", "H"}, {"run", "r"},
|
||
|
|
{"coverage", Json::array({{{"item", "whole:order"}, {"verdict", "clear"}, {"basis", ""}}, {{"item", "whole:absent"}, {"verdict", "clear"}, {"basis", ""}}})},
|
||
|
|
{"findings", Json::array()}, {"notes", Json::array()}, {"summary", "s"}};
|
||
|
|
Json copy = wj;
|
||
|
|
const bool reusedNone = g.ReuseWhole("repo-better-solution", copy).has_value();
|
||
|
|
g.environments["repo-better-solution.md"] = "supabase";
|
||
|
|
Json ws = wj;
|
||
|
|
const bool reusedSup = g.ReuseWhole("repo-better-solution", ws).has_value();
|
||
|
|
g.records = {{{"label", ws.at("label")}, {"lens", "repo-better-solution"}, {"lensFile", "repo-better-solution.md"}, {"unit", nullptr}, {"kind", "whole"}, {"status", "ok"}, {"reuseKey", ws.at("reuseKey")}, {"otherKeys", ws.at("otherKeys")}}};
|
||
|
|
g.results = {{{"lens", "repo-better-solution"}, {"coverage", Json::array()}, {"findings", Json::array({finding("whole:order", false)})}, {"notes", Json::array()}, {"parts", Json::array()}}};
|
||
|
|
g.wholeParts = {{"repo-better-solution", {{"verdict", "FIX FIRST"}, {"coverageMissing", Json::array()}, {"status", "ok"}}}};
|
||
|
|
g.StoreState();
|
||
|
|
Check("store: the whole pass - a clean whole verdict is reused in its environment and not in another; a whole pass that is not clean in supabase drops the clean one none held",
|
||
|
|
reusedNone && !reusedSup && ws.at("reuseKey") != kwNone && std::ranges::contains(ws.at("otherKeys"), Json(kwNone)) && !g.state.at("entries").contains(kwNone));
|
||
|
|
// Resume across environments: the checkpoint's `environments` (none in a
|
||
|
|
// checkpoint written before 2026-10-16.1 = the defaults).
|
||
|
|
g.resume = {{"run", "r1"},
|
||
|
|
{"results", Json::array({{{"lens", "repo-bug-prover"}, {"verdict", "SEND-READY"}, {"findings", Json::array()}, {"coverageMissing", Json::array()}, {"parts", Json::array()}}})},
|
||
|
|
{"environments", {{"repo-bug-prover", "supabase"}}}};
|
||
|
|
g.newRound = false;
|
||
|
|
g.records.clear();
|
||
|
|
g.environments = Defaults();
|
||
|
|
const bool no = g.Resumable("repo-bug-prover", "repo-bug-prover.md", "G").has_value();
|
||
|
|
g.environments["repo-bug-prover.md"] = "supabase";
|
||
|
|
const std::optional<Json> yes = g.Resumable("repo-bug-prover", "repo-bug-prover.md", "G");
|
||
|
|
g.resume.erase("environments");
|
||
|
|
const bool old = g.Resumable("repo-bug-prover", "repo-bug-prover.md", "G").has_value();
|
||
|
|
g.environments["repo-bug-prover.md"] = "none";
|
||
|
|
const bool oldDefault = g.Resumable("repo-bug-prover", "repo-bug-prover.md", "G").has_value();
|
||
|
|
Check("resume: a lens result is resumed only in the environment it was bought in (an old checkpoint without environments = the defaults)",
|
||
|
|
!no && yes && yes->at("verdict") == "SEND-READY" && !old && oldDefault);
|
||
|
|
|
||
|
|
// The preflight of a gate with the database under test.
|
||
|
|
const fs::path repo = td / "mig";
|
||
|
|
fs::create_directories(repo / "supabase" / "migrations");
|
||
|
|
for (const std::string_view f : {"20261001000000_a.sql", "20261002000000_b.sql"}) Write(repo / "supabase" / "migrations" / f, "select 1;\n");
|
||
|
|
Write(repo / "supabase" / "migrations" / "README", "not a migration\n");
|
||
|
|
Git(repo, {"init", "-q"});
|
||
|
|
Git(repo, {"add", "."});
|
||
|
|
Git(repo, {"commit", "-qm", "base"});
|
||
|
|
const std::string mbase = Git(repo, {"rev-parse", "HEAD"});
|
||
|
|
Write(repo / "supabase" / "migrations" / "20261002000000_b.sql", "-- b, now with a filter\nselect 2 where true;\n");
|
||
|
|
Git(repo, {"commit", "-qam", "the range changes b"});
|
||
|
|
g.trees = {repo.string()};
|
||
|
|
g.a.base = mbase;
|
||
|
|
// What the CLI records: the statements, its own spacing, no comment.
|
||
|
|
const std::string appliedA = "select 1";
|
||
|
|
const std::string appliedB = "select 2 where true";
|
||
|
|
using Rows = std::vector<std::vector<std::optional<std::string>>>;
|
||
|
|
auto rows = [](Rows r) { return [r](std::string_view, std::string_view) { return Run::QueryResult({"version", "s"}, r); }; };
|
||
|
|
g.query = rows({{"20261001000000", appliedA}, {"20261002000000", appliedB}});
|
||
|
|
const auto same = g.MigrationsDrift("u");
|
||
|
|
g.query = rows({{"20261001000000", appliedA}});
|
||
|
|
const auto behind = g.MigrationsDrift("u");
|
||
|
|
g.query = rows({{"20261001000000", appliedA}, {"20261002000000", appliedB}, {"20260901000000", "x"}});
|
||
|
|
const auto ahead = g.MigrationsDrift("u");
|
||
|
|
g.query = rows({{"20261001000000", "select 1 -- other text, a base migration: versions alone"}, {"20261002000000", "select 1"}});
|
||
|
|
const auto stale = g.MigrationsDrift("u");
|
||
|
|
g.query = [](std::string_view, std::string_view sql) -> Run::QueryResult {
|
||
|
|
if (sql.contains("statements")) throw PgWire::SqlError({{'C', "42703"}, {'M', "column \"statements\" does not exist"}});
|
||
|
|
return {{"version", "s"}, {{"20261001000000", std::nullopt}, {"20261002000000", std::nullopt}}};
|
||
|
|
};
|
||
|
|
const auto oldCli = g.MigrationsDrift("u");
|
||
|
|
g.query = [](std::string_view, std::string_view) -> Run::QueryResult { throw PgWire::SqlError({{'C', "42P01"}, {'M', "relation \"supabase_migrations.schema_migrations\" does not exist"}}); };
|
||
|
|
const auto plain = g.MigrationsDrift("u");
|
||
|
|
auto has = [](const std::optional<std::string>& text, std::string_view part) { return text && text->contains(part); };
|
||
|
|
Check("preflight: the database's applied migrations must be the range HEAD's - one not applied, one applied that HEAD lacks, or a migration of the range applied with other content (amended, the stack not reset) is evidence drift",
|
||
|
|
!same.first && has(same.second, "2 applied migrations") && has(same.second, "1 of the range compared by content") && has(behind.first, "at HEAD, not applied: 20261002000000")
|
||
|
|
&& has(ahead.first, "applied, not at HEAD: 20260901000000") && has(stale.first, "applied with other content than HEAD has: 20261002000000") && !has(stale.first, "20261001000000"),
|
||
|
|
std::format("{} | {} | {} | {}", same.second.value_or("-"), behind.first.value_or("-"), ahead.first.value_or("-"), stale.first.value_or("-")));
|
||
|
|
Check("preflight: the content is compared blind to comments, whitespace and semicolons; an old CLI without the statements column compares versions; a database without the table is a note",
|
||
|
|
PgWire::Fingerprint("-- b, now with a filter\nselect 2 where true;\n") == PgWire::Fingerprint("select 2 where true") && PgWire::Fingerprint("select 2 where true") != PgWire::Fingerprint("select 2 where false")
|
||
|
|
&& PgWire::Fingerprint("select '--not a comment'") != PgWire::Fingerprint("select") && !oldCli.first && !plain.first && has(plain.second, "not compared"));
|
||
|
|
g.trees = {td.string()};
|
||
|
|
Check("preflight: a tree without supabase/migrations/ at HEAD compares nothing", !g.MigrationsDrift("u").first);
|
||
|
|
g.query = [](std::string_view url, std::string_view sql) { return PgWire::Query(url, sql); };
|
||
|
|
g.cfg = {{"supabase", {{"dbUrl", "postgresql://postgres:postgres@127.0.0.1:1/postgres"}, {"start", nullptr}, {"stop", nullptr}, {"notes", ""}}},
|
||
|
|
{"functional", {{"start", "bash x.sh full"}, {"readyPath", "/"}, {"exclude", Json::array()}}},
|
||
|
|
{"slots", {{"dir", nullptr}, {"start", nullptr}, {"stop", nullptr}, {"memMb", nullptr}}}};
|
||
|
|
g.slotMode = false;
|
||
|
|
g.slotProblems.clear();
|
||
|
|
g.loginFixture = std::nullopt;
|
||
|
|
g.stacks = std::make_unique<Pool::Stacks>(std::vector<Json> {
|
||
|
|
{{"id", "the stack"}, {"appUrl", "http://localhost:1"}, {"apiUrls", Json::array()}, {"dbUrl", g.cfg.at("supabase").at("dbUrl")}, {"login", nullptr}, {"slot", false}}});
|
||
|
|
g.environments = Defaults();
|
||
|
|
g.environments["repo-bug-prover.md"] = "supabase";
|
||
|
|
std::vector<std::string> logs;
|
||
|
|
g.log = [&](const std::string& line) { logs.push_back(line); };
|
||
|
|
auto preflight = [&](const std::string& harness, bool dryRun) -> std::pair<std::optional<std::int32_t>, std::string> {
|
||
|
|
g.a.harness = harness;
|
||
|
|
g.a.dryRun = dryRun;
|
||
|
|
try {
|
||
|
|
g.InstrumentPreflight(std::vector<std::string> {"repo-bug-prover.md"});
|
||
|
|
return {std::nullopt, ""};
|
||
|
|
} catch (const Run::Preflight& e) {
|
||
|
|
return {3, std::format("preflight: {}\n", e.what())};
|
||
|
|
}
|
||
|
|
};
|
||
|
|
const auto [refusedOc, whyOc] = preflight("opencode", true);
|
||
|
|
preflight("claude", true);
|
||
|
|
const auto [dead, whyDead] = preflight("claude", false);
|
||
|
|
Check("preflight: the opencode harness refuses a lens with the database (exit 3); a dry run names the masked URL and probes nothing; a database that does not answer stops the run before its gate (exit 3)",
|
||
|
|
refusedOc == 3 && whyOc.contains("needs the claude harness") && std::ranges::any_of(logs, [](const std::string& x) { return x.contains("dry run") && x.contains("postgres:***@"); }) && dead == 3
|
||
|
|
&& whyDead.contains("does not answer") && whyDead.contains("(bash x.sh full)") && !whyDead.contains(":postgres@"),
|
||
|
|
whyDead);
|
||
|
|
|
||
|
|
fs::remove_all(td);
|
||
|
|
return Failures == 0 ? 0 : 1;
|
||
|
|
}
|