The 5-hour share of a running pass was a flat 0.25 points; it follows the run's effort now, as the weekly share already did: 0.5 at max and any other effort, 0.125 at xhigh - the pooled means of 5438 Opus 5.5 passes on 141 seat-runs (2026-10-12..15), rounded up. The weekly cutoff moves from 95 % to 99 %: the weekly window is the budget the lenses run out of, and with the running passes projected against the cutoff the last percent is spent a pass at a time, not in a burst. REVIEW_GATES_LENS_COST_PCT and REVIEW_GATES_LENS_COST_7D_PCT still override every effort.
259 lines
19 KiB
C++
259 lines
19 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// Seat usage: the pure functions against reference vectors, numbered in
|
|
// order, so "check 17" always names the same vector.
|
|
|
|
import std;
|
|
import Crafter.Review;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter::Review;
|
|
using namespace Crafter::Review::Seats;
|
|
|
|
namespace {
|
|
std::int32_t Failures = 0;
|
|
std::int32_t N = 0;
|
|
|
|
void Eq(const std::string& got, const std::string& want) {
|
|
++N;
|
|
if (got != want) {
|
|
std::println(std::cerr, "check {}:\n got: {}\nwant: {}", N, Dyn::Repr(got), Dyn::Repr(want));
|
|
++Failures;
|
|
}
|
|
}
|
|
|
|
void Eq(bool got, bool want) {
|
|
Eq(std::string(got ? "True" : "False"), std::string(want ? "True" : "False"));
|
|
}
|
|
|
|
void EqJson(const Json& got, const Json& want) {
|
|
++N;
|
|
if (!Dyn::Equal(got, want)) {
|
|
std::println(std::cerr, "check {}:\n got: {}\nwant: {}", N, DumpCompactSorted(got), DumpCompactSorted(want));
|
|
++Failures;
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> Lines(const std::string& text) {
|
|
return Dyn::Split(text, "\n");
|
|
}
|
|
|
|
Json With(Json value, const Json& changes) {
|
|
for (auto it = changes.begin(); it != changes.end(); ++it) value[it.key()] = it.value();
|
|
return value;
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
const Zone utc = ZoneNamed("UTC");
|
|
const Zone ams = ZoneNamed("Europe/Amsterdam");
|
|
// 2026-09-07 10:00 UTC
|
|
constexpr double now = 1788775200.0;
|
|
const auto nowS = static_cast<std::int64_t>(now);
|
|
const Json cfg = DefaultConfig();
|
|
auto seat = [&](std::int32_t k, double five, double seven = 0, const Json& over = Json::object()) {
|
|
Json windows = {{"five_hour", {{"utilization", five}, {"resetsAt", nowS + 300 * k}}}, {"seven_day", {{"utilization", seven}, {"resetsAt", nowS + 3 * 86400}}}};
|
|
// An integer stays an integer.
|
|
for (auto& w : windows) {
|
|
const double u = w.at("utilization").get<double>();
|
|
if (u == std::floor(u)) w["utilization"] = static_cast<std::int64_t>(u);
|
|
}
|
|
const auto [util, binding] = SeatUtil(windows);
|
|
Json s = {{"seat", k}, {"state", "ok"}, {"windows", windows}, {"util", util}, {"binding", binding}, {"resetsAt", windows.at("five_hour").at("resetsAt")}};
|
|
return With(s, over);
|
|
};
|
|
const std::string fable(FableWindow);
|
|
|
|
// format_when: the clock in the report zone and a countdown; a reset over
|
|
// a day out carries its date.
|
|
Eq(FormatWhen(nowS + 45 * 60, now, utc), "10:45 (in 45 min)");
|
|
Eq(FormatWhen(nowS + 3 * 3600 + 5 * 60, now, ams), "15:05 (in 3h 05m)");
|
|
Eq(FormatWhen(nowS + 3 * 86400, now, utc), "Thu 10 Sept, 10:00 (in 3d 0h)");
|
|
Eq(FormatWhen(nowS - 60, now, utc), "09:59 (now)");
|
|
// The report: a row per seat, columns padded to line up; who is eligible
|
|
// and where the next lens lands.
|
|
const Json lim = seat(1, 116, 21, {{"state", "limited"}, {"util", 115}, {"error", "five_hour rejected, overage out_of_credits"}});
|
|
Json three = seat(3, 62, 16);
|
|
three["windows"][fable] = {{"utilization", 22}, {"resetsAt", 0}};
|
|
std::vector<std::string> lines = Lines(RenderStatusReport({lim, seat(2, 61, 18), three}, cfg, now, utc));
|
|
Eq(lines.at(0), "| seat | 5h | resets | 7d | resets | fable 7d |");
|
|
Eq(lines.at(1), "|------|------|-------------------|-----|-------------------------------|----------|");
|
|
Eq(lines.at(2), "| 1 | 116% | 10:05 (in 5 min) | 21% | Thu 10 Sept, 10:00 (in 3d 0h) | — |");
|
|
Eq(lines.at(3), "| 2 | 61% | 10:10 (in 10 min) | 18% | Thu 10 Sept, 10:00 (in 3d 0h) | — |");
|
|
Eq(lines.at(4), "| 3 | 62% | 10:15 (in 15 min) | 16% | Thu 10 Sept, 10:00 (in 3d 0h) | 22% |");
|
|
Eq(lines.at(5), "");
|
|
Eq(lines.at(6), "policy spread · ceiling 85% · handover at 90% · a lens is placed at +10% · 7d cutoff 99%");
|
|
Eq(lines.at(7), "eligible now: seat 2 (25% headroom), seat 3 (25% headroom) → next lens: seat 2");
|
|
Eq(lines.at(8), "fable lenses: seat 3 (63% fable headroom)");
|
|
Eq(lines.at(9), "limited: seat 1 until 10:05 (in 5 min)");
|
|
// A seat the probe could not read: an empty row, the reason under the table.
|
|
const std::string dead = RenderStatusReport({seat(2, 10), {{"seat", 4}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout after 20000 ms"}}}, cfg, now, utc);
|
|
Eq(dead.contains("| 4 | — | — | — | — | — |"), true);
|
|
// The board: the passes every driver on this machine has in flight on an
|
|
// account, a column only when there are any.
|
|
const std::vector<std::string> held = Lines(RenderStatusReport({seat(2, 61, 18), three}, cfg, now, utc, {{"seats", {{"k3", {{"inflight", 4}}}}}}, {{3, "k3"}}));
|
|
Eq(held.at(0), "| seat | 5h | resets | 7d | resets | fable 7d | in flight |");
|
|
Eq(held.at(3), "| 3 | 62% | 10:15 (in 15 min) | 16% | Thu 10 Sept, 10:00 (in 3d 0h) | 22% | 4 |");
|
|
Eq(dead.contains("not usable: seat 4 unknown (probe timeout after 20000 ms)"), true);
|
|
// A Fable probe refused for the Fable window needs the Haiku stand-in,
|
|
// which it then lands on: its window, and 'limited' for that window alone.
|
|
Json fableWindows = seat(2, 10).at("windows");
|
|
fableWindows[fable] = {{"utilization", 100}, {"resetsAt", nowS + 86400}};
|
|
const Json fableLimited = With(seat(2, 10), {{"state", "limited"}, {"util", 100}, {"binding", fable}, {"error", std::format("{} rejected", fable)}, {"windows", fableWindows}});
|
|
Eq(NeedsHaiku(fableLimited), true);
|
|
const Json merged = MergeFableProbe(seat(2, 10), fableLimited);
|
|
EqJson(merged.at("fable"), {{"state", "limited"}});
|
|
EqJson(merged.at("windows").at(fable), {{"utilization", 100}, {"resetsAt", nowS + 86400}});
|
|
std::string r = RenderStatusReport({merged}, cfg, now, utc);
|
|
Eq(Lines(r).at(2).contains("| limited (100%) |"), true);
|
|
Eq(r.contains("fable lenses: no seat with room on its Fable window — Fable lenses run on their fallback model"), true);
|
|
Eq(r.contains("eligible now: seat 2 (75% headroom)"), true);
|
|
// A readable Fable probe is the seat itself.
|
|
Json fullWindows = seat(3, 20, 30).at("windows");
|
|
fullWindows[fable] = {{"utilization", 44}, {"resetsAt", nowS + 86400}};
|
|
const Json full = With(seat(3, 20, 30), {{"windows", fullWindows}});
|
|
Eq(NeedsHaiku(full), false);
|
|
EqJson(MergeFableProbe(full, full).at("fable"), {{"state", "ok"}});
|
|
// A drained Fable window makes an Opus seat, never an idle one.
|
|
Json drainedWindows = seat(4, 10, 20).at("windows");
|
|
drainedWindows[fable] = {{"utilization", 98}, {"resetsAt", nowS + 86400}};
|
|
Json drained = With(seat(4, 10, 20), {{"util", 100}, {"binding", fable}, {"windows", drainedWindows}});
|
|
drained = MergeFableProbe(drained, drained);
|
|
EqJson(Json::array({drained.at("util"), drained.at("binding"), drained.at("fable")}), Json::array({20, "seven_day", {{"state", "ok"}}}));
|
|
r = RenderStatusReport({drained, full}, cfg, now, utc);
|
|
Eq(r.contains("eligible now: seat 4 (65% headroom), seat 3 (55% headroom) → next lens: seat 4"), true);
|
|
Eq(r.contains("fable lenses: seat 3 (41% fable headroom)"), true);
|
|
// Reported under the CLI's other name, it lands under the canonical key.
|
|
Json altWindows = seat(5, 10).at("windows");
|
|
altWindows["seven_day_fable"] = {{"utilization", 44}, {"resetsAt", nowS + 86400}};
|
|
Json alt = With(seat(5, 10), {{"windows", altWindows}});
|
|
alt = MergeFableProbe(alt, alt);
|
|
EqJson(alt.at("windows").at(fable), {{"utilization", 44}, {"resetsAt", nowS + 86400}});
|
|
Eq(alt.at("windows").contains("seven_day_fable"), false);
|
|
Eq(RenderStatusReport({alt}, cfg, now, utc).contains("fable lenses: seat 5 (41% fable headroom)"), true);
|
|
// A Fable probe refused for the 5-hour window: the seat is limited for
|
|
// everything, no stand-in, the Fable window still read.
|
|
Json fiveWindows = seat(2, 110).at("windows");
|
|
fiveWindows[fable] = {{"utilization", 40}, {"resetsAt", nowS + 86400}};
|
|
const Json fiveLimited = With(seat(2, 110), {{"state", "limited"}, {"util", 110}, {"binding", "five_hour"}, {"error", "five_hour rejected"}, {"windows", fiveWindows}});
|
|
Eq(NeedsHaiku(fiveLimited), false);
|
|
r = RenderStatusReport({MergeFableProbe(fiveLimited, fiveLimited)}, cfg, now, utc);
|
|
Eq(Lines(r).at(2).contains("| 110% | 10:10 (in 10 min) | 0%") && Lines(r).at(2).ends_with("| 40% |"), true);
|
|
Eq(r.contains("limited: seat 2 until 10:10 (in 10 min)"), true);
|
|
Eq(r.contains("fable lenses: seat 2"), false);
|
|
Eq(NeedsHaiku({{"seat", 1}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout"}}), true);
|
|
Eq(NeedsHaiku(seat(1, 10)), false);
|
|
// A Fable probe that could not be read: its state in the cell, the reason
|
|
// under the table.
|
|
const Json deadFable = MergeFableProbe(seat(2, 10), {{"seat", 2}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout after 20000 ms"}});
|
|
r = RenderStatusReport({deadFable}, cfg, now, utc);
|
|
Eq(Lines(r).at(2).contains("| unknown |"), true);
|
|
Eq(r.contains("fable probe: seat 2 unknown (probe timeout after 20000 ms)"), true);
|
|
const Json revoked = {{"seat", 3}, {"state", "invalid"}, {"windows", Json::object()}, {"error", "token revoked"}};
|
|
r = RenderStatusReport({MergeFableProbe(revoked, revoked)}, cfg, now, utc);
|
|
Eq(r.contains("not usable: seat 3 invalid (token revoked)"), true);
|
|
Eq(r.contains("fable probe:"), false);
|
|
// 2026-10-31: an account whose 5-hour limit is removed reports the weekly
|
|
// window alone - `no limit` in its 5h cell, the weekly window its burst
|
|
// window, ranked by it.
|
|
const Json nf = {{"seat", 3}, {"state", "ok"}, {"windows", {{"seven_day", {{"utilization", 3}, {"resetsAt", nowS + 6 * 86400}}}}}, {"util", 5}, {"binding", "seven_day"}, {"resetsAt", nowS + 6 * 86400}};
|
|
r = RenderStatusReport({seat(2, 61, 18), nf}, cfg, now, utc);
|
|
Eq(Lines(r).at(3), std::string("| 3 | no limit | — | 3% | Sun 13 Sept, 10:00 (in 6d 0h) | — |"));
|
|
Eq(Lines(r).at(6), std::string("eligible now: seat 3 (80% headroom), seat 2 (25% headroom) → next lens: seat 3"));
|
|
Eq(BurstWindow(nf.at("windows")) == "seven_day" && BurstWindow(seat(2, 61).at("windows")) == "five_hour" && !BurstWindow(Json::object()), true);
|
|
Eq(std::format("{}|{}|{}", WindowCell(nf.at("windows"), "five_hour"), WindowCell(Json::object(), "five_hour"), WindowCell(nf.at("windows"), "seven_day")), std::string("no limit|—|3%"));
|
|
// The local login is seat 0, named `local`.
|
|
r = RenderStatusReport({seat(0, 30), seat(2, 50)}, cfg, now, utc);
|
|
Eq(Lines(r).at(2).starts_with("| local | 30%"), true);
|
|
Eq(r.contains("eligible now: local (55% headroom), seat 2 (35% headroom) → next lens: local"), true);
|
|
// The pick: the pipeline's one-lens wave.
|
|
auto unknown = [](std::int32_t k) { return Json{{"seat", k}, {"state", "unknown"}, {"windows", Json::object()}, {"error", "probe timeout"}}; };
|
|
auto invalid = [](std::int32_t k) { return Json{{"seat", k}, {"state", "invalid"}, {"windows", Json::object()}, {"error", "authentication failed"}}; };
|
|
EqJson(PickNext({}, cfg), {{"kind", "stop"}, {"status", "error"}, {"reason", "no seat token configured"}});
|
|
EqJson(PickNext({invalid(1), invalid(2)}, cfg).at("reason"), "no valid seat token (seats 1, 2 rejected)");
|
|
EqJson(PickNext({unknown(3), unknown(1)}, cfg), {{"kind", "seat"}, {"seat", 1}, {"source", "round-robin"}});
|
|
EqJson(PickNext({seat(1, 5), seat(2, 1), seat(3, 0)}, cfg), {{"kind", "seat"}, {"seat", 2}, {"source", "probe"}});
|
|
EqJson(PickNext({seat(1, 5), seat(2, 1), seat(3, 0)}, With(cfg, {{"policy", "pack"}})).at("seat"), 1);
|
|
EqJson(PickNext({seat(1, 90, 10), seat(2, 80, 96), seat(3, 86)}, cfg).at("seat"), 3);
|
|
EqJson(PickNext({seat(1, 50), unknown(2)}, cfg).at("seat"), 1);
|
|
// Tokens: TOKEN1..6 by fixed name, the legacy name as seat 1, a dotenv
|
|
// file's token lines only.
|
|
const std::string text = Dyn::Join(std::vector<std::string>{"MAILOSAUR_API_KEY=abc def", "export CLAUDE_CODE_OAUTH_TOKEN1=\"one\"", "CLAUDE_CODE_OAUTH_TOKEN2='two'",
|
|
"CLAUDE_CODE_OAUTH_TOKEN3 = three ", "CLAUDE_CODE_OAUTH_TOKEN7=seven", "CLAUDE_CODE_OAUTH_TOKEN=legacy",
|
|
"# CLAUDE_CODE_OAUTH_TOKEN4=commented"}, "\n");
|
|
const std::map<std::string, std::string> parsed = SeatTokensFromEnvFile(text);
|
|
const std::map<std::string, std::string> wantParsed = {{"CLAUDE_CODE_OAUTH_TOKEN1", "one"}, {"CLAUDE_CODE_OAUTH_TOKEN2", "two"}, {"CLAUDE_CODE_OAUTH_TOKEN3", "three"}, {"CLAUDE_CODE_OAUTH_TOKEN", "legacy"}};
|
|
Eq(parsed == wantParsed, true);
|
|
std::vector<std::int32_t> ids;
|
|
for (const auto& [n, token] : SeatTokensFromEnv(parsed)) ids.push_back(n);
|
|
Eq(ids == std::vector<std::int32_t>{1, 2, 3}, true);
|
|
using Tokens = std::vector<std::pair<std::int32_t, std::string>>;
|
|
Eq(SeatTokensFromEnv({{"CLAUDE_CODE_OAUTH_TOKEN1", "a"}, {"CLAUDE_CODE_OAUTH_TOKEN2", " "}, {"CLAUDE_CODE_OAUTH_TOKEN3", "c"}, {"CLAUDE_CODE_OAUTH_TOKEN7", "x"},
|
|
{"CLAUDE_CODE_OAUTH_TOKENX", "y"}, {"CLAUDE_CODE_OAUTH_TOKEN", "legacy"}}) == Tokens{{1, "a"}, {3, "c"}}, true);
|
|
Eq(SeatTokensFromEnv({{"CLAUDE_CODE_OAUTH_TOKEN", "legacy"}, {"CLAUDE_CODE_OAUTH_TOKEN2", "b"}}) == Tokens{{1, "legacy"}, {2, "b"}}, true);
|
|
Eq(SeatTokensFromEnv({}).empty(), true);
|
|
// Config: repo variables override the defaults, garbage does not.
|
|
EqJson(ConfigFromEnv({}), {{"maxUtil", 85}, {"lensCostPct", 10}, {"waitMaxMin", 15}, {"policy", "spread"}, {"handoverPct", 90}});
|
|
EqJson(ConfigFromEnv({{"REVIEW_GATES_SEAT_MAX_UTIL", "70"}, {"REVIEW_GATES_LENS_COST_PCT", "abc"}, {"REVIEW_GATES_SEAT_WAIT_MIN", "0"}, {"REVIEW_GATES_SEAT_HANDOVER_PCT", "80"},
|
|
{"REVIEW_GATES_SEAT_POLICY", "pack"}}),
|
|
{{"maxUtil", 70}, {"lensCostPct", 10}, {"waitMaxMin", 0}, {"policy", "pack"}, {"handoverPct", 80}});
|
|
EqJson(ConfigFromEnv({{"REVIEW_GATES_SEAT_POLICY", "weird"}}).at("policy"), "spread");
|
|
// The stream: captured from claude 2.1.263, values shortened.
|
|
const std::string init = R"({"type":"system","subtype":"init","model":"claude-haiku-4-5-20251001","claude_code_version":"2.1.263"})";
|
|
const std::string event = R"({"type":"rate_limit_event","rate_limit_info":{"status":"allowed","resetsAt":1788789600,"rateLimitType":"five_hour",)"
|
|
R"("overageStatus":"rejected","overageDisabledReason":"out_of_credits","isUsingOverage":false,"unifiedWindows":{)"
|
|
R"("five_hour":{"utilization":0.05,"resetsAt":1788789600},"seven_day":{"utilization":0.01,"resetsAt":1789038000}}},)"
|
|
R"("uuid":"u","session_id":"400f3eb9-4732-42f1-a257-b84d1da95164"})";
|
|
const std::string assistant = R"({"type":"assistant","message":{"role":"assistant","content":[{"type":"text","text":"ok"}]}})";
|
|
const std::string result = R"({"type":"result","subtype":"success","is_error":false,"structured_output":{"ok":true},"result":"ok","modelUsage":{"claude-haiku-4-5-20251001":{"inputTokens":134}}})";
|
|
for (const std::vector<std::string>& order : {std::vector<std::string>{init, event, assistant, result}, std::vector<std::string>{init, assistant, event, result},
|
|
std::vector<std::string>{init, assistant, result, event}}) {
|
|
const Json p = ParseStream(std::format("{}\n", Dyn::Join(order, "\n")));
|
|
EqJson(p.at("result").at("structured_output"), {{"ok", true}});
|
|
EqJson(p.at("sessionId"), "400f3eb9-4732-42f1-a257-b84d1da95164");
|
|
const Json& rl = p.at("rateLimit");
|
|
EqJson(Json::array({rl.at("status"), rl.at("rateLimitType"), rl.at("resetsAt"), rl.at("overageReason")}), Json::array({"allowed", "five_hour", 1788789600, "out_of_credits"}));
|
|
EqJson(rl.at("windows").at("five_hour"), {{"utilization", 5}, {"resetsAt", 1788789600}});
|
|
EqJson(rl.at("windows").at("seven_day"), {{"utilization", 1}, {"resetsAt", 1789038000}});
|
|
}
|
|
EqJson(ParseStream(Dyn::Join(std::vector<std::string>{"garbage", event, "more garbage", result}, "\n")).at("result").at("structured_output"), {{"ok", true}});
|
|
EqJson(ParseStream("{\n \"type\": \"result\",\n \"subtype\": \"success\"\n}").at("result"), nullptr);
|
|
EqJson(ParseStream("").at("rateLimit"), nullptr);
|
|
EqJson(Json::array({Pct(1.16), Pct(0.05), Pct(0.625), Pct("x"), Pct(-1)}), Json::array({116, 5, 62.5, 0, 0}));
|
|
auto pair = [](const std::pair<Json, std::string>& p) { return Json::array({p.first, p.second}); };
|
|
EqJson(pair(SeatUtil({{"five_hour", {{"utilization", 61}, {"resetsAt", 1}}}, {"seven_day", {{"utilization", 18}, {"resetsAt", 2}}}})), Json::array({60, "five_hour"}));
|
|
EqJson(pair(SeatUtil({{"five_hour", {{"utilization", 12}, {"resetsAt", 1}}}, {"seven_day_fable", {{"utilization", 97}, {"resetsAt", 2}}}})), Json::array({95, "seven_day_fable"}));
|
|
Eq(IsEligible(seat(1, 10, 99), With(cfg, {{"maxUtil", 100}})), false);
|
|
Eq(IsEligible(seat(1, 10, 98), With(cfg, {{"maxUtil", 100}})), true);
|
|
Eq(IsEligible(seat(1, 10, 94), cfg), false);
|
|
// Redaction: the seat's own token, token shapes and addresses; auth
|
|
// failures by status or by text.
|
|
const std::vector<std::string> secret = {"lit-secret"};
|
|
Eq(ClipReason("token sk-ant-oat01-abcdefghijklmnopqrstuvwxyz0123 and jorijn@example.com x\n y", secret), "token [redacted:token] and [redacted:email] x y");
|
|
Eq(ClipReason("bad lit-secret here", secret), "bad [redacted:token] here");
|
|
Eq(LooksLikeAuthFailure({{"api_error_status", 401}}, ""), true);
|
|
Eq(LooksLikeAuthFailure(nullptr, "Invalid API key · Please run /login"), true);
|
|
Eq(LooksLikeAuthFailure({{"result", "ok"}}, ""), false);
|
|
// find_claude_bin: CLAUDE_BIN, then PATH, then the newest editor-bundled
|
|
// binary (numerically), else the bare name.
|
|
const fs::path home = fs::temp_directory_path() / std::format("seats-home-{}", Platform::PublishedPid());
|
|
fs::remove_all(home);
|
|
fs::create_directories(home);
|
|
Eq(FindClaudeBin({{"CLAUDE_BIN", "/x/claude"}, {"PATH", ""}}, home.string()), "/x/claude");
|
|
const fs::path binDir = home / "bin";
|
|
fs::create_directories(binDir);
|
|
const fs::path fake = binDir / "claude";
|
|
std::ofstream(fake).close();
|
|
fs::permissions(fake, fs::perms::owner_all | fs::perms::group_read | fs::perms::group_exec | fs::perms::others_read | fs::perms::others_exec);
|
|
Eq(FindClaudeBin({{"PATH", std::format("/nowhere:{}", binDir.string())}}, home.string()), fake.string());
|
|
for (const std::string_view v : {"2.1.100-linux-x64", "2.1.99-linux-x64", "2.1.263-linux-x64"}) {
|
|
const fs::path d = home / ".vscode-oss/extensions" / std::format("anthropic.claude-code-{}", v) / "resources/native-binary";
|
|
fs::create_directories(d);
|
|
std::ofstream(d / "claude").close();
|
|
}
|
|
Eq(FindClaudeBin({{"PATH", "/nowhere"}}, home.string()), (home / ".vscode-oss/extensions/anthropic.claude-code-2.1.263-linux-x64/resources/native-binary/claude").string());
|
|
Eq(FindClaudeBin({{"PATH", "/nowhere"}}, (home / "empty").string()), "claude");
|
|
fs::remove_all(home);
|
|
std::println("seats: {} checks{}", N, Failures ? std::format(", {} failed", Failures) : std::string(" ok"));
|
|
return Failures ? 1 : 0;
|
|
}
|