Crafter.Review/tests/Commands/main.cpp
Jorijn van der Graaf ceb87f70ef
All checks were successful
Flatpak / check (push) Has been skipped
Flatpak / publish (push) Successful in 5m30s
feat(seats): an API seat's cycle starts on its own grant day
The ledger reset every API seat's spend on one
REVIEW_GATES_API_CYCLE_DAY and cleared every credit-out, an OpenRouter
key's too, at that reset. Each API seat's cycle starts on its
ANTHROPIC_API_KEY<n>_GRANT_DAY now (1..31, a day past a month's end its
last), else the cycle day, also 1..31. A credit-out carries across the
rollover, and the run's probe tries a key held out on credit again, as
`seats` does an API key: an ok lifts the mark - an API key's is new
credits, its cycle restarting from the grant day, or from today when
that day is over a week back; an OpenRouter key keeps its cycle's spend,
and a key that answers with its cycle's budget spent stays out on that
budget. A probe that finds a key out of credit records it, unless the
ledger already has it out. Both are decided in one locked update on the
file's entry, a mark another driver made since the probe started
standing, and the ledger's stamps carry their UTC offset so marks are
ordered by instant.

`seats` shows the probe's reading beside the driver's verdict (the
`probe` and `driver` columns replace `state` and `ledger: out`), each
seat's grant day and when its next credits are due, and notes on what
the probe changed in the ledger or why the driver's verdict stands; the
run logs the days.
2026-11-02 18:44:02 +01:00

456 lines
37 KiB
C++

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The commands around a run, this binary as crafter-review: init (writes,
// refreshes, keeps what it did not write, replaces the old link into the
// submodule, refuses a foreign link, a subdirectory and the home directory
// before it writes anything), doc, stop (a scratch dir, --hash on the
// board), seats on a claude stand-in, and browser's usage.
#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) {
return Platform::ReadFile(path).value_or("");
}
// A claude that answers a seat probe as the CLI does: a rate_limit_event
// with both windows, and on a Fable request the Fable window too.
constexpr std::string_view FakeClaude = R"sh(#!/bin/sh
model=
prev=
for a in "$@"; do [ "$prev" = --model ] && model=$a; prev=$a; done
cat > /dev/null
# A Console key whose credit is gone, answering after two seconds.
if [ "$ANTHROPIC_API_KEY" = sk-ant-api-spent ]; then
sleep 2
echo '{"type":"result","subtype":"success","is_error":true,"result":"Credit balance is too low","total_cost_usd":0}'
exit 1
fi
now=$(date +%s)
fable=
[ "$model" = claude-fable-5-1 ] && fable=",\"seven_day_overage_included\":{\"utilization\":0.3,\"resetsAt\":$((now+172800))}"
echo '{"type":"system","subtype":"init","session_id":"0b6f3c56-1111-4222-8333-944455556666"}'
echo "{\"type\":\"rate_limit_event\",\"rate_limit_info\":{\"status\":\"allowed\",\"rateLimitType\":\"five_hour\",\"resetsAt\":$((now+3600)),\"unifiedWindows\":{\"five_hour\":{\"utilization\":0.42,\"resetsAt\":$((now+3600))},\"seven_day\":{\"utilization\":0.13,\"resetsAt\":$((now+259200))}$fable}}}"
echo '{"type":"result","subtype":"success","is_error":false,"result":"ok","total_cost_usd":0.0001}'
)sh";
// The environment of every child: git with no user config, this
// checkout as the home, nothing of the shell's review or seat setup.
std::map<std::string, std::string> 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;
}
Process::Captured Self(std::vector<std::string> args, const fs::path& cwd, const std::map<std::string, std::string>& env = {}) {
args.insert(args.begin(), Platform::SelfExe().string());
return Process::Run(args, cwd, std::chrono::seconds(60), {}, With(Env, env));
}
std::int32_t Checks() {
std::println("commands: init, doc, stop, seats on a claude stand-in, browser's usage");
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-commands-{}", Platform::PublishedPid());
fs::remove_all(base);
for (const auto& [k, v] : Platform::Environment()) {
const bool review = k.starts_with("REVIEW_GATES_") || k.starts_with("CRAFTER_REVIEW_") || k == "SEATS_ENV_FILE" || k == "CLAUDE_BIN";
const bool seat = k.starts_with("CLAUDE_CODE_OAUTH_TOKEN") || k.starts_with("ANTHROPIC_API_KEY") || k.starts_with("OPENROUTER_KEY");
if (!review && !seat) Env[k] = v;
}
// No seats file of the machine's own: XDG_CONFIG_HOME natively, and
// HOST_XDG_CONFIG_HOME, which the Flatpak's Platform::Init copies
// over it - a directory of this run's own, which nothing creates.
const std::string noConfig = (base / "no-config").string();
Env = With(Env, {{"XDG_CONFIG_HOME", noConfig}, {"HOST_XDG_CONFIG_HOME", noConfig}, {"GIT_CONFIG_GLOBAL", "/dev/null"}, {"GIT_CONFIG_SYSTEM", "/dev/null"}, {"CRAFTER_REVIEW_HOME", checkout.string()},
{"REVIEW_GATES_BOARD", (base / "board.json").string()}, {"REVIEW_GATES_SCRATCH", (base / "scratch").string()}, {"TZ", "UTC"}});
auto repo = [&](std::string_view name) {
const fs::path dir = base / name;
fs::create_directories(dir);
Process::Run({"git", "init", "-q", "-b", "main", dir.string()}, std::nullopt, std::chrono::seconds(60), {}, Env);
return dir;
};
const std::string shipped = Read(checkout / "process" / "skill" / "prepush" / "SKILL.md");
const std::string marker = "<!-- written by crafter-review init";
const std::string ignoreMarker = "# written by crafter-review init";
// init on a repository that has nothing yet, then again.
const fs::path r = repo("r");
const Process::Captured first = Self({"init"}, r);
const std::string skill = Read(r / ".claude" / "skills" / "prepush" / "SKILL.md");
const std::string skillIgnore = Read(r / ".claude" / "skills" / "prepush" / ".gitignore");
const Process::Captured firstStatus = Process::Run({"git", "-C", r.string(), "status", "--porcelain", "--untracked-files=all"}, std::nullopt, std::chrono::seconds(60), {}, Env);
const std::size_t frontmatter = shipped.find("\n---\n", 3) + 5;
Check("init: the config from the template (the three kinds as modes), the artifact and worktree dirs ignored, the skill with init's marker after its frontmatter and a .gitignore of `*` beside it - git sees nothing of the skill, by no rule of the repository's - the next steps (trim each mode's lenses; the browser line, since the template lists the functional lens)",
first.returnCode == 0 && Read(r / ".review-gates" / "config.json") == Read(checkout / "process" / "templates" / "config.json")
&& Read(r / ".gitignore") == ".review-gates/push/\n.review-gates/trees/\n" && skill.starts_with(shipped.substr(0, frontmatter))
&& skill.substr(frontmatter).starts_with(marker) && skill.ends_with(shipped.substr(frontmatter)) && skillIgnore.starts_with(ignoreMarker) && skillIgnore.ends_with("\n*\n")
&& firstStatus.returnCode == 0 && firstStatus.out.contains(".review-gates/config.json") && !firstStatus.out.contains(".claude")
&& first.out.contains("wrote .claude/skills/prepush/.gitignore") && first.out.contains(" config - until it reports no problem")
&& first.out.contains("trim each to what this repository runs") && first.out.contains(" browser install - "),
first.out + first.err + firstStatus.out);
// A kept config has no trim step (that line is the template's own,
// printed as init writes it); one that lists no lens is told to list
// them.
Write(r / ".review-gates" / "config.json", R"({"modes": {"commits": {"lenses": ["repo-facts"]}}})");
const Process::Captured listed = Self({"init"}, r);
Write(r / ".review-gates" / "config.json", R"({"base": "origin/dev"})");
const Process::Captured unlisted = Self({"init"}, r);
Check("init on a kept config: one whose modes list lenses gets no lens line; one that lists none is told to list them",
listed.returnCode == 0 && listed.out.contains("kept .review-gates/config.json") && !listed.out.contains("trim ") && !listed.out.contains("list the lenses")
&& unlisted.returnCode == 0 && unlisted.out.contains("list the lenses each mode runs in .review-gates/config.json (modes.<mode>.lenses) - every lens is off until a mode lists it"),
listed.out + unlisted.out);
Write(r / ".review-gates" / "config.json", R"({"base": "origin/dev"})");
Write(r / ".gitignore", "node_modules/\n/.review-gates/push\n");
Write(r / ".claude" / "skills" / "prepush" / "SKILL.md", std::format("---\nname: prepush\n---\n{} 0.0.1: old -->\nold text\n", marker));
const Process::Captured again = Self({"init"}, r);
Check("init again: the config and an entry already ignored are kept, the missing one added once, the skill it wrote refreshed, and the .gitignore beside it",
again.returnCode == 0 && Read(r / ".review-gates" / "config.json") == R"({"base": "origin/dev"})"
&& Read(r / ".gitignore") == "node_modules/\n/.review-gates/push\n.review-gates/trees/\n" && Read(r / ".claude" / "skills" / "prepush" / "SKILL.md") == skill
&& again.out.contains("refreshed .claude/skills/prepush/SKILL.md") && again.out.contains("refreshed .claude/skills/prepush/.gitignore")
&& Read(r / ".claude" / "skills" / "prepush" / ".gitignore") == skillIgnore,
again.out + again.err);
// The repository's own .gitignore is never edited for the skill: one
// that ignores .claude wholesale (Crafter.Build) is kept as it is.
Write(r / ".gitignore", "node_modules/\n/.review-gates/push\n.review-gates/trees/\n.claude\n");
const Process::Captured wholesale = Self({"init"}, r);
Check("init: a repository that ignores .claude wholesale keeps its .gitignore as it is, and the skill its own .gitignore",
wholesale.returnCode == 0 && Read(r / ".gitignore") == "node_modules/\n/.review-gates/push\n.review-gates/trees/\n.claude\n" && wholesale.out.contains("kept .gitignore")
&& Read(r / ".claude" / "skills" / "prepush" / ".gitignore") == skillIgnore,
wholesale.out + wholesale.err);
// A copy init wrote that the repository tracks (a base-era init
// ignored nothing): the note says how to untrack it, on every init.
// A hand-written tracked skill is kept and not ignored - the ignore
// file the earlier init wrote beside it goes - with no note.
const fs::path tracked = repo("tracked");
Self({"init"}, tracked);
Process::Run({"git", "-C", tracked.string(), "add", "-f", ".claude/skills/prepush/SKILL.md", ".gitignore"}, std::nullopt, std::chrono::seconds(60), {}, Env);
Process::Run({"git", "-C", tracked.string(), "-c", "user.email=me@example.com", "-c", "user.name=Me", "commit", "-q", "-m", "track the skill"}, std::nullopt, std::chrono::seconds(60), {}, Env);
const Process::Captured trackedInit = Self({"init"}, tracked);
const Process::Captured trackedAgain = Self({"init"}, tracked);
Write(tracked / ".claude" / "skills" / "prepush" / "SKILL.md", "my own skill\n");
Process::Run({"git", "-C", tracked.string(), "-c", "user.email=me@example.com", "-c", "user.name=Me", "commit", "-q", "-am", "my own skill"}, std::nullopt, std::chrono::seconds(60), {}, Env);
const Process::Captured handWritten = Self({"init"}, tracked);
const bool stillIgnored =
Process::Run({"git", "-C", tracked.string(), "check-ignore", "--no-index", "-q", ".claude/skills/prepush/SKILL.md"}, std::nullopt, std::chrono::seconds(60), {}, Env).returnCode == 0;
Check("init on a tracked skill: one init wrote gets the note with `git rm --cached` on every init; one the repository wrote is kept and no rule ignores it any more - the earlier init's .gitignore beside it removed - and no note",
trackedInit.returnCode == 0 && trackedInit.out.contains("git rm --cached .claude/skills/prepush/SKILL.md") && trackedAgain.returnCode == 0 && trackedAgain.out.contains("git rm --cached")
&& handWritten.returnCode == 0 && handWritten.out.contains("kept .claude/skills/prepush/SKILL.md") && handWritten.out.contains("removed .claude/skills/prepush/.gitignore")
&& !fs::exists(tracked / ".claude" / "skills" / "prepush" / ".gitignore") && !stillIgnored && !handWritten.out.contains("git rm --cached"),
std::format("exits {} {} {}, ignored {}; {}{}{}{}", trackedInit.returnCode, trackedAgain.returnCode, handWritten.returnCode, stillIgnored, trackedInit.out, trackedAgain.out, handWritten.out,
handWritten.err));
Write(r / ".claude" / "skills" / "prepush" / "SKILL.md", "my own skill\n");
const Process::Captured kept = Self({"init"}, r);
const std::string mine = Read(r / ".claude" / "skills" / "prepush" / "SKILL.md");
const bool keptIgnoreFile = fs::exists(r / ".claude" / "skills" / "prepush" / ".gitignore");
const Process::Captured forced = Self({"init", "--force"}, r);
Check("init: a SKILL.md init did not write is kept, and the .gitignore init wrote beside it goes; --force replaces the skill and writes the .gitignore again",
kept.returnCode == 0 && mine == "my own skill\n" && kept.out.contains("--force replaces it") && !keptIgnoreFile && forced.returnCode == 0
&& Read(r / ".claude" / "skills" / "prepush" / "SKILL.md") == skill && Read(r / ".claude" / "skills" / "prepush" / ".gitignore") == skillIgnore,
kept.out + forced.out);
// A .gitignore beside the skill that init did not write is kept;
// --force replaces it.
Write(r / ".claude" / "skills" / "prepush" / ".gitignore", "my own rules\n");
const Process::Captured foreignIgnore = Self({"init"}, r);
const std::string ownRules = Read(r / ".claude" / "skills" / "prepush" / ".gitignore");
const Process::Captured forcedIgnore = Self({"init", "--force"}, r);
Check("init: a .gitignore beside the skill that init did not write is kept, saying so; --force replaces it",
foreignIgnore.returnCode == 0 && ownRules == "my own rules\n" && foreignIgnore.out.contains("kept .claude/skills/prepush/.gitignore (init did not write it: --force replaces it)")
&& forcedIgnore.returnCode == 0 && Read(r / ".claude" / "skills" / "prepush" / ".gitignore") == skillIgnore,
foreignIgnore.out + forcedIgnore.out);
// The old setup: the skill dir a link into the submodule.
const fs::path old = repo("old");
Write(old / "review-gates" / "process" / "skill" / "prepush" / "SKILL.md", "the submodule's file\n");
fs::create_directories(old / ".claude" / "skills");
fs::create_directory_symlink("../../review-gates/process/skill/prepush", old / ".claude" / "skills" / "prepush");
const Process::Captured relinked = Self({"init"}, old);
std::error_code error;
Check("init: the old link into the submodule goes - a real directory with the skill in its place, the submodule's file untouched",
relinked.returnCode == 0 && !fs::is_symlink(fs::symlink_status(old / ".claude" / "skills" / "prepush", error))
&& Read(old / ".claude" / "skills" / "prepush" / "SKILL.md") == skill && Read(old / "review-gates" / "process" / "skill" / "prepush" / "SKILL.md") == "the submodule's file\n"
&& relinked.out.contains("removed the link .claude/skills/prepush -> ../../review-gates/process/skill/prepush"),
relinked.out + relinked.err);
// What init refuses, before it writes anything.
const fs::path foreign = repo("foreign");
fs::create_directories(base / "elsewhere");
fs::create_directories(foreign / ".claude" / "skills");
fs::create_directory_symlink("../../../elsewhere", foreign / ".claude" / "skills" / "prepush");
const Process::Captured refused = Self({"init"}, foreign);
fs::create_directories(r / "sub");
const Process::Captured sub = Self({"init"}, r / "sub");
const fs::path home = repo("home");
const Process::Captured atHome = Self({"init"}, home, {{"HOME", home.string()}});
const bool homeUntouched = !fs::exists(home / ".review-gates");
const Process::Captured atHomeForced = Self({"init", "--force"}, home, {{"HOME", home.string()}});
fs::create_directories(base / "plain");
const Process::Captured plain = Self({"init"}, base / "plain");
Check("init refuses, exit 3, writing nothing: a link that is not into the submodule (--force replaces it), a subdirectory, the home directory (--force goes on), no checkout",
refused.returnCode == 3 && refused.err.contains(".claude/skills/prepush is a link to ../../../elsewhere") && !fs::exists(foreign / ".review-gates") && !fs::exists(foreign / ".gitignore")
&& sub.returnCode == 3 && sub.err.contains("from the repository's toplevel") && !fs::exists(r / "sub" / ".review-gates") && atHome.returnCode == 3
&& atHome.err.contains("is your home directory") && homeUntouched && atHomeForced.returnCode == 0 && plain.returnCode == 3
&& plain.err.contains("is not in a git checkout"),
refused.err + sub.err + atHome.err + plain.err);
// A .gitignore beside the skill that is a link - to a file outside
// the skill's directory, or dangling - is a link like the skill's
// others: refused before anything is written, never written through;
// --force replaces the link with init's file and leaves its target as
// it was.
const fs::path linked = repo("linked");
Write(linked / "outside.ignore", "keep me\n");
fs::create_directories(linked / ".claude" / "skills" / "prepush");
fs::create_symlink("../../../outside.ignore", linked / ".claude" / "skills" / "prepush" / ".gitignore");
const Process::Captured linkRefused = Self({"init"}, linked);
const bool linkUntouched = !fs::exists(linked / ".review-gates") && Read(linked / "outside.ignore") == "keep me\n";
const Process::Captured linkForced = Self({"init", "--force"}, linked);
std::error_code linkError;
const bool replaced = !fs::is_symlink(fs::symlink_status(linked / ".claude" / "skills" / "prepush" / ".gitignore", linkError))
&& Read(linked / ".claude" / "skills" / "prepush" / ".gitignore") == skillIgnore && Read(linked / "outside.ignore") == "keep me\n";
const fs::path dangling = repo("dangling");
fs::create_directories(dangling / "shared");
fs::create_directories(dangling / ".claude" / "skills" / "prepush");
fs::create_symlink("../../../shared/skill.ignore", dangling / ".claude" / "skills" / "prepush" / ".gitignore");
const Process::Captured danglingRefused = Self({"init"}, dangling);
Check("init: a .gitignore beside the skill that is a link is refused before anything is written and never written through, dangling or not; --force replaces the link with init's file, "
"its target untouched",
linkRefused.returnCode == 3 && linkRefused.err.contains(".claude/skills/prepush/.gitignore is a link to ../../../outside.ignore") && linkUntouched && linkForced.returnCode == 0
&& replaced && danglingRefused.returnCode == 3 && !fs::exists(dangling / "shared" / "skill.ignore") && !fs::exists(dangling / ".review-gates"),
linkRefused.err + linkForced.out + linkForced.err + danglingRefused.err);
// A .gitignore link whose target names review-gates is the
// repository's too - the old setup linked the skill's directory, never
// its .gitignore - so it is refused like any other. A link init cannot
// remove (its directory not writable, where permissions bind) refuses
// under --force before anything is written, nothing written through it.
const fs::path named = repo("named");
Write(named / ".review-gates" / "skill.gitignore", "mine\n");
fs::create_directories(named / ".claude" / "skills" / "prepush");
fs::create_symlink("../../../.review-gates/skill.gitignore", named / ".claude" / "skills" / "prepush" / ".gitignore");
const Process::Captured namedRefused = Self({"init"}, named);
// A SKILL.md link into the pipeline's own .review-gates/ names the
// submodule but does not resolve into it: refused and kept too.
const fs::path namedSkill = repo("named-skill");
Write(namedSkill / ".review-gates" / "skill.md", "mine\n");
fs::create_directories(namedSkill / ".claude" / "skills" / "prepush");
fs::create_symlink("../../../.review-gates/skill.md", namedSkill / ".claude" / "skills" / "prepush" / "SKILL.md");
const Process::Captured namedSkillRefused = Self({"init"}, namedSkill);
const bool namedSkillKept = fs::is_symlink(fs::symlink_status(namedSkill / ".claude" / "skills" / "prepush" / "SKILL.md", linkError))
&& Read(namedSkill / ".review-gates" / "skill.md") == "mine\n" && !fs::exists(namedSkill / ".review-gates" / "config.json");
const bool namedKept = fs::is_symlink(fs::symlink_status(named / ".claude" / "skills" / "prepush" / ".gitignore", linkError))
&& Read(named / ".review-gates" / "skill.gitignore") == "mine\n" && !fs::exists(named / ".review-gates" / "config.json");
const fs::path stuck = repo("stuck");
Write(stuck / "outside.ignore", "keep me\n");
fs::create_directories(stuck / ".claude" / "skills" / "prepush");
fs::create_symlink("../../../outside.ignore", stuck / ".claude" / "skills" / "prepush" / ".gitignore");
fs::permissions(stuck / ".claude" / "skills" / "prepush", fs::perms::owner_read | fs::perms::owner_exec);
const bool writable = static_cast<bool>(std::ofstream(stuck / ".claude" / "skills" / "prepush" / ".probe"));
fs::remove(stuck / ".claude" / "skills" / "prepush" / ".probe", linkError);
const Process::Captured stuckForced = Self({"init", "--force"}, stuck);
fs::permissions(stuck / ".claude" / "skills" / "prepush", fs::perms::owner_all);
Check("init: a .gitignore or SKILL.md link into a review-gates path that is not the submodule is refused and kept; a link init cannot remove refuses under --force, writing nothing (where "
"permissions bind)",
namedRefused.returnCode == 3 && namedRefused.err.contains(".claude/skills/prepush/.gitignore is a link to ../../../.review-gates/skill.gitignore") && namedKept
&& namedSkillRefused.returnCode == 3 && namedSkillRefused.err.contains(".claude/skills/prepush/SKILL.md is a link to ../../../.review-gates/skill.md") && namedSkillKept
&& (writable
|| (stuckForced.returnCode == 3 && stuckForced.err.contains("cannot remove the link .claude/skills/prepush/.gitignore") && Read(stuck / "outside.ignore") == "keep me\n"
&& !fs::exists(stuck / ".review-gates"))),
namedRefused.err + namedSkillRefused.err + stuckForced.out + stuckForced.err);
// doc: the texts, and where they are.
const Process::Captured prepush = Self({"doc", "prepush"}, base);
const Process::Captured where = Self({"doc", "--path", "config"}, base);
const Process::Captured missing = Self({"doc"}, base);
const Process::Captured wrong = Self({"doc", "nope"}, base);
Check("doc: the procedure byte for byte, a template's path, usage errors for no name and an unknown one (exit 3)",
prepush.returnCode == 0 && prepush.out == Read(checkout / "process" / "prepush.md") && where.returnCode == 0
&& where.out == std::format("{}\n", (checkout / "process" / "templates" / "config.json").string()) && missing.returnCode == 3
&& missing.err.contains("the following arguments are required: name") && wrong.returnCode == 3
&& wrong.err.contains("argument name: invalid choice: 'nope' (choose from 'prepush', 'manifest', 'config')"),
prepush.err + where.out + missing.err + wrong.err);
// stop: a live driver is this process; one that is gone, a sleep that
// was reaped.
const std::string me = Platform::Self()->Key();
const fs::path live = base / "scratch" / "abcd1234-run-1";
Write(live / ".driver", me);
const std::int64_t gone = Process::Start({.argv = {"true"}}).pid;
Process::Wait(gone);
const fs::path dead = base / "scratch" / "abcd1234-run-2";
Write(dead / ".driver", std::format("{}:{}", gone, 1));
const Process::Captured byDir = Self({"stop", live.string()}, base);
const bool wroteDir = fs::exists(live / "stop");
fs::remove(live / "stop");
const Process::Captured byDead = Self({"stop", dead.string()}, base);
const Process::Captured byNone = Self({"stop", (base / "plain").string()}, base);
const Process::Captured neither = Self({"stop"}, base);
Check("stop <dir>: the stop file for the live driver its .driver names (exit 0); a driver that is gone, a dir no run holds, no target: nothing written",
byDir.returnCode == 0 && wroteDir && byDir.out.contains(std::format("stop: driver {}", me)) && byDead.returnCode == 1 && byDead.err.contains("is no longer running")
&& !fs::exists(dead / "stop") && byNone.returnCode == 1 && byNone.err.contains("no .driver marker") && neither.returnCode == 3,
byDir.out + byDir.err + byDead.err + byNone.err);
Json drivers = Json::object();
drivers[me] = {{"run", "commits x @ abcd1234"}, {"scratch", live.string()}, {"since", Platform::Now()}, {"boot", Platform::BootId()}};
Write(base / "board.json", DumpFile({{"version", 1}, {"drivers", drivers}, {"seats", Json::object()}}));
const std::string boardBefore = Read(base / "board.json");
const Process::Captured byHash = Self({"stop", "--hash", "abcd1234ffff"}, base);
const bool wroteHash = fs::exists(live / "stop");
const Process::Captured otherHash = Self({"stop", "--hash", "00000000"}, base);
Check("stop --hash: every live run of the hash on the board gets its stop file (the first 8 characters decide); another hash finds none (exit 1); the board is read, never written",
byHash.returnCode == 0 && wroteHash && otherHash.returnCode == 1 && otherHash.err.contains("no live run of 00000000 on the board") && Read(base / "board.json") == boardBefore,
byHash.out + byHash.err + otherHash.err);
// Only `run` and `seats` read the seats file, and its lines reach the
// placement readers alone (rounds 8 and 11 of the self-review): a
// `stop` with the board path only in the working directory's .env
// looks at the machine's default board, as every driver does - under
// a HOME of the test's own here.
std::map<std::string, std::string> envNoPaths = Env;
envNoPaths.erase("REVIEW_GATES_BOARD");
envNoPaths.erase("REVIEW_GATES_SCRATCH");
envNoPaths["HOME"] = (base / "home").string();
const fs::path fileRepo = base / "file-repo";
fs::create_directories(fileRepo);
Write(fileRepo / ".env", std::format("REVIEW_GATES_BOARD={}\nREVIEW_GATES_SCRATCH={}\n", (base / "file-board.json").string(), (base / "file-scratch").string()));
const Process::Captured fromFile = Process::Run({Platform::SelfExe().string(), "stop", "--hash", "00000000"}, fileRepo, std::chrono::seconds(60), {}, envNoPaths);
Check("no line of the seats file reaches `stop`: with the board's and the scratch root's paths only in the working directory's .env it reads the machine's default board", fromFile.returnCode == 1
&& fromFile.err.contains(std::format("on the board ({})", (base / "home" / ".cache" / "review-gates" / "seat-board.json").string())) && !fromFile.err.contains("file-board"),
fromFile.err);
// seats on the stand-in: a token from the env file, an API key, the
// CLI's own login.
const fs::path fake = base / "claude";
Write(fake, FakeClaude);
fs::permissions(fake, fs::perms::owner_all);
Write(base / "seats" / ".env", "CLAUDE_CODE_OAUTH_TOKEN1=\"tok-one\"\nANTHROPIC_API_KEY1=sk-ant-api-one\n");
fs::remove(base / "board.json");
const std::map<std::string, std::string> withFake = {{"CLAUDE_BIN", fake.string()}};
const Process::Captured report = Self({"seats", "--local", "--env-file", ".env"}, base / "seats", withFake);
const Process::Captured json = Self({"seats", "--json", "--no-fable"}, base / "seats", withFake);
// --env (a prefix of --env-file) names the file for the tokens and
// the settings alike, with ./.env in the way (the API seat's section
// is where the order shows).
Write(base / "seats" / "other.env", "CLAUDE_CODE_OAUTH_TOKEN2=tok-two\nANTHROPIC_API_KEY2=sk-ant-api-two\nANTHROPIC_API_KEY2_GRANT_DAY=16\nREVIEW_GATES_SEAT_ORDER=oauth-first\n");
const Process::Captured other = Self({"seats", "--no-fable", "--env", "other.env"}, base / "seats", withFake);
// An application's .env (no seat line, no setting) is passed over for
// the machine's own seats file.
Write(base / "app" / ".env", "VITE_API_URL=http://localhost:54321\n");
Write(base / "app-xdg" / "crafter-review" / "env", "CLAUDE_CODE_OAUTH_TOKEN3=tok-three\n");
const Process::Captured appSeats =
Self({"seats", "--no-fable"}, base / "app", {{"CLAUDE_BIN", fake.string()}, {"XDG_CONFIG_HOME", (base / "app-xdg").string()}, {"HOST_XDG_CONFIG_HOME", (base / "app-xdg").string()}});
// A seats file that can be read only once - a FIFO, as `--env-file
// <(gpg -d seats.env.gpg)` gives one - is read once: its settings and
// its tokens come from that one read (round 10 of the self-review).
fs::create_directories(base / "fifo");
const Process::Captured fromFifo = Process::Run(
{"sh", "-c",
std::format("mkfifo seats.fifo && (printf 'CLAUDE_CODE_OAUTH_TOKEN4=tok-four\\nANTHROPIC_API_KEY4=sk-ant-api-four\\nREVIEW_GATES_SEAT_ORDER=oauth-first\\n' > seats.fifo &) && exec '{}' seats --no-fable --env-file seats.fifo",
Platform::SelfExe().string())},
base / "fifo", std::chrono::seconds(60), {}, With(Env, withFake));
Json parsed;
try {
parsed = Json::parse(json.out);
} catch (const std::exception&) {
}
Check("seats: every seat probed - the CLI's own login, the env file's token, the API key - its windows read, the API seat beside the driver's ledger, the drivers on the board; an application's .env "
"is passed over for the machine's own file; a FIFO is read once, its settings and its seats alike",
report.returnCode == 0 && report.out.starts_with("seat pool: 3 seat(s) (local, 1, api1) from .env and your own login, probed ")
&& report.out.contains("| local | 42% |") && report.out.contains("| 1 | 42% |") && report.out.contains("| api1 | ok | ok | $0.00 of $200") && report.out.contains("drivers: none running")
&& other.returnCode == 0 && other.out.starts_with("seat pool: 2 seat(s) (2, api2) from other.env, probed ") && other.out.contains("| api2 | ok | ok | $0.00 of $200") && other.out.contains("| 16 |")
&& other.out.contains("oauth seats run first (REVIEW_GATES_SEAT_ORDER=oauth-first)") && !other.out.contains("tok-two") && !other.out.contains("sk-ant-api-two")
&& appSeats.returnCode == 0 && appSeats.out.starts_with(std::format("seat pool: 1 seat(s) (3) from {}, probed ", (base / "app-xdg" / "crafter-review" / "env").string()))
&& fromFifo.returnCode == 0 && fromFifo.out.starts_with("seat pool: 2 seat(s) (4, api4) from seats.fifo, probed ")
&& fromFifo.out.contains("oauth seats run first (REVIEW_GATES_SEAT_ORDER=oauth-first)"),
report.out + report.err + other.out + other.err + appSeats.out + appSeats.err + fromFifo.out + fromFifo.err);
Check("seats --json: the probe results as JSON - the seats, the API seats, the eligible ones and the next placement; ./.env read by default; no token, no key printed",
json.returnCode == 0 && parsed.is_object() && parsed.at("seats").size() == 1 && parsed.at("seats").at(0).at("probedWith") == "haiku" && parsed.at("apiSeats").size() == 1
&& parsed.at("eligible") == Json::array({1}) && parsed.at("next").at("seat") == 1 && Dyn::Str(parsed.at("probedAt")).ends_with("Z")
&& !(report.out + json.out).contains("tok-one") && !(report.out + json.out).contains("sk-ant-api-one"),
json.out.substr(0, 600) + json.err);
// The credit error a probe reads is recorded in the ledger stamped
// when it is written, not at the start the probes share (1.1.0 round
// 3): the spent key answers two seconds after the command began.
Write(base / "spent" / ".env", "ANTHROPIC_API_KEY3=sk-ant-api-spent\n");
const double spentStart = Platform::Now();
const Process::Captured spent = Self({"seats", "--json"}, base / "spent", withFake);
Json spentParsed;
try {
spentParsed = Json::parse(spent.out);
} catch (const std::exception&) {
}
// The child's TZ is UTC (Env): the stamp is compared in UTC.
const auto utc = [](double epoch) {
return std::format("{:%Y-%m-%dT%H:%M:%S}", std::chrono::floor<std::chrono::seconds>(std::chrono::sys_seconds(std::chrono::seconds(static_cast<std::int64_t>(epoch)))));
};
Json spentEntry;
if (spentParsed.is_object() && spentParsed.contains("apiSeats") && !spentParsed.at("apiSeats").empty()) {
spentEntry = Dyn::Get(Dyn::Get(spentParsed.at("apiSeats").at(0), "ledger"), "outSince");
}
Check("seats: a credit error the probe read is recorded in the shared ledger stamped when written, after the probe, never at the probes' common start",
spent.returnCode == 0 && spentParsed.is_object() && spentParsed.at("apiSeats").at(0).at("event") == "credit-gone" && spentEntry.is_string()
&& Dyn::Slice(spentEntry.get<std::string>(), 0, 19) >= utc(spentStart + 2),
spent.out.substr(0, 800) + spent.err);
const Process::Captured none = Self({"seats"}, base / "plain", withFake);
const Process::Captured noFile = Self({"seats", "--env-file", "missing.env"}, base / "plain", withFake);
Write(base / "plain" / "bad.env", std::string("\xff\n", 2));
const Process::Captured badFile = Self({"seats", "--env-file", "bad.env"}, base / "plain", withFake);
Check("seats: no seat configured is exit 1 with what it looked for; a missing env file and one that is not UTF-8 are exit 1 with the read error",
none.returnCode == 1 && none.out.starts_with("no seat configured (CLAUDE_CODE_OAUTH_TOKEN1..6 / ANTHROPIC_API_KEY1..6; --local probes your own login)")
&& noFile.returnCode == 1 && noFile.err.contains("FileNotFoundError: [Errno 2] No such file or directory: 'missing.env'") && badFile.returnCode == 1
&& badFile.err.contains("UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"),
none.out + noFile.err + badFile.err);
// What escapes a command ends it with exit 1, never an abort; config's
// help.
const Process::Captured noHome = Self({"doc", "prepush"}, base, {{"CRAFTER_REVIEW_HOME", (base / "nowhere").string()}});
const Process::Captured configHelp = Self({"config", "-h"}, base);
Check("an error no command catches ends it with exit 1 and its message, never an abort; config -h prints its usage, exit 0",
noHome.returnCode == 1 && noHome.err.starts_with("crafter-review: ") && noHome.err.contains("holds no process/lenses") && configHelp.returnCode == 0
&& configHelp.out.starts_with("usage: crafter-review config [-h]"),
noHome.err + configHelp.out + configHelp.err);
// browser: its usage (an install fetches from npm).
const Process::Captured noAction = Self({"browser"}, base);
const Process::Captured badAction = Self({"browser", "remove"}, base);
Check("browser: an action is required, and install is the one there is (exit 3)",
noAction.returnCode == 3 && noAction.err.contains("the following arguments are required: action") && badAction.returnCode == 3
&& badAction.err.contains("argument action: invalid choice: 'remove' (choose from 'install')"),
noAction.err + badAction.err);
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, 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, "commands: {}", error.what());
return 1;
}
}