Crafter.Review/tests/Runs/main.cpp
Jorijn van der Graaf 32a0de1a07
fix: what an audit of the documented behaviour found
An independent check of every behaviour AGENTS.md was to describe:
- `--units ,` and `--only ,` skipped their empty items and so had no
  filter left: a typo started a run of every unit. They are usage errors
  now (exit 3).
- Two fixes the plan lists were missing: a dry run now clears the prompts
  a dry run before it left in the dir they share (the report counted
  them), and `config` reports an absolute functional.mcp.command that is
  no program here - /usr/bin/npx inside the Flatpak, whose /usr is the
  Sdk's.
- A patch that is not UTF-8 stops `run` at preflight (exit 3), as the
  plan has it; it exited 1.
- An exception no command caught aborted the process (134), e.g. a home
  that holds no lenses: it ends with its message and exit 1. An error
  escaping `run` or `freeze` now also clears the signal handler's run and
  sweeps what is left below the subreaper.
- `config -h` printed "unrecognized arguments" (exit 3): it prints the
  usage, exit 0.
- An API seat's .claude.json is linked into place, so a second writer
  never replaces the first's file and what claude wrote to it since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 16:45:53 +01:00

457 lines
27 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Full runs end to end, at no cost: this binary as crafter-review on a
// scratch repository, and a claude stand-in (/bin/sh) that answers every pass
// from what the driver hands it. A run SIGKILLed in its last gate, then the
// next one resuming its first gate from the checkpoint and salvaging the
// outputs of its last; a run with nothing left to buy; a FIX FIRST; a stop by
// `crafter-review stop`'s file and by SIGTERM; a stop between two gates. No
// pass outlives any of them.
#include <signal.h>
#include <unistd.h>
import std;
import Crafter.Review;
namespace fs = std::filesystem;
using namespace Crafter::Review;
namespace {
std::int32_t Failures = 0;
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::string Read(const fs::path& path) {
std::ifstream in(path, std::ios::binary);
return {std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>()};
}
std::vector<std::string> Lines(const fs::path& path) {
const std::string text = Read(path);
return text.empty() ? std::vector<std::string>() : Dyn::Split(Dyn::RStrip(text, "\n"), "\n");
}
// claude for the runs: the lens from its system file, the units from its
// prompt, every unit clear. Each pass adds a line to $FAKE_DIR/calls (its
// pid, its lens, its units). FAKE_FIX: a lens:unit that comes back a
// defect; FAKE_SLEEP: seconds a pass waits before it answers - every pass,
// or the one FAKE_SLEEP_ON names. It speaks first, so the warm-up lets
// the passes behind it go.
constexpr std::string_view FakeClaude = R"sh(#!/bin/sh
set -f
[ "$1" = --version ] && { echo '2.1.314 (Claude Code)'; exit 0; }
sys=
model=
sid=
prev=
for a in "$@"; do
case $prev in
--append-system-prompt-file) sys=$a ;;
--model) model=$a ;;
--session-id) sid=$a ;;
esac
prev=$a
done
lens=$(sed -n 's/^# Lens: //p' "$sys" | head -n 1)
ids=$(sed -n -e 's/^SCOPE - BLOCK PASS, unit "\([^"]*\)".*/\1/p' -e 's/^- \(whole:[a-z]*\) \[.*/\1/p')
echo "$$ $lens" $ids >> "$FAKE_DIR/calls"
echo "{\"type\":\"system\",\"subtype\":\"init\",\"session_id\":\"$sid\",\"model\":\"$model\",\"mcp_servers\":[],\"tools\":[\"Read\"]}"
echo '{"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"reading"}]}}'
pause=$FAKE_SLEEP
if [ -n "$FAKE_SLEEP_ON" ]; then
pause=
for id in $ids; do [ "$lens:$id" = "$FAKE_SLEEP_ON" ] && pause=$FAKE_SLEEP; done
fi
if [ -n "$pause" ]; then
sleep "$pause" &
echo "$!" >> "$FAKE_DIR/sleeps"
wait "$!"
fi
verdict=SEND-READY
findings=
coverage=
summary=
for id in $ids; do
v=clear
if [ "$lens:$id" = "$FAKE_FIX" ]; then
v=defect
verdict='FIX FIRST'
findings="{\"finding\":\"planted\",\"evidence\":\"hello.py:2\",\"proposedAction\":\"fix it\",\"items\":[\"$id\"]}"
fi
coverage="$coverage${coverage:+,}{\"item\":\"$id\",\"verdict\":\"$v\",\"basis\":\"read at HEAD\"}"
case $id in whole:*) summary=',"summary":"the approach holds"' ;; esac
done
echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\":\"ok\",\"structured_output\":{\"verdict\":\"$verdict\",\"findings\":[$findings],\"notes\":[],\"coverage\":[$coverage]$summary},\"usage\":{\"input_tokens\":1,\"output_tokens\":1},\"total_cost_usd\":0.01}"
)sh";
// The environment of every child: git with no user config (the user's
// signs commits), this checkout as the home, a board of the test's own,
// and none of the review or seat variables of the shell the test runs in.
std::map<std::string, std::string> Env;
Process::Captured Git(const fs::path& repo, std::vector<std::string> args) {
args.insert(args.begin(), {"git", "-C", repo.string()});
return Process::Run(args, std::nullopt, std::chrono::seconds(60), {}, Env);
}
std::map<std::string, std::string> With(std::map<std::string, std::string> env, const std::map<std::string, std::string>& changes) {
for (const auto& [k, v] : changes) env[k] = v;
return env;
}
// The stand-in's processes and their sleeps that still run.
std::vector<std::string> Leftovers(const fs::path& fakeDir) {
std::vector<std::string> alive;
auto look = [&](const fs::path& list, std::string_view marker) {
for (const std::string& line : Lines(list)) {
const std::string pid = line.substr(0, line.find(' '));
const std::string cmdline = Read(std::format("/proc/{}/cmdline", pid));
if (cmdline.contains(marker)) alive.push_back(std::format("{}: {}", pid, Dyn::Replace(cmdline, std::string(1, '\0'), " ")));
}
};
look(fakeDir / "calls", (fakeDir / "claude").string());
look(fakeDir / "sleeps", "sleep");
return alive;
}
// Leftovers once the supervisors have had their moment.
std::vector<std::string> LeftoversAfter(const fs::path& fakeDir) {
for (std::int32_t i = 0; i < 100; ++i) {
if (Leftovers(fakeDir).empty()) break;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return Leftovers(fakeDir);
}
bool WaitFor(const std::function<bool()>& ready, std::chrono::seconds limit) {
const auto end = std::chrono::steady_clock::now() + limit;
while (std::chrono::steady_clock::now() < end) {
if (ready()) return true;
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
return ready();
}
// The dump of the highest run number on this hash.
Json LatestDump(const fs::path& gateDir, std::string_view h8) {
const std::string prefix = std::format("commits-{}-run-", h8);
std::int64_t best = 0;
fs::path found;
for (const fs::directory_entry& entry : fs::directory_iterator(gateDir)) {
const std::string name = entry.path().filename().string();
if (!name.starts_with(prefix) || !name.ends_with(".json")) continue;
const std::string number = name.substr(prefix.size(), name.size() - prefix.size() - 5);
if (number.empty() || !std::ranges::all_of(number, [](char c) { return c >= '0' && c <= '9'; })) continue;
if (const std::int64_t n = std::stoll(number); n > best) {
best = n;
found = entry.path();
}
}
return found.empty() ? Json() : Dyn::LoadJsonFile(found);
}
// The run number in a dump's own name.
std::int64_t RunNumber(const Json& dump) {
if (!dump.is_object()) return 0;
const std::string path = Dyn::Str(dump.at("dumpPath"));
const std::size_t at = path.rfind("-run-");
return at == std::string::npos ? 0 : std::stoll(path.substr(at + 5));
}
std::int64_t Count(const Json& records, std::string_view key, const Json& value) {
return std::ranges::count_if(records, [&](const Json& r) { return Dyn::Get(r, key) == value; });
}
std::string Text(const Json& value) {
return value.is_string() ? value.get<std::string>() : value.dump();
}
std::int32_t Checks() {
std::println("runs: a full run on a claude stand-in - killed and salvaged, resumed, nothing left to buy, FIX FIRST, stopped by the stop file, by SIGTERM and between gates");
Platform::SetMode(Platform::Mode::Native);
const fs::path checkout = fs::current_path();
SetHome(checkout);
const fs::path base = fs::temp_directory_path() / std::format("crafter-review-runs-{}", Platform::PublishedPid());
fs::remove_all(base);
const fs::path repo = base / "repo";
const fs::path fakeDir = base / "fake";
const fs::path fake = fakeDir / "claude";
const fs::path scratchRoot = base / "scratch";
Write(fake, FakeClaude);
fs::permissions(fake, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec);
for (const auto& [k, v] : Platform::Environment()) {
const bool review = k.starts_with("REVIEW_GATES_") || k.starts_with("CRAFTER_REVIEW_") || k == "SEATS_ENV_FILE";
const bool seat = k.starts_with("CLAUDE_CODE_OAUTH_TOKEN") || k.starts_with("ANTHROPIC_API_KEY") || k.starts_with("OPENROUTER_KEY") || k.starts_with("FAKE_");
if (!review && !seat) Env[k] = v;
}
Env = With(Env, {{"GIT_CONFIG_GLOBAL", "/dev/null"}, {"GIT_CONFIG_SYSTEM", "/dev/null"}, {"GIT_AUTHOR_NAME", "Me"}, {"GIT_AUTHOR_EMAIL", "me@example.com"},
{"GIT_COMMITTER_NAME", "Me"}, {"GIT_COMMITTER_EMAIL", "me@example.com"}, {"CRAFTER_REVIEW_HOME", checkout.string()},
{"REVIEW_GATES_BOARD", (base / "board.json").string()}, {"FAKE_DIR", fakeDir.string()}});
// A repository with one commit to review, the functional lens off (it
// needs a running application).
fs::create_directories(repo);
Git(repo, {"init", "-q", "-b", "main"});
Git(repo, {"config", "user.email", "me@example.com"});
Git(repo, {"config", "user.name", "Me"});
Write(repo / ".gitignore", ".review-gates/push/\n.review-gates/trees/\n");
Write(repo / ".review-gates" / "config.json", R"({"lenses": {"repo-functional": {"run": false}}})");
Write(repo / "README.md", "a greeter\n");
Git(repo, {"add", "-A"});
Git(repo, {"commit", "-q", "-m", "base"});
const std::string baseSha = Dyn::Strip(Git(repo, {"rev-parse", "HEAD"}).out);
Write(repo / "hello.py", "def hello(name):\n return f'hello {name}'\n");
Git(repo, {"add", "hello.py"});
Git(repo, {"commit", "-q", "-m", "add hello\n\nThe greeting the README promises."});
const Process::Captured frozen = Process::Run({Platform::SelfExe().string(), "freeze", "--slug", "x", "--base", baseSha}, repo, std::chrono::seconds(60), {}, Env);
const fs::path artifact = repo / ".review-gates" / "push" / "x";
const fs::path gateDir = artifact / "gate-state";
Json freezeJson;
try {
freezeJson = Dyn::LoadJsonFile(artifact / "freeze.json");
} catch (const std::exception&) {
Check("freeze: the scratch range freezes", false, frozen.out + frozen.err);
return 1;
}
const std::string hash = freezeJson.at("hash").get<std::string>();
const std::string h8 = hash.substr(0, 8);
auto runArgs = [&](std::vector<std::string> extra) {
std::vector<std::string> args = {Platform::SelfExe().string(), "run", "--artifact", ".review-gates/push/x", "--hash", hash, "--base", baseSha, "--trees", ".",
"--dispositions", ".review-gates/push/x/gate-state/dispositions.json", "--date", Text(freezeJson.at("date")),
"--claude-bin", fake.string(), "--scratch", scratchRoot.string(), "--seats", "local", "--no-probe", "--reads", "1"};
std::ranges::move(extra, std::back_inserter(args));
return args;
};
auto run = [&](std::vector<std::string> extra, const std::map<std::string, std::string>& env = {}) {
return Process::Run(runArgs(std::move(extra)), repo, std::chrono::seconds(60), {}, With(Env, env));
};
auto start = [&](std::vector<std::string> extra, const std::map<std::string, std::string>& env, std::string_view name) {
return Process::Start({.argv = runArgs(std::move(extra)),
.environment = With(Env, env),
.cwd = repo,
.input = {.kind = Process::Stream::Kind::Null},
.output = {.kind = Process::Stream::Kind::File, .file = base / std::format("{}.out", name)},
.error = {.kind = Process::Stream::Kind::File, .file = base / std::format("{}.err", name)}});
};
auto calls = [&] { return static_cast<std::int64_t>(Lines(fakeDir / "calls").size()); };
// The plan, from a dry run: what every full run below buys.
const Process::Captured dry = run({"--dry-run"});
Json plan;
try {
plan = Dyn::LoadJsonFile(scratchRoot / std::format("{}-run-1-dry", h8) / "dry-run.json");
} catch (const std::exception&) {
}
const Json& planned = plan.is_object() ? plan.at("processes") : Json::array();
const auto all = static_cast<std::int64_t>(planned.size());
const std::int64_t gate1 = Count(planned, "lens", "repo-better-solution");
std::string securityUnit;
std::string proverUnit;
for (const Json& p : planned) {
if (p.at("kind") != "block") continue;
if (securityUnit.empty() && p.at("lens") == "repo-security") securityUnit = Text(p.at("unit"));
if (proverUnit.empty() && p.at("lens") == "repo-bug-prover") proverUnit = Text(p.at("unit"));
}
Check("dry run: the range plans the approach lens's whole pass and blocks, then the bug-prover's and the security lens's blocks - and no pass reaches claude",
dry.returnCode == 2 && all > gate1 && gate1 > 1 && !securityUnit.empty() && !proverUnit.empty() && calls() == 0 && Count(planned, "lens", "repo-functional") == 0,
dry.out.substr(dry.out.size() > 2000 ? dry.out.size() - 2000 : 0) + dry.err);
// Dry runs with no full run between them share one dir: the next
// drops the last one's prompts.
const fs::path dryDir = scratchRoot / std::format("{}-run-1-dry", h8);
Write(dryDir / "prompts" / "stale.txt", "a prompt of another range");
const Process::Captured again = run({"--dry-run"});
std::int64_t prompts = 0;
for (const fs::directory_entry& entry : fs::directory_iterator(dryDir / "prompts")) prompts += entry.path().extension() == ".txt" ? 1 : 0;
Check("dry run: a second dry run on a hash counts its own prompts, the one before's gone", again.returnCode == 2 && !fs::exists(dryDir / "prompts" / "stale.txt") && prompts == all,
std::to_string(prompts));
// Killed in its last gate: one security pass never answers, and the
// driver is SIGKILLed once every other pass has.
const fs::path scratchA = scratchRoot / std::format("{}-run-1", h8);
const Process::Child a = start({}, {{"FAKE_SLEEP", "571"}, {"FAKE_SLEEP_ON", std::format("repo-security:{}", securityUnit)}}, "a");
auto answered = [&] {
std::int64_t n = 0;
std::error_code error;
for (fs::directory_iterator it(scratchA / "out", error), end; !error && it != end; it.increment(error)) {
if (it->path().extension() == ".jsonl" && Read(it->path()).contains(R"("type":"result")")) ++n;
}
return n;
};
const bool settled = WaitFor([&] { return calls() == all && answered() == all - 1; }, std::chrono::seconds(30));
Process::Signal(a.pid, SIGKILL);
const std::optional<std::int32_t> aCode = Process::Wait(a.pid, std::chrono::seconds(10));
const std::vector<std::string> aLeft = LeftoversAfter(fakeDir);
Json resumeA;
try {
resumeA = Dyn::LoadJsonFile(gateDir / std::format("resume-{}.json", h8));
} catch (const std::exception&) {
}
const Json dumpA = LatestDump(gateDir, h8);
Check("SIGKILL: a driver killed in its last gate takes its passes with it (each pass's supervisor ends its tree); its first gate is on record in the checkpoint and the partial dump",
settled && aCode == -9 && aLeft.empty() && dumpA.is_object() && Text(dumpA.at("overall")).starts_with("IN PROGRESS - after Gate 1 — Approach") && resumeA.is_object()
&& resumeA.at("results").size() == 1 && resumeA.at("results").at(0).at("lens") == "repo-better-solution",
std::format("settled {}, exit {}, left {}, {} of {} answered, {}", settled, aCode.value_or(0), Dyn::Join(aLeft, "; "), answered(), all, resumeA.dump().substr(0, 300)));
// The next run: the first gate resumed, the outputs of the killed run
// salvaged, the one pass that never answered bought again.
std::int64_t before = calls();
const Process::Captured b = run({});
const Json dumpB = LatestDump(gateDir, h8);
const Json& processesB = dumpB.is_object() ? dumpB.at("processes") : Json::array();
Check("the next run resumes the first gate from the checkpoint, salvages every output the killed run finished and buys only the pass that never answered - SEND-READY, exit 0",
b.returnCode == 0 && calls() - before == 1 && dumpB.is_object() && dumpB.at("overall") == "SEND-READY" && dumpB.at("resumedLenses") == Json::array({"repo-better-solution"})
&& std::ranges::count_if(processesB, [](const Json& r) { return Dyn::Truthy(Dyn::Get(r, "salvaged")); }) == all - gate1 - 1
&& Count(processesB, "status", "ok") == all - gate1,
std::format("exit {}, {} call(s), {}", b.returnCode, calls() - before, dumpB.is_object() ? Text(dumpB.at("overall")) : b.out + b.err));
const Json stored = State::LoadState(gateDir / "unit-state.json").at("entries");
Check("...and stores the clean block results of the range for reuse", static_cast<std::int64_t>(stored.size()) >= all - gate1, std::to_string(stored.size()));
// Nothing left to buy.
before = calls();
const Process::Captured c = run({});
const Json dumpC = LatestDump(gateDir, h8);
Check("a run on the same hash has nothing left to buy: every lens resumes, claude is never called, SEND-READY",
c.returnCode == 0 && calls() == before && dumpC.is_object() && dumpC.at("overall") == "SEND-READY" && dumpC.at("resumedLenses").size() == 3,
std::format("exit {}, {} call(s)", c.returnCode, calls() - before));
// A defect, on a run that reuses nothing; the scratch dir it claims
// holds a stop file from before, which is not this run's.
const std::string nextRun = std::format("{}-run-{}", h8, RunNumber(dumpC) + 1);
const fs::path stale = scratchRoot / nextRun / "stop";
Write(stale, "");
before = calls();
const Process::Captured d = run({"--no-reuse", "--no-salvage"},
{{"FAKE_FIX", std::format("repo-bug-prover:{}", proverUnit)}, {"FAKE_SLEEP", "3"}, {"FAKE_SLEEP_ON", "repo-better-solution:whole:order"}});
const Json dumpD = LatestDump(gateDir, h8);
Check("FIX FIRST: a defect in the last gate stops the run there, exit 1 - every pass bought again with --no-reuse --no-salvage",
d.returnCode == 1 && calls() - before == all && dumpD.is_object() && dumpD.at("overall") == "FIX FIRST" && dumpD.at("stoppedAt") == "Gate 3 — Proof",
std::format("exit {}, {} call(s), {}", d.returnCode, calls() - before, dumpD.is_object() ? Text(dumpD.at("stoppedAt")) : d.out + d.err));
Check("stop: a stop file left in the scratch dir a run claims is dropped - the run ran its course past the watcher's first look",
!fs::exists(stale) && dumpD.is_object() && Dyn::Str(dumpD.at("scratch")).ends_with(nextRun), nextRun);
// Stopped by `crafter-review stop`'s file, and by SIGTERM, while the
// first gate's whole pass is under way: the pass is killed, no later
// gate starts, nothing is left running.
for (const bool bySignal : {false, true}) {
before = calls();
const std::string name = bySignal ? "term" : "file";
const Process::Child child = start({"--no-reuse", "--no-salvage"}, {{"FAKE_SLEEP", "573"}}, name);
const bool running = WaitFor([&] { return calls() > before; }, std::chrono::seconds(30));
std::string scratch;
for (const std::string& line : Lines(base / std::format("{}.out", name))) {
if (const std::size_t at = line.rfind("; scratch "); at != std::string::npos) scratch = line.substr(at + 10);
}
if (bySignal) {
Process::Signal(child.pid, SIGTERM);
} else if (!scratch.empty()) {
Write(fs::path(scratch) / "stop", "");
}
const std::optional<std::int32_t> code = Process::Wait(child.pid, std::chrono::seconds(30));
if (!code) Process::Signal(child.pid, SIGKILL);
const std::vector<std::string> left = LeftoversAfter(fakeDir);
const Json dump = LatestDump(gateDir, h8);
const std::string why = bySignal ? "stopped by signal 15 (1 running processes killed)" : "stopped by crafter-review stop (1 running processes killed)";
Check(bySignal ? "SIGTERM: the run stops as the stop file stops it - the same abort, the same exit, nothing left running"
: "stop file: `crafter-review stop` stops the run - its running pass killed, no later gate started, INCOMPLETE, exit 2, nothing left running",
running && code == 2 && calls() - before == 1 && left.empty() && dump.is_object() && dump.at("stoppedAt") == std::format("Gate 1 — Approach - ABORTED ({})", why)
&& Text(dump.at("overall")).starts_with("INCOMPLETE - NOT SEND-READY"),
std::format("running {}, exit {}, {} call(s), left {}, {}", running, code.value_or(-1), calls() - before, Dyn::Join(left, "; "),
dump.is_object() ? Text(dump.at("stoppedAt")) : Read(base / std::format("{}.err", name))));
}
// An env file that is not UTF-8: the preflight stops the run.
Write(base / "bad.env", std::string("\xff\n", 2));
const Process::Captured badEnv = run({"--no-reuse", "--no-salvage", "--env-file", (base / "bad.env").string()});
Check("preflight: an env file that is not UTF-8 stops the run, exit 3, with the decoder's own words",
badEnv.returnCode == 3 && badEnv.err.contains("preflight: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"), badEnv.err);
// A filter that names nothing is refused: skipping its empty items
// must not leave no filter, which is a full run.
const Process::Captured noUnits = run({"--units", ","});
const Process::Captured noLenses = run({"--only", " , "});
Check("--units and --only that name nothing are usage errors (exit 3) - never a full run",
noUnits.returnCode == 3 && noUnits.err.contains("argument --units: names no unit id (',')") && noLenses.returnCode == 3
&& noLenses.err.contains("argument --only: names no lens file (' , ')"),
noUnits.err + noLenses.err);
// A patch that is not UTF-8 is no artifact: a preflight stop.
const fs::path badArtifact = repo / ".review-gates" / "push" / "bad";
fs::copy(artifact, badArtifact, fs::copy_options::recursive);
for (const fs::directory_entry& entry : fs::directory_iterator(badArtifact)) {
if (entry.path().extension() == ".patch") std::ofstream(entry.path(), std::ios::binary | std::ios::app) << std::string("\xff\n", 2);
}
const Process::Captured badPatch = Process::Run({Platform::SelfExe().string(), "run", "--artifact", ".review-gates/push/bad", "--base", baseSha, "--trees", ".", "--dispositions",
".review-gates/push/bad/gate-state/dispositions.json", "--claude-bin", fake.string(), "--scratch", scratchRoot.string(), "--dry-run"},
repo, std::chrono::seconds(60), {}, Env);
Check("preflight: a patch that is not UTF-8 stops the run, exit 3, saying so",
badPatch.returnCode == 3 && badPatch.err.contains("the artifact's patches are not UTF-8 ('utf-8' codec can't decode byte 0xff"), badPatch.err);
// An error that escapes a run ends it with its message, exit 1,
// through the run's cleanup.
const Process::Captured badReserve = run({"--no-reuse", "--no-salvage"}, {{"REVIEW_GATES_MEM_RESERVE_MB", "x"}});
Check("an error escaping a full run (a reserve that is no number) ends it with its ValueError, exit 1",
badReserve.returnCode == 1 && badReserve.err.contains("crafter-review: ValueError: could not convert string to float: 'x'"), badReserve.err);
// A stop between two gates: the next gate's pool starts aborted and
// runs nothing of it.
{
const fs::path fx = checkout / "tests" / "Goldens" / "fixture" / "commits";
Run::Run g;
g.log = [](const std::string&) {};
g.a.artifact = fx.string();
g.hash = Artifact::ArtifactHash("commits", fx);
g.testMode = true;
g.cfg = Config::Defaults();
g.environments = Config::Environments(g.cfg);
for (const Plan::Lens& lens : Plan::Lenses) {
const std::string lf(lens.file);
g.charters[lf] = Prompt::Charter(lf);
g.charterSha[lf] = Plan::Sha(g.charters[lf]);
}
const Inventory::Built built = Inventory::Build("commits", fx.string());
g.units = built.out.at("units");
g.lines = built.lines;
g.scratch = base / "between";
fs::create_directories(g.scratch);
g.calibration = std::make_unique<Memory::Calibration>(base / "between" / "memory.json", base, std::map<std::string, std::int64_t> {{"none", 210}});
std::int32_t ran = 0;
g.runPass = [&](const Json& job, const std::string&, const fs::path&, std::string_view, std::int64_t, std::int64_t, bool) {
++ran;
return std::pair<Json, Json>({{"label", job.at("label")}, {"status", "error"}, {"reason", "ran"}}, nullptr);
};
g.Stop("crafter-review stop");
const std::vector<Json> res = g.RunGate("Gate 3 — Proof", {"repo-bug-prover.md"});
Check("stop between gates: the next gate's pool starts aborted - none of its passes runs, and the abort is the run's",
ran == 0 && g.abort && g.abort->starts_with("stopped by crafter-review stop") && res.size() == 1 && !res[0].at("coverageMissing").empty(), g.abort.value_or("None"));
}
fs::remove_all(base);
return Failures ? 1 : 0;
}
}
int main(int argc, char** argv) {
const std::vector<std::string_view> args(argv, argv + argc);
// With arguments this binary is crafter-review itself - the driver, its
// supervisor, its helpers - in native mode whatever builds it.
if (args.size() > 1) {
Platform::SystemPaths paths = Platform::Paths();
paths.flatpakInfo = "/nonexistent/.flatpak-info";
Platform::SetPaths(paths);
return Crafter::Review::Main(args);
}
try {
return Checks();
} catch (const std::exception& error) {
std::println(std::cerr, "runs: {}", error.what());
return 1;
}
}