// SPDX-License-Identifier: AGPL-3.0-only // SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // The OpenCode harness, against the goldens (tests/Goldens/fixture/ // pipeline.json, under `opencode`): the tool source, the permission rules, // the agent config, the commands, the environment, the home, the event // parsing, the classification, and a warm-up against a fake opencode. The // test binary is its own supervisor (`__supervise`), as the driver is. #include #include 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(""); } std::vector Strings(const Json& array) { std::vector out; for (const Json& item : array) out.push_back(item.get()); return out; } std::vector Names(const fs::path& dir) { std::vector names; for (const fs::directory_entry& entry : fs::directory_iterator(dir)) names.push_back(entry.path().filename().string()); std::ranges::sort(names); return names; } // What the generator's temp dir is in the goldens. std::string Tidy(std::string text, const fs::path& td) { return Dyn::Replace(text, td.string(), ""); } std::int32_t Checks() { std::println("opencode harness + OpenRouter seats: loader, environments, permissions, event parsing, the tool schema"); Platform::SetMode(Platform::Mode::Native); // The directory modes below are the generator's, made under 022. umask(022); const fs::path td = fs::temp_directory_path() / std::format("crafter-review-opencode-{}", Platform::PublishedPid()); fs::remove_all(td); fs::create_directories(td); const fs::path envFile = td / ".env"; Write(envFile, "CLAUDE_CODE_OAUTH_TOKEN1=tok\nOPENROUTER_KEY=sk-or-one\n"); // The seats file as run and seats take it: one read, its tokens over // an environment (an empty one here). const std::map fileEnv = SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile(envFile.string(), td)); auto ids = [](const std::vector& seats) { std::vector out; for (const Json& s : seats) out.push_back(s.at("id").get()); return out; }; Check("OPENROUTER_KEY is a seat only on request", ids(SeatPool::LoadSeats(fileEnv, "")) == std::vector{"1"} && ids(SeatPool::LoadSeats(fileEnv, "openrouter")) == std::vector{"or1"}); try { SeatPool::LoadSeats(fileEnv, "or1,1"); Check("an OpenRouter seat never shares a pool with Anthropic seats", false); } catch (const std::invalid_argument& e) { Check("an OpenRouter seat never shares a pool with Anthropic seats", std::string_view(e.what()).contains("cannot share"), e.what()); } const std::vector allow = {"Read", "Glob", "Grep", "Bash(cat:*)", "Bash(git -C . diff:*)", "Bash(python3 -:*)"}; const Json p = OpenCode::Permissions(allow); Check("allowlist -> permission rules", p.at("read") == "allow" && p.at("StructuredOutput") == "allow" && p.at("bash").at("*") == "deny" && p.at("bash").at("git -C . diff*") == "allow"); const std::vector ev = { {{"type", "step_start"}, {"sessionID", "ses_1"}, {"part", Json::object()}}, {{"type", "tool_use"}, {"part", {{"type", "tool"}, {"tool", "StructuredOutput"}, {"state", {{"status", "completed"}, {"input", {{"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, {"coverage", {{{"item", "u"}, {"verdict", "clear"}, {"basis", "b"}}}}}}}}}}}, {{"type", "step_finish"}, {"part", {{"type", "step-finish"}, {"reason", "stop"}, {"tokens", {{"input", 20}, {"output", 4}, {"reasoning", 1}, {"cache", {{"read", 150}, {"write", 0}}}}}, {"cost", 0.0005}}}}, }; std::vector lines; for (const Json& e : ev) lines.push_back(DumpSpaced(e)); const Json ps = OpenCode::ParseEvents(Dyn::Join(lines, "\n")); Check("events -> the structured output, usage, classify ok", ps.at("structured").at("coverage").at(0).at("item") == "u" && ps.at("usage").at("output") == 5 && OpenCode::Classify(ps, 0, "") == std::pair{"ok", ""}); const std::string src = OpenCode::ToolSource(); Check("the tool schema carries the note kinds, the coverage vocabulary and the verdicts of the schema; no persona field", std::ranges::all_of(std::array{"reserve-answer", "unverified", "FIX FIRST", "refutation-verified", "n/a"}, [&](std::string_view k) { return src.contains(DumpSpaced(Json(std::string(k)))); }) && !src.contains("notMine") && !src.contains("wouldComment")); const fs::path repo = td / "repo"; fs::create_directories(repo / "src"); fs::create_directories(repo / ".git"); fs::create_directories(repo / "node_modules"); Write(repo / "package.json", "{}"); const std::vector one = {"openrouter/x/y"}; const fs::path home = OpenCode::PrepareHome(td / "sc", repo, one); Check("prepare_home: the view links every top-level entry but .git and node_modules", Names(home / "view") == std::vector{"package.json", "src"} && fs::is_symlink(fs::symlink_status(home / "view" / "src"))); // ---- against the goldens --------------------------------------------------------------- const Json g = Dyn::LoadJsonFile(fs::current_path() / "tests" / "Goldens" / "fixture" / "pipeline.json").at("opencode"); Check("goldens: the tool source, byte for byte", src == g.at("toolSource").get()); Check("goldens: the provider routing", DumpFile(OpenCode::Routing()) == DumpFile(g.at("routing"))); std::vector missed; for (const Json& entry : g.at("modelId")) { const auto [prov, id] = OpenCode::ModelId(entry.at(0).get()); if (Json::array({prov, id}) != entry.at(1)) missed.push_back(entry.at(0).get()); } Check("goldens: model ids", missed.empty(), Dyn::Join(missed, ", ")); missed.clear(); std::size_t index = 0; for (const Json& entry : g.at("permissions")) { const std::vector list = Strings(entry.at(0)); if (DumpFile(OpenCode::Permissions(list)) != DumpFile(entry.at(1))) missed.push_back(DumpCompact(entry.at(0))); const std::map child = OpenCode::ChildEnv({{"PATH", "/bin"}}, "/abs/system é.md", "/abs/so.json", list, td / "data" / std::to_string(index)); if (child.at("OPENCODE_CONFIG_CONTENT") != g.at("agentConfig").at(index).at(1).get()) missed.push_back(std::format("agent config {}", std::to_string(index))); if (child.at("REVIEW_GATES_STRUCTURED_OUT") != "/abs/so.json" || child.at("XDG_DATA_HOME") != (td / "data" / std::to_string(index)).string()) missed.push_back(std::format("child env {}", std::to_string(index))); ++index; } Check("goldens: permission rules and the inline agent config, key for key", missed.empty(), Dyn::Join(missed, "; ")); missed.clear(); for (const Json& entry : g.at("cmd")) { const Json& a = entry.at(0); const std::vector got = OpenCode::Cmd(a.at(0).get(), a.at(1).get(), a.at(2).get(), a.at(3).get(), a.at(4).is_null() ? std::string() : a.at(4).get(), a.at(5).get()); if (got != Strings(entry.at(1))) missed.push_back(DumpCompact(a)); } Check("goldens: the opencode command lines", missed.empty(), Dyn::Join(missed, "; ")); std::map baseIn; for (auto it = g.at("baseEnv").at(0).begin(); it != g.at("baseEnv").at(0).end(); ++it) baseIn[it.key()] = it.value().get(); std::map baseWant; for (auto it = g.at("baseEnv").at(1).begin(); it != g.at("baseEnv").at(1).end(); ++it) baseWant[it.key()] = it.value().get(); Check("goldens: the shared environment - its own XDG home and key, no Anthropic or OpenCode variable", OpenCode::BaseEnv("/h", "sk-or-key", baseIn) == baseWant); // The generator's temp dir, which the goldens call . const fs::path gd = td / "g"; const fs::path repo2 = gd / "repo"; for (const std::string_view d : {".git", "node_modules", "src", "b", "a"}) fs::create_directories(repo2 / d); for (const std::string_view name : {"package.json", ".env", "Z"}) Write(repo2 / name, "{}"); const Json& h = g.at("home"); const fs::path home2 = OpenCode::PrepareHome(gd / "sc", repo2, Strings(h.at("models"))); const fs::path config = home2 / "xdg" / "opencode" / "opencode.json"; const std::string firstConfig = Read(config); OpenCode::PrepareHome(gd / "sc", repo2, one); Check("goldens: prepare_home's config, byte for byte, and rewritten by the next call", firstConfig == h.at("firstConfig").get() && Read(config) == h.at("config").get()); std::vector view; for (const std::string& name : Names(home2 / "view")) view.push_back(std::format("{} -> {}", name, Tidy(fs::read_symlink(home2 / "view" / name).string(), gd))); std::vector viewWant; for (const Json& entry : h.at("view")) viewWant.push_back(std::format("{} -> {}", entry.at(0).get(), entry.at(1).get())); Check("goldens: the view's links, sorted, dot files included, .git and node_modules left out; idempotent", view == viewWant && Tidy(home2.string(), gd) == h.at("home").get(), Dyn::Join(view, "; ")); missed.clear(); for (auto it = h.at("modes").begin(); it != h.at("modes").end(); ++it) { const auto mode = static_cast(fs::status(home2 / it.key()).permissions()) & 0777; if (std::format("0o{:o}", mode) != it.value().get()) missed.push_back(std::format("{} 0o{:o}", it.key(), mode)); } Check("goldens: os.makedirs' modes - the last directory 0700, its parents as mkdir makes them", missed.empty() && Read(home2 / "xdg" / "opencode" / "tools" / "StructuredOutput.ts") == src, Dyn::Join(missed, ", ")); missed.clear(); for (const Json& entry : g.at("events")) { if (DumpFile(OpenCode::ParseEvents(entry.at(0).get())) != DumpFile(entry.at(1))) missed.push_back(Dyn::Repr(Dyn::Slice(entry.at(0).get(), 0, 60))); } Check("goldens: the event stream - steps, tokens, tool calls and errors, the session, the first turn", missed.empty(), Dyn::Join(missed, "; ")); missed.clear(); for (const Json& entry : g.at("classify")) { const Json& c = entry.at(0); const auto [status, reason] = OpenCode::Classify(c.at(0), c.at(1).get(), c.at(2).get(), c.at(3).get()); if (Json::array({status, reason}) != entry.at(1)) missed.push_back(std::format("{} -> {} {}", Dyn::Repr(Dyn::Slice(c.at(2).get(), 0, 40)), status, Dyn::Repr(Dyn::Slice(reason, 0, 60)))); } Check("goldens: classify - the time budget, then credits, auth and limits as the regexes read them (re.I, \\b), then why nothing came back", missed.empty(), Dyn::Join(missed, "; ")); const Json& w = g.at("warm"); const fs::path fake = td / "opencode"; Write(fake, g.at("fake").get()); fs::permissions(fake, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec); const std::map env = OpenCode::BaseEnv(home2, "sk-or", std::map{{"PATH", "/usr/bin:/bin"}}); std::vector log; const auto warmed = OpenCode::Warm(fake.string(), home2, env, "openrouter/x/y", [&](const std::string& line) { log.push_back(line); }); const std::string args = Tidy(Read(home2 / "data" / "warm" / "args"), gd); Check("goldens: a warm-up that fails - its log line, the tail of its stderr, the command it ran", warmed == std::pair{w.at("result").at(0).get(), w.at("result").at(1).get()} && log == Strings(w.at("log")) && args == w.at("args").get(), std::format("{} | {}", Dyn::Join(log, " | "), Dyn::Repr(args))); const fs::path slow = td / "slow-opencode"; Write(slow, "#!/bin/sh\necho $$ > \"$XDG_DATA_HOME/pid\"\nexec sleep 30\n"); fs::permissions(slow, fs::perms::owner_all); log.clear(); const auto timed = OpenCode::Warm(slow.string(), home2, env, "openrouter/x/y", [&](const std::string& line) { log.push_back(line); }, 1); const std::optional sleeper = Dyn::ParseInt(Dyn::Strip(Read(home2 / "data" / "warm" / "pid"))); Check("goldens: a warm-up out of time says so and logs nothing", timed == std::pair{w.at("timedOut").at(0).get(), w.at("timedOut").at(1).get()} && log.empty() && w.at("timedLog").empty()); Check("...and leaves no process behind: the supervisor takes it down", sleeper && !Platform::ProcStart(*sleeper)); // ---- beyond the goldens ----------------------------------------------------------------- const fs::path so = td / "so.json"; Write(so, R"({"verdict": "SEND-READY", "findings": [], "notes": [], "coverage": []})"); const fs::path bad = td / "bad.json"; Write(bad, R"({"verdict": "SEND-READY", "findings": "none"})"); const fs::path garbage = td / "garbage.json"; Write(garbage, "{"); const std::string noTool = DumpSpaced({{"type", "step_finish"}, {"part", {{"reason", "stop"}}}}); Check("the structured-output file stands in when the stream carries no call: one with a findings list, nothing else", OpenCode::ParseEvents(noTool, so).at("structured").at("verdict") == "SEND-READY" && OpenCode::ParseEvents(noTool, bad).at("structured").is_null() && OpenCode::ParseEvents(noTool, garbage).at("structured").is_null() && OpenCode::ParseEvents(noTool, td / "missing.json").at("structured").is_null() && OpenCode::ParseEvents(noTool).at("structured").is_null()); fs::remove_all(td); return Failures == 0 ? 0 : 1; } } int main(int argc, char** argv) { const std::vector args(argv, argv + argc); if (args.size() > 1 && args[1] == Process::SuperviseVerb) return Process::SuperviseMain(std::span(args).subspan(2)); try { return Checks(); } catch (const std::exception& error) { std::println(std::cerr, "opencode: {}", error.what()); return 1; } }