Crafter.Review/tests/Commands/main.cpp
Jorijn van der Graaf 101471d81a
feat(config): every lens off until the repository turns it on
Jorijn: "all lenses off by default and opt in only". Config::Defaults()
gives every lens run: false; a config that turns on none - a missing
`lenses` key included - has the one problem that names the lenses, so
config, freeze and run all stop on it; the template ships every lens off,
init says to turn lenses on, and the "no lens runs" preflight names the
cause. A consumer config without a `lenses` key now needs one, as the CI
smoke test's does.

Proven with the lens commit before it: the suite (26 passed), and the
dry-run comparison recorded there.
2026-10-31 20:09:07 +01:00

260 lines
18 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
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;
}
Env = With(Env, {{"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";
// 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::size_t frontmatter = shipped.find("\n---\n", 3) + 5;
Check("init: the config from the template (every lens off), the artifact and worktree dirs ignored, the skill with init's marker after its frontmatter, the next steps (turn lenses on; no browser line while none drives the application)",
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)) && first.out.contains(" config - until it reports no problem")
&& first.out.contains("turn lenses on in .review-gates/config.json") && !first.out.contains(" browser install - "),
first.out + first.err);
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",
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 + again.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 Process::Captured forced = Self({"init", "--force"}, r);
Check("init: a SKILL.md init did not write is kept; --force replaces it",
kept.returnCode == 0 && mine == "my own skill\n" && kept.out.contains("--force replaces it") && forced.returnCode == 0 && Read(r / ".claude" / "skills" / "prepush" / "SKILL.md") == skill,
kept.out + forced.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);
// 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);
// 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);
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",
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 | $0.00 of $200") && report.out.contains("drivers: none running"),
report.out + report.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);
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;
}
}