Crafter.Review/tests/SeatPool/main.cpp

552 lines
51 KiB
C++
Raw Permalink Normal View History

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// The seat pool: seats from .env, placement, settling, credentials, the
// ledger; the checks that drive the Scheduler and the Runner are in the
// Scheduler test. Then reference vectors for the retry-after scan, the cycle
// start, the knobs, the OpenRouter cost and the description.
#include <stdlib.h>
#include <time.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;
}
Json With(Json value, const Json& changes) {
for (auto it = changes.begin(); it != changes.end(); ++it) value[it.key()] = it.value();
return value;
}
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 Ids(const std::vector<Json>& seats) {
std::vector<std::string> ids;
for (const Json& s : seats) ids.push_back(s.at("id").get<std::string>());
return Dyn::Join(ids, ",");
}
}
int main() {
std::println("seat pool: seats from .env, placement, settling, credentials, the ledger, re-placement");
// The dates below are UTC's; the process zone decides what a timestamp's
// date is, as it does for dt.date.fromtimestamp.
setenv("TZ", "UTC", 1);
tzset();
Platform::SetMode(Platform::Mode::Native);
const fs::path td = fs::temp_directory_path() / std::format("crafter-review-seatpool-{}", Platform::PublishedPid());
fs::remove_all(td);
fs::create_directories(td);
setenv("REVIEW_GATES_BOARD", (td / "seat-board.json").c_str(), 1);
const fs::path envFile = td / ".env";
Write(envFile, "CLAUDE_CODE_OAUTH_TOKEN1=\"tok-one\"\nCLAUDE_CODE_OAUTH_TOKEN2=tok-two\nANTHROPIC_API_KEY1=sk-ant-api-one\nANTHROPIC_API_KEY=sk-ant-bare-never-a-seat\nMAILOSAUR=x\n");
const Json acfg = SeatPool::ApiConfig({{"REVIEW_GATES_API_BUDGET_USD", "10"}, {"REVIEW_GATES_API_LENS_COST_USD", "1"}, {"REVIEW_GATES_API_CONCURRENCY", "2"}});
const Json cfg = Seats::ConfigFromEnv({});
SeatPool::Ledger ledger(td / "ledger.json", acfg);
feat(seats): the env file's settings count, and a machine's own seats file ApiConfig and PoolConfig read the seat order, the budgets and the concurrency from the process environment alone, so a `.env` that said REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver reads those lines from the file under the process environment (config_env, 2026-10-17: a setting lives beside the seats it orders). SeatPool::SettingsOf does the same - the file's placement settings under the process environment, in a map handed to the placement readers alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the process environment untouched, so no other reader sees a line of the file: a path's or a binary's line reaches nothing, and two drivers reading one .env keep one board and one ledger. The placement settings are one list, SeatPool::PlacementSettings - the fifteen names those readers read - for the map, the run's log and what makes a working directory's .env a seats file; a REVIEW_GATES_* line of any other name stays out of the map and is named, by the run's log and on `seats`'s stderr. One dotenv grammar serves the tokens and the settings (Seats::DotenvLines). `run` and `seats` resolve the file from their own --env-file (SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else the working directory's .env - the operator's, as the Python driver and `seats` read it, not the repository under review's - when it is a file holding a seat or a placement setting, each with a value, or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application .env, its Vite or Supabase lines, one of blank placeholder lines or one that is no file is passed over, and the run says so - else the machine's own $XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env: one file for every repository on a machine, a repository's seats .env winning over it. Each file is read once and the settings and the tokens are taken from that read (SeatsFile::Text), so a file that can be read only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives its settings and its seats alike, and a failed read raises its own error where the tokens are laid over the environment: SeatPool::WithFileTokens, for `run` and `seats` alike, and LoadSeats takes the merged environment and no longer reads a file by path. An empty `seats --env-file` means no file now, as it did for `run`, and the `seats` report names the side that runs first (it said "api seats run first" whatever the order). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a path of their own that nothing creates, so a global file stays out of them; the grammar, the settings and their precedence, the lookup and the one read have their tests, a run with `--env` (a prefix) and with a repeated --env-file, and a `seats` with `--env`, take tokens and settings from the one file, an application's .env and a venv are passed over, a FIFO is read once, no line of the file reaches `stop`, every listed placement setting moves a reader, another REVIEW_GATES_* line is named and a .env of blank lines passed over, and the README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
std::vector<Json> seats = SeatPool::LoadSeats(SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile(envFile.string(), td)), "", &ledger, acfg);
Check(".env: api seats, oauth seats, never the bare ANTHROPIC_API_KEY, local only when nothing is configured",
feat(seats): the env file's settings count, and a machine's own seats file ApiConfig and PoolConfig read the seat order, the budgets and the concurrency from the process environment alone, so a `.env` that said REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver reads those lines from the file under the process environment (config_env, 2026-10-17: a setting lives beside the seats it orders). SeatPool::SettingsOf does the same - the file's placement settings under the process environment, in a map handed to the placement readers alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the process environment untouched, so no other reader sees a line of the file: a path's or a binary's line reaches nothing, and two drivers reading one .env keep one board and one ledger. The placement settings are one list, SeatPool::PlacementSettings - the fifteen names those readers read - for the map, the run's log and what makes a working directory's .env a seats file; a REVIEW_GATES_* line of any other name stays out of the map and is named, by the run's log and on `seats`'s stderr. One dotenv grammar serves the tokens and the settings (Seats::DotenvLines). `run` and `seats` resolve the file from their own --env-file (SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else the working directory's .env - the operator's, as the Python driver and `seats` read it, not the repository under review's - when it is a file holding a seat or a placement setting, each with a value, or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application .env, its Vite or Supabase lines, one of blank placeholder lines or one that is no file is passed over, and the run says so - else the machine's own $XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env: one file for every repository on a machine, a repository's seats .env winning over it. Each file is read once and the settings and the tokens are taken from that read (SeatsFile::Text), so a file that can be read only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives its settings and its seats alike, and a failed read raises its own error where the tokens are laid over the environment: SeatPool::WithFileTokens, for `run` and `seats` alike, and LoadSeats takes the merged environment and no longer reads a file by path. An empty `seats --env-file` means no file now, as it did for `run`, and the `seats` report names the side that runs first (it said "api seats run first" whatever the order). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a path of their own that nothing creates, so a global file stays out of them; the grammar, the settings and their precedence, the lookup and the one read have their tests, a run with `--env` (a prefix) and with a repeated --env-file, and a `seats` with `--env`, take tokens and settings from the one file, an application's .env and a venv are passed over, a FIFO is read once, no line of the file reaches `stop`, every listed placement setting moves a reader, another REVIEW_GATES_* line is named and a .env of blank lines passed over, and the README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
Ids(seats) == "api1,1,2" && Ids(SeatPool::LoadSeats({}, "")) == "local", Ids(seats));
const std::string summary = DumpSpaced(SeatPool::Summary(seats));
Check("no secret in the summary or the table", !(summary + SeatPool::Table(seats)).contains("tok-one") && !summary.contains("sk-ant"));
Check("the 5-hour share of a pass follows the run's effort (0.125 at xhigh, 0.5 at max and any other); the env overrides every effort",
Dyn::Equal(SeatPool::PoolConfig({}).at("lensCostPct"), 0.5) && Dyn::Equal(SeatPool::PoolConfig({}, "max").at("lensCostPct"), 0.5)
&& Dyn::Equal(SeatPool::PoolConfig({}, "xhigh").at("lensCostPct"), 0.125) && Dyn::Equal(SeatPool::PoolConfig({{"REVIEW_GATES_LENS_COST_PCT", "5"}}, "xhigh").at("lensCostPct"), 5));
const double now = std::floor(Platform::Now());
Json& api1 = seats[0];
Json& s1 = seats[1];
Json& s2 = seats[2];
api1["state"] = "ok";
s1 = With(s1, {{"state", "ok"}, {"util", 60}, {"windows", {{"five_hour", {{"utilization", 60}, {"resetsAt", now + 600}}}, {"seven_day", {{"utilization", 10}, {"resetsAt", now + 86400}}}}}});
s2 = With(s2, {{"state", "ok"}, {"util", 50}, {"windows", {{"five_hour", {{"utilization", 20}, {"resetsAt", now + 900}}}, {"seven_day", {{"utilization", 50}, {"resetsAt", now + 86400}}}}}});
auto pick = [&](const Json& apiConfig) {
const Json* picked = SeatPool::Pick(seats, now, cfg, apiConfig);
return picked ? picked->at("id").get<std::string>() : std::string("None");
};
const std::string first = pick(acfg);
api1["running"] = 2;
Check("api seat first; at its cap the oauth seat with the most 5-hour headroom", first == "api1" && pick(acfg) == "2");
api1["running"] = 0;
api1["spentUsd"] = 9.5;
Check("api seat without budget room: skipped; oauth-first order", pick(acfg) == "2" && pick(With(acfg, {{"order", "oauth-first"}, {"budgetUsd", 100}})) == "2");
std::vector<std::string> logs;
auto log = [&](const std::string& line) { logs.push_back(line); };
s1 = With(s1, {{"state", "ok"}, {"util", 20}, {"running", 1}, {"windows", {{"five_hour", {{"utilization", 20}, {"resetsAt", now + 600}}}}}});
Json limit = {{"status", "limit"}, {"reason", "rate limit"}, {"rateType", "five_hour"}, {"rateLimit", {{"five_hour", {{"utilization", 100}, {"resetsAt", now + 600}}}}}};
bool requeued = SeatPool::Settle(s1, limit, now, cfg, acfg, &ledger, log);
Check("5h limit on an oauth seat: limited, paused until its reset + 30 s, requeued",
requeued && s1.at("state") == "limited" && s1.at("pauseUntil").get<double>() == now + 630 && !SeatPool::Eligible(s1, now, cfg, acfg));
Json spend = {{"status", "ok"}, {"costUsd", 2.5}, {"rateLimit", Json::object()}};
requeued = SeatPool::Settle(api1, spend, now, cfg, acfg, &ledger, log);
Check("api seat: spend accrues in the seat and the ledger, and the budget takes the seat out",
!requeued && api1.at("spentUsd").get<double>() == 12.0 && ledger.Entry("api1").at("spentUsd").get<double>() == 2.5 && api1.at("state") == "out");
Json api2 = SeatPool::NewSeat("api2", "api", "k");
api2["state"] = "ok";
Json credits = {{"status", "credits"}, {"reason", "Your credit balance is too low"}, {"costUsd", 0}, {"rateLimit", Json::object()}};
requeued = SeatPool::Settle(api2, credits, now, cfg, acfg, &ledger, log);
Check("credit error: out, recorded in the ledger, requeued", requeued && api2.at("state") == "out" && Dyn::Truthy(ledger.Entry("api2").at("outSince")));
// 2026-10-31: a 'concurrents' rejection (too many requests at once on the
// account) is a short back-off, not the window's reset.
Json sc = With(SeatPool::NewSeat("c", "oauth", "t"), {{"state", "ok"}, {"util", 10}, {"running", 1}, {"pauseUntil", 0}, {"windows", {{"five_hour", {{"utilization", 10}, {"resetsAt", now + 7000}}}}}});
Json concurrents = {{"status", "limit"}, {"reason", "You've hit your usage limit"}, {"rateType", "concurrents"}, {"rateLimit", {{"five_hour", {{"utilization", 10}, {"resetsAt", now + 7000}}}}}};
requeued = SeatPool::Settle(sc, concurrents, now, cfg, acfg, &ledger, log);
Check("a 'concurrents' rejection on an oauth seat: requeued, paused for seconds, not until the 5h reset",
requeued && sc.at("state") == "limited" && sc.at("pauseUntil").get<double>() == now + SeatPool::ConcurrentBackoffS && !SeatPool::Eligible(sc, now, cfg, acfg));
Json backOk = {{"status", "ok"}, {"rateLimit", {{"five_hour", {{"utilization", 11}, {"resetsAt", now + 7000}}}}}};
requeued = SeatPool::Settle(sc, backOk, now + SeatPool::ConcurrentBackoffS + 1, cfg, acfg, &ledger, log);
Check("...and the seat is back after the back-off", !requeued && sc.at("state") == "ok" && SeatPool::Eligible(sc, now + SeatPool::ConcurrentBackoffS + 1, cfg, acfg));
// 2026-10-31: an account whose 5-hour limit is removed reports the weekly
// window alone; until then the pool took it for an unprobed seat - ranked
// after every other, never held to the weekly cutoff. The cutoff is 99 %.
const Json px = SeatPool::PoolConfig({}, "xhigh");
Json nf = With(SeatPool::NewSeat("n", "oauth", "tn"), {{"state", "ok"}, {"running", 0}, {"pauseUntil", 0}, {"windows", {{"seven_day", {{"utilization", 3}, {"resetsAt", now + 6 * 86400}}}}}});
Json f15 = With(SeatPool::NewSeat("f", "oauth", "tf"), {{"state", "ok"}, {"running", 0}, {"pauseUntil", 0},
{"windows", {{"five_hour", {{"utilization", 15}, {"resetsAt", now + 3600}}}, {"seven_day", {{"utilization", 11}, {"resetsAt", now + 86400}}}}}});
std::vector<Json> pair = {f15, nf};
std::vector<std::string> order;
for (const Json* s : SeatPool::Ranked(pair, now, px, acfg)) order.push_back(s->at("id").get<std::string>());
Check("an account without a 5-hour window is placed by its weekly one: ranked by it, ahead of a seat at 5h 15 %, its running passes projected at the weekly figure",
order == std::vector<std::string>{"n", "f"} && std::abs(SeatPool::OauthProjected(With(nf, {{"running", 100}}), px) - 7.0) < 1e-9, Dyn::Join(order, ","));
Json n97 = With(nf, {{"windows", {{"seven_day", {{"utilization", 97}, {"resetsAt", now + 86400}}}}}});
Json n97r40 = With(n97, {{"running", 40}});
Json n97r60 = With(n97, {{"running", 60}});
Check("...under the weekly cutoff alone, no 5-hour ceiling: at 97 % it takes a pass with 40 running, not with 60 (97 + 61 x 0.04 >= 99)",
SeatPool::Eligible(n97r40, now, px, acfg) && !SeatPool::Eligible(n97r60, now, px, acfg));
Json n99 = With(nf, {{"windows", {{"seven_day", {{"utilization", 99}, {"resetsAt", now + 86400}}}}}});
Check("...and over the cutoff it waits for its weekly reset + 30 s", !SeatPool::Eligible(n99, now, px, acfg) && SeatPool::EarliestResume({n99}, now) == now + 86430);
Json n127 = With(nf, {{"running", 127}});
Json n128 = With(nf, {{"running", 128}});
Json nShared = With(nf, {{"running", 100}, {"foreign", {{"count", 28}, {"cost5h", 0.0}, {"cost7d", 0.0}, {"costUsd", 0.0}}}});
Json f127 = With(f15, {{"running", 127}});
Json f128 = With(f15, {{"running", 128}});
Check("128 passes per OAuth account, every driver counted, whatever its windows; REVIEW_GATES_OAUTH_CONCURRENCY overrides",
SeatPool::Eligible(n127, now, px, acfg) && !SeatPool::Eligible(n128, now, px, acfg) && !SeatPool::Eligible(nShared, now, px, acfg) && SeatPool::Eligible(f127, now, px, acfg)
&& !SeatPool::Eligible(f128, now, px, acfg) && px.at("oauthConcurrency") == 128 && SeatPool::PoolConfig({{"REVIEW_GATES_OAUTH_CONCURRENCY", "64"}}, "xhigh").at("oauthConcurrency") == 64);
Json weekly = {{"status", "ok"}, {"rateLimit", {{"seven_day", {{"utilization", 4}, {"resetsAt", now + 6 * 86400}}}}}};
requeued = SeatPool::Settle(nf, weekly, now, px, acfg, &ledger, log);
Check("...a finished pass's reading keeps it a weekly seat, unpaused; the log table says `no limit` in its 5h column",
!requeued && Seats::BurstWindow(nf.at("windows")) == "seven_day" && nf.at("pauseUntil") == 0 && SeatPool::Table({nf}).contains("| no limit |"), SeatPool::Table({nf}));
Check("the weekly share of a pass follows the run's effort (0.04 at xhigh, 0.125 otherwise); the env overrides it",
Dyn::Equal(px.at("lensCost7dPct"), 0.04) && Dyn::Equal(SeatPool::PoolConfig({}).at("lensCost7dPct"), 0.125) && Dyn::Equal(SeatPool::PoolConfig({}, "max").at("lensCost7dPct"), 0.125)
&& Dyn::Equal(SeatPool::PoolConfig({{"REVIEW_GATES_LENS_COST_7D_PCT", "0.5"}}, "xhigh").at("lensCost7dPct"), 0.5));
const fs::path scratch = td / "scratch";
fs::create_directories(scratch);
const std::map<std::string, std::string> baseEnv = {{"PATH", "/bin"}, {"ANTHROPIC_API_KEY", "leak"}, {"CLAUDE_CODE_OAUTH_TOKEN", "leak2"}, {"HOME", td.string()}};
const std::map<std::string, std::string> e1 = SeatPool::ChildEnv(SeatPool::NewSeat("1", "oauth", "tok-one"), baseEnv, scratch);
const fs::path cred = scratch / "seat-1" / ".credentials.json";
Check("oauth seat: its own config dir, the token in .credentials.json (0600), no token variable",
e1.at("CLAUDE_CONFIG_DIR") == (scratch / "seat-1").string() && Dyn::LoadJsonFile(cred).at("claudeAiOauth").at("accessToken") == "tok-one"
&& (fs::status(cred).permissions() & fs::perms::all) == (fs::perms::owner_read | fs::perms::owner_write) && !e1.contains("CLAUDE_CODE_OAUTH_TOKEN") && !e1.contains("ANTHROPIC_API_KEY"));
std::map<std::string, std::string> launcher = baseEnv;
launcher["CLAUDE_CODE_ENTRYPOINT"] = "claude-vscode";
const std::map<std::string, std::string> e2 = SeatPool::ChildEnv(SeatPool::NewSeat("api1", "api", "sk-ant-api-one"), launcher, scratch);
Check("api seat: the key in ANTHROPIC_API_KEY, the launcher's CLAUDE_CODE_ENTRYPOINT dropped, the key approved in its own .claude.json",
e2.at("ANTHROPIC_API_KEY") == "sk-ant-api-one" && !e2.contains("CLAUDE_CODE_ENTRYPOINT")
&& Dyn::Equal(Dyn::LoadJsonFile(scratch / "seat-api1" / ".claude.json").at("customApiKeyResponses").at("approved"), Json::array({"sk-ant-api-one"})));
// ---- reference vectors ------------------------------------------------------------------
const std::vector<std::pair<std::string, double>> retry = {
{"Retry-After: 30", 35},
{"retry after 2000 seconds", 900},
{"RETRY-AFTER:\u0663", 30},
{"please retry after a moment", 90},
{std::format("retry-after:{}45", std::string(14, ' ')), 90},
{std::format("retry-after:{}45", std::string(12, ' ')), 90},
{"x retry_after 5 retry after 7", 30},
{"retry-after: \u0663\u0660 x", 30},
{"Retry after 99999999999999999999999", 900},
{"", 90},
{"retry\u2028after 5", 90},
{"RETRY AFTER\n\n12", 30},
};
std::vector<std::string> missed;
for (const auto& [text, want] : retry) {
if (SeatPool::RetryAfter(text, 90) != want) missed.push_back(std::format("{} -> {}", Dyn::Repr(text), SeatPool::RetryAfter(text, 90)));
}
Check("retry-after: the regex's greedy run, Unicode digits, the 30..900 clamp, the fallback", missed.empty(), Dyn::Join(missed, "; "));
const std::vector<std::tuple<double, std::int32_t, std::string>> cycles = {
{1788775200.0, 1, "2026-09-01"}, {1788775200.0, 7, "2026-09-07"}, {1788775200.0, 8, "2026-08-08"}, {1788775200.0, 28, "2026-08-28"},
{1772323200.0, 28, "2026-02-28"}, {1772323200.0, 15, "2026-02-15"}, {1767225599.0, 5, "2025-12-05"}, {1709164800.0, 28, "2024-02-28"},
};
missed.clear();
for (const auto& [at, day, want] : cycles) {
if (SeatPool::CycleStart(at, day) != want) missed.push_back(std::format("{} {} -> {}", at, day, SeatPool::CycleStart(at, day)));
}
Check("cycle start: this month's billing day once reached, else last month's, clamped to its length", missed.empty(), Dyn::Join(missed, "; "));
// 2026-10-17 (the Python driver's, ported 2026-11-02): the cycle is the
// seat's grant day - the credits' own, from the Console's expiry minus
// three days - and a key that answered the credit error stays out across
// the rollover until a probe sees it answer again (then: new credits, a
// new cycle). The Python driver's vectors.
{
auto at = [](std::int32_t y, unsigned m, unsigned d) {
using namespace std::chrono;
return static_cast<double>(duration_cast<seconds>((sys_days(year_month_day(year(y), month(m), day(d))) + hours(12)).time_since_epoch()).count());
};
Check("cycle start: a day past the month's end is its last day",
SeatPool::CycleStart(at(2026, 4, 30), 31) == "2026-04-30" && SeatPool::CycleStart(at(2026, 3, 1), 31) == "2026-02-28" && SeatPool::CycleStart(at(2026, 3, 30), 28) == "2026-03-28");
constexpr double t17 = at(2026, 10, 17);
Check("next credits: the grant day after the cycle's; the cycle's own when the seat went out on credit before it",
SeatPool::NextGrant(t17, 17) == "2026-11-17" && SeatPool::NextGrant(t17, 5) == "2026-11-05" && SeatPool::NextGrant(t17, 17, "2026-10-08T20:19:06") == "2026-10-17"
&& SeatPool::NextGrant(t17, 5, "2026-10-13T22:46:02") == "2026-11-05" && SeatPool::NextGrant(at(2026, 12, 20), 31) == "2026-12-31");
Check("ledger verdict: a credit-out, the budget, else ok",
SeatPool::LedgerVerdict({{"outSince", "2026-10-08T20:19:06"}, {"outReason", "credit balance too low"}, {"spentUsd", 0}}, 200)
== std::pair<bool, std::string>(true, "ledger: credit balance too low since 2026-10-08T20:19")
&& SeatPool::LedgerVerdict({{"spentUsd", 200.0}}, 200) == std::pair<bool, std::string>(true, "ledger: $200.00 spent this cycle, budget $200")
&& SeatPool::LedgerVerdict({{"spentUsd", 1}}, 200) == std::pair<bool, std::string>(false, ""));
const std::string text2 = "ANTHROPIC_API_KEY3=sk-x\nANTHROPIC_API_KEY3_GRANT_DAY=16\nANTHROPIC_API_KEY4_GRANT_DAY = 5\nANTHROPIC_API_KEY1_GRANT_DAY=32\nANTHROPIC_API_KEY2_GRANT_DAY=x\nANTHROPIC_API_KEY_GRANT_DAY=9\n";
Check("a grant day: decimal digits of any script, leading zeros of any length; anything else ignored",
Seats::ApiGrantDays({{"ANTHROPIC_API_KEY1_GRANT_DAY", "0000000016"}, {"ANTHROPIC_API_KEY2_GRANT_DAY", "\uFF11\uFF16"}, {"ANTHROPIC_API_KEY3_GRANT_DAY", "1_6"},
{"ANTHROPIC_API_KEY4_GRANT_DAY", "99999999999999999999"}, {"ANTHROPIC_API_KEY5_GRANT_DAY", "\u00B2"}, {"ANTHROPIC_API_KEY6_GRANT_DAY", "0"}})
== std::map<std::int32_t, std::int32_t> {{1, 16}, {2, 16}});
const std::map<std::string, std::string> tokens2 = Seats::SeatTokensFromEnvFile(text2);
Check("the grant day per seat from the dotenv file, 1..31 only, never a key",
tokens2 == std::map<std::string, std::string> {{"ANTHROPIC_API_KEY3", "sk-x"}, {"ANTHROPIC_API_KEY3_GRANT_DAY", "16"}, {"ANTHROPIC_API_KEY4_GRANT_DAY", "5"},
{"ANTHROPIC_API_KEY1_GRANT_DAY", "32"}, {"ANTHROPIC_API_KEY2_GRANT_DAY", "x"}}
&& Seats::ApiGrantDays(tokens2) == std::map<std::int32_t, std::int32_t> {{3, 16}, {4, 5}}
&& Seats::ApiKeysFromEnv(tokens2) == std::vector<std::pair<std::int32_t, std::string>> {{3, "sk-x"}});
SeatPool::Ledger old(td / "old.json", acfg,
{{"api1", {{"cycle", "1999-01-01"}, {"spentUsd", 500}, {"procs", 9}, {"outSince", "x"}, {"outReason", "y"}}},
{"api2", {{"cycle", "1999-01-01"}, {"spentUsd", 500}, {"procs", 9}, {"outSince", nullptr}, {"outReason", nullptr}}},
{"or1", {{"cycle", "1999-01-01"}, {"spentUsd", 50}, {"procs", 9}, {"outSince", "x"}, {"outReason", "y"}}}});
Check("a new cycle resets the spend; a credit-out carries over until a probe lifts it, an OpenRouter key's too (the month's turn puts nothing on it)",
old.Entry("api1").at("spentUsd") == 0 && old.Entry("api1").at("procs") == 0 && old.Entry("api1").at("outSince") == "x" && old.Entry("api2").at("outSince").is_null()
&& old.Entry("api2").at("spentUsd") == 0 && old.Entry("or1").at("outSince") == "x" && old.Entry("or1").at("outReason") == "y" && old.Entry("or1").at("spentUsd") == 0);
Check("ledger out: credit, budget, else null",
SeatPool::LedgerOut({{"outSince", "2026-10-08T20:19:06"}, {"spentUsd", 0}}, 200) == "credit" && SeatPool::LedgerOut({{"spentUsd", 200.0}}, 200) == "budget"
&& SeatPool::LedgerOut({{"spentUsd", 1}}, 200).is_null());
const fs::path daysEnv = td / "days.env";
Write(daysEnv, "ANTHROPIC_API_KEY1=sk-ant-api-one\nANTHROPIC_API_KEY1_GRANT_DAY=17\nANTHROPIC_API_KEY2=sk-ant-api-two\nANTHROPIC_API_KEY2_GRANT_DAY=0\n");
SeatPool::Ledger lg(td / "days.json", acfg,
{{"api1", {{"cycle", "2026-10-01"}, {"spentUsd", 200.18}, {"procs", 112}, {"outSince", "2026-10-08T20:19:06"}, {"outReason", "credit balance too low"}}},
{"api2", {{"cycle", "2026-09-01"}, {"spentUsd", 150.0}, {"procs", 90}, {"outSince", nullptr}, {"outReason", nullptr}}}});
std::vector<Json> ds = SeatPool::LoadSeats(SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile(daysEnv.string(), td)), "api", &lg, acfg, t17);
Check("the grant day per seat from the seats file (0 is ignored: the global day), into the ledger",
Ids(ds) == "api1,api2" && ds[0].at("grantDay") == 17 && ds[1].at("grantDay") == 1 && lg.Day("api1") == 17 && lg.Day("api2") == 1, Ids(ds));
Check("rollover on the grant day: the spend starts over, a credit-out carries - the seat stays out",
ds[0].at("state") == "out" && ds[0].at("ledgerOut") == "credit" && ds[0].at("spentUsd") == 0 && lg.Entry("api1", t17).at("cycle") == "2026-10-17"
&& lg.Entry("api1", t17).at("procs") == 0 && ds[0].at("error").get<std::string>().contains("since 2026-10-08"),
DumpSpaced(ds[0]));
Check("...a budget-out resets with its cycle",
ds[1].at("state") == "unknown" && Dyn::Get(ds[1], "ledgerOut").is_null() && ds[1].at("spentUsd") == 0 && lg.Entry("api2", t17).at("cycle") == "2026-10-01");
std::vector<std::string> calls;
std::map<std::string, std::string> answers = {{"api1", "ok"}, {"api2", "out"}};
const SeatPool::ProbeFunction fake = [&](std::int32_t n, const std::string&, const std::string& model, double, const std::string&, bool, bool) {
const std::string id = std::format("api{}", n);
calls.push_back(id);
const std::string state = answers.at(id);
return Json {{"seat", n}, {"state", state}, {"windows", Json::object()}, {"probedWith", model},
{"error", state == "ok" ? Json() : Json(state == "out" ? "Your credit balance is too low" : "probe timeout after 1000 ms")}};
};
std::vector<std::string> plog;
auto plogf = [&](const std::string& line) { plog.push_back(line); };
SeatPool::Probe(ds, "claude", 1, {"claude-opus-5-5"}, plogf, fake, &lg, t17);
const Json e1 = lg.Entry("api1", t17);
Check("the run-start probe re-probes a credit-out api seat; a clean answer is new credits: the seat is back, its cycle restarted on its grant day",
calls == std::vector<std::string> {"api1", "api2"} && ds[0].at("state") == "ok" && ds[0].at("ledgerOut").is_null() && ds[0].at("error").is_null() && e1.at("outSince").is_null()
&& e1.at("cycle") == "2026-10-17" && e1.at("grantSeen") == "2026-10-17T12:00:00+00:00"
&& std::ranges::any_of(plog, [](const std::string& line) { return line.contains("new credits"); }),
Dyn::Join(plog, " | "));
const Json e2 = lg.Entry("api2", t17);
Check("...the credit error on a seat the ledger had as fine is recorded, with the next credits in the reason",
ds[1].at("state") == "out" && ds[1].at("ledgerOut") == "credit" && Dyn::Truthy(e2.at("outSince"))
&& e2.at("outSince") == "2026-10-17T12:00:00+00:00" && ds[1].at("error") == "credit balance too low since 2026-10-17T12:00; next credits due 2026-11-01",
Dyn::Str(ds[1].at("error")));
Check("reconcile: nothing to do when the probe and the ledger agree", lg.Reconcile("api1", "ok", t17, t17).empty() && lg.Reconcile("api2", "out", t17, t17).empty());
// Two drivers on one ledger (1.1.0 round 2): the decision is made on
// the file's entry under the lock, never on a copy read before the
// probe, and a mark made since the probe started stands.
const std::string outLine = R"("cycle": "2026-10-17", "spentUsd": 0, "procs": 0, "outSince": "2026-10-08T20:19:06", "outReason": "credit balance too low")";
Write(td / "race1.json", std::format("{{\"api1\": {{{}}}}}", outLine));
SeatPool::Ledger la = SeatPool::Ledger::Load(td / "race1.json", acfg, {{"api1", 17}});
SeatPool::Ledger lb = SeatPool::Ledger::Load(td / "race1.json", acfg, {{"api1", 17}});
const std::string firstGrant = la.Reconcile("api1", "ok", t17, t17);
la.Add("api1", 5.0, t17);
const std::string secondGrant = lb.Reconcile("api1", "ok", t17 + 60, t17 + 60);
const Json race1 = SeatPool::Ledger::Load(td / "race1.json", acfg, {{"api1", 17}}).Entry("api1", t17 + 60);
Check("two drivers: the second's copy, read before the first saw the grant, lifts nothing - the first's grant and its spend stand",
firstGrant == "new-grant" && secondGrant.empty() && race1.at("spentUsd") == 5.0 && race1.at("procs") == 1 && race1.at("outSince").is_null(), DumpSpaced(race1));
Write(td / "race2.json", R"({"api1": {"cycle": "2026-10-17", "spentUsd": 199.4, "procs": 180, "outSince": "2026-10-17T12:00:06", "outReason": "credit balance too low"}})");
SeatPool::Ledger lc = SeatPool::Ledger::Load(td / "race2.json", acfg, {{"api1", 17}});
const std::string late = lc.Reconcile("api1", "ok", t17, t17 + 30);
const Json race2 = SeatPool::Ledger::Load(td / "race2.json", acfg, {{"api1", 17}}).Entry("api1", t17 + 30);
Check("a credit error recorded after the probe started outlives its answer: no grant, the spend kept",
late.empty() && race2.at("outSince") == "2026-10-17T12:00:06" && race2.at("spentUsd") == 199.4 && race2.at("procs") == 180, DumpSpaced(race2));
Write(td / "race3.json", R"({"api1": {"cycle": "2026-10-17", "spentUsd": 2, "procs": 1, "outSince": null, "outReason": null, "grantSeen": "2026-10-17T12:00:10"}})");
SeatPool::Ledger ld = SeatPool::Ledger::Load(td / "race3.json", acfg, {{"api1", 17}});
const std::string stale = ld.Reconcile("api1", "out", t17, t17 + 30);
const std::string fresh = ld.Reconcile("api1", "out", t17 + 60, t17 + 60);
Check("...and a credit error read before a grant another driver saw since records nothing; one read after it does",
stale.empty() && fresh == "credit-gone" && SeatPool::Ledger::Load(td / "race3.json", acfg, {{"api1", 17}}).Entry("api1", t17 + 60).at("spentUsd") == 2);
// The grant seen at 23:59:55, the cycle turning at midnight (grant
// day 17) before a credit error read at 23:59:40 is reconciled.
Write(td / "race6.json", R"({"api1": {"cycle": "2026-11-16", "spentUsd": 2, "procs": 1, "outSince": null, "outReason": null, "grantSeen": "2026-11-16T23:59:55"}})");
SeatPool::Ledger le = SeatPool::Ledger::Load(td / "race6.json", acfg, {{"api1", 17}});
const std::string acrossMidnight = le.Reconcile("api1", "out", at(2026, 11, 16) + 43180, at(2026, 11, 17) - 43198);
const Json race6 = SeatPool::Ledger::Load(td / "race6.json", acfg, {{"api1", 17}}).Entry("api1", at(2026, 11, 17) - 43198);
// The stamps carry their UTC offset and Reconcile orders marks by
// instant (1.1.0 round 4). A POSIX rule, not a zone file: Central
// European time, whose summer time ends 2026-10-25 03:00, 02:00-03:00
// then passing twice.
setenv("TZ", "CET-1CEST,M3.5.0,M10.5.0/3", 1);
tzset();
constexpr double fallBack = at(2026, 10, 25) - 39610;
Write(td / "race7.json", R"({"api1": {"cycle": "2026-10-17", "spentUsd": 150, "procs": 120, "outSince": null, "outReason": null}})");
SeatPool::Ledger::Load(td / "race7.json", acfg, {{"api1", 17}}).MarkOut("api1", "credit balance too low", fallBack + 15);
const std::string afterFallBack = SeatPool::Ledger::Load(td / "race7.json", acfg, {{"api1", 17}}).Reconcile("api1", "ok", fallBack, fallBack + 16);
const Json race7 = SeatPool::Ledger::Load(td / "race7.json", acfg, {{"api1", 17}}).Entry("api1", fallBack + 16);
// A stamp without an offset, as older drivers wrote, is local time:
// 13:30 CEST is 11:30 UTC, before a probe started at 12:00 UTC (14:00
// CEST) - new credits; read as UTC it would be after the probe.
Write(td / "race9.json", R"({"api1": {"cycle": "2026-10-17", "spentUsd": 3, "procs": 1, "outSince": "2026-10-17T13:30:00", "outReason": "credit balance too low"}})");
const std::string legacy = SeatPool::Ledger::Load(td / "race9.json", acfg, {{"api1", 17}}).Reconcile("api1", "ok", t17, t17 + 30);
setenv("TZ", "UTC", 1);
tzset();
Check("a credit error recorded after the probe started outlives its answer in the hour DST repeats: 02:00:05 CET is after 02:59:50 CEST",
afterFallBack.empty() && race7.at("outSince") == "2026-10-25T02:00:05+01:00" && race7.at("spentUsd") == 150, DumpSpaced(race7));
Write(td / "race8.json", R"({"api1": {"cycle": "2026-10-17", "spentUsd": 2, "procs": 1, "outSince": "2026-10-17T11:00:06-01:00", "outReason": "credit balance too low"}})");
const std::string otherZone = SeatPool::Ledger::Load(td / "race8.json", acfg, {{"api1", 17}}).Reconcile("api1", "ok", t17, t17 + 30);
Check("...and from a driver in another zone: its 11:00:06-01:00 is after a probe started at 12:00:00 UTC", otherZone.empty());
Check("a stamp without an offset, as older drivers wrote, is read as local time: 13:30 CEST is before a probe started at 14:00 CEST", legacy == "new-grant", legacy);
Write(td / "race10.json", std::format("{{\"api1\": {{{}}}}}", outLine));
const std::string race10Before = Platform::ReadFile(td / "race10.json").value_or("");
const std::string unread = SeatPool::Ledger::Load(td / "race10.json", acfg, {{"api1", 17}}).Reconcile("api1", "unknown", t17, t17);
Check("a reading neither ok nor the credit error writes nothing", unread.empty() && Platform::ReadFile(td / "race10.json").value_or("") == race10Before);
// OpenRouter keys (1.1.0, Jorijn: a prepaid key has no cycle of its
// own): one the ledger holds out on credit is probed again through
// the key probe, and room on its limit lifts the mark with its
// cycle's spend kept; the limit reached is recorded; one out on its
// budget is not probed. Their cycle is the cycle day's.
Write(td / "or.env", "OPENROUTER_KEY1=or-one\nOPENROUTER_KEY2=or-two\nOPENROUTER_KEY3=or-three\n");
Write(td / "or-ledger.json", R"({"or1": {"cycle": "2026-10-01", "spentUsd": 7.5, "procs": 3, "outSince": "2026-10-10T09:00:00+00:00", "outReason": "credit balance too low"},
"or2": {"cycle": "2026-10-01", "spentUsd": 1, "procs": 1, "outSince": null, "outReason": null},
"or3": {"cycle": "2026-10-01", "spentUsd": 150, "procs": 90, "outSince": null, "outReason": null}})");
SeatPool::Ledger orLedger = SeatPool::Ledger::Load(td / "or-ledger.json", acfg);
std::vector<Json> orSeats = SeatPool::LoadSeats(SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile((td / "or.env").string(), td)), "openrouter", &orLedger, acfg, t17);
std::vector<std::string> keyCalls;
const SeatPool::OpenRouterProbeFunction fakeKey = [&](const std::string& secret, double) {
keyCalls.push_back(secret);
return secret == "or-one" ? Json {{"state", "ok"}, {"remaining", 3.0}} : Json {{"state", "out"}, {"error", "key limit reached ($10.00 of $10 used)"}, {"remaining", 0}};
};
plog.clear();
SeatPool::Probe(orSeats, "claude", 1, {}, plogf, fake, &orLedger, t17, fakeKey);
SeatPool::Ledger orFile = SeatPool::Ledger::Load(td / "or-ledger.json", acfg);
const Json or1 = orFile.Entry("or1", t17);
const Json or2 = orFile.Entry("or2", t17);
Check("an OpenRouter key held out on credit is probed again, one out on its budget is not; room on its limit lifts the mark and keeps its cycle's spend",
Ids(orSeats) == "or1,or2,or3" && keyCalls == std::vector<std::string> {"or-one", "or-two"} && orSeats[0].at("state") == "ok" && orSeats[0].at("ledgerOut").is_null()
&& orSeats[0].at("spentUsd") == 7.5 && or1.at("outSince").is_null() && or1.at("spentUsd") == 7.5 && or1.at("procs") == 3 && or1.at("cycle") == "2026-10-01"
&& or1.at("grantSeen") == "2026-10-17T12:00:00+00:00" && std::ranges::any_of(plog, [](const std::string& line) { return line.contains("limit has room again"); })
&& orSeats[2].at("state") == "out" && orSeats[2].at("ledgerOut") == "budget",
std::format("{} {}", DumpSpaced(or1), Dyn::Join(keyCalls, ",")));
Check("...and the limit reached is recorded as the key's credit-out, the probe's error the seat's reason",
orSeats[1].at("state") == "out" && orSeats[1].at("ledgerOut") == "credit" && orSeats[1].at("error") == "key limit reached ($10.00 of $10 used)"
&& or2.at("outSince") == "2026-10-17T12:00:00+00:00" && or2.at("outReason") == "key limit reached",
DumpSpaced(or2));
// A key that answers with its cycle's budget spent is out on that
// budget, not left ok - neither placed nor out, a pool of it waiting
// forever (1.1.0 round 9): an OpenRouter key held out on credit with
// its spend over the budget, and an API key another driver lifted and
// spent the budget on since this copy was read.
Write(td / "or-spent.env", "OPENROUTER_KEY1=or-one\n");
Write(td / "or-spent.json",
R"({"or1": {"cycle": "2026-10-01", "spentUsd": 100.4, "procs": 40, "outSince": "2026-10-10T09:00:00+00:00", "outReason": "credit balance too low"}})");
SeatPool::Ledger spentLedger = SeatPool::Ledger::Load(td / "or-spent.json", acfg);
std::vector<Json> spentSeats =
SeatPool::LoadSeats(SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile((td / "or-spent.env").string(), td)), "openrouter", &spentLedger, acfg, t17);
SeatPool::Probe(spentSeats, "claude", 1, {}, plogf, fake, &spentLedger, t17, fakeKey);
Check("an OpenRouter key held out on credit with its budget spent too: the mark lifted, the seat out on the budget, a pool of it all out",
spentSeats[0].at("state") == "out" && spentSeats[0].at("ledgerOut") == "budget" && SeatPool::AllOut(spentSeats)
&& SeatPool::Ledger::Load(td / "or-spent.json", acfg).Entry("or1", t17).at("outSince").is_null(),
DumpSpaced(spentSeats[0]));
Write(td / "race11.json", std::format("{{\"api1\": {{{}}}}}", outLine));
SeatPool::Ledger stale11(SeatPool::Ledger::Load(td / "race11.json", acfg));
std::vector<Json> mine11 = SeatPool::LoadSeats(SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile(daysEnv.string(), td)), "api1", &stale11, acfg, t17);
SeatPool::Ledger other11 = SeatPool::Ledger::Load(td / "race11.json", acfg, {{"api1", 17}});
other11.Reconcile("api1", "ok", t17, t17);
other11.Add("api1", 12.0, t17);
answers["api1"] = "ok";
SeatPool::Probe(mine11, "claude", 1, {}, plogf, fake, &stale11, t17 + 60);
Check("...and an API key another driver lifted and spent its budget on since: out on the budget",
mine11[0].at("state") == "out" && mine11[0].at("ledgerOut") == "budget" && mine11[0].at("spentUsd") == 12.0 && SeatPool::AllOut(mine11), DumpSpaced(mine11[0]));
Check("an OpenRouter key's cycle starts on the cycle day", orLedger.Day("or1") == 1 && SeatPool::Ledger::Load(td / "or-ledger.json", acfg).Entry("or1", at(2026, 11, 2)).at("cycle") == "2026-11-01");
Check("...across the cycle's turn too: the grant's time carries over the rollover with an API seat's entry",
acrossMidnight.empty() && race6.at("outSince").is_null() && race6.at("grantSeen") == "2026-11-16T23:59:55" && race6.at("cycle") == "2026-11-17", DumpSpaced(race6));
Write(td / "race4.json", std::format("{{\"api1\": {{{}}}}}", outLine));
SeatPool::Ledger stale4(SeatPool::Ledger::Load(td / "race4.json", acfg));
std::vector<Json> mine = SeatPool::LoadSeats(SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile(daysEnv.string(), td)), "api1", &stale4, acfg, t17);
SeatPool::Ledger other4 = SeatPool::Ledger::Load(td / "race4.json", acfg, {{"api1", 17}});
other4.Reconcile("api1", "ok", t17, t17);
other4.Add("api1", 5.0, t17);
answers["api1"] = "ok";
plog.clear();
SeatPool::Probe(mine, "claude", 1, {}, plogf, fake, &stale4, t17 + 60);
Check("the run's probe after another driver's grant: the seat is back with that driver's spend, no second grant",
mine[0].at("state") == "ok" && mine[0].at("spentUsd") == 5.0 && mine[0].at("ledgerOut").is_null()
&& std::ranges::none_of(plog, [](const std::string& line) { return line.contains("new credits"); })
&& SeatPool::Ledger::Load(td / "race4.json", acfg).Entry("api1", t17 + 60).at("spentUsd") == 5.0,
DumpSpaced(mine[0]));
Write(td / "race5.json", R"({"api1": {"cycle": "2026-10-17", "spentUsd": 2, "procs": 1, "outSince": null, "outReason": null}})");
SeatPool::Ledger mine5(SeatPool::Ledger::Load(td / "race5.json", acfg));
std::vector<Json> seats5 = SeatPool::LoadSeats(SeatPool::WithFileTokens({}, SeatPool::ResolveSeatsFile(daysEnv.string(), td)), "api1", &mine5, acfg, t17);
SeatPool::Ledger::Load(td / "race5.json", acfg, {{"api1", 17}}).MarkOut("api1", "credit balance too low", t17 + 6);
SeatPool::Probe(seats5, "claude", 1, {}, plogf, fake, &mine5, t17);
Check("...and the run's probe that answered before another driver's credit error: the seat is out on the ledger's verdict",
seats5[0].at("state") == "out" && seats5[0].at("ledgerOut") == "credit" && seats5[0].at("error") == "ledger: credit balance too low since 2026-10-17T12:00",
DumpSpaced(seats5[0]));
calls.clear();
answers["api1"] = "unknown";
Json apiB = With(ds[1], {{"state", "out"}, {"ledgerOut", "budget"}, {"error", "ledger: $10.00 spent this cycle, budget $10"}});
Json apiU = With(ds[0], {{"state", "out"}, {"ledgerOut", "credit"}, {"error", "ledger: credit balance too low since 2026-10-08T20:19"}});
std::vector<Json> pair2 = {apiB, apiU};
SeatPool::Probe(pair2, "claude", 1, {}, plogf, fake, nullptr, t17);
Check("a budget-out seat is not probed; a credit-out seat whose probe is unreadable stays out on the ledger's verdict",
calls == std::vector<std::string> {"api1"} && pair2[0].at("state") == "out" && pair2[1].at("state") == "out"
&& pair2[1].at("error") == "ledger: credit balance too low since 2026-10-08T20:19 (probe unreadable: probe timeout after 1000 ms)",
Dyn::Str(pair2[1].at("error")));
// A grant's cycle, through the production path: the credit error
// recorded, then a probe that started after it answers.
lg.SetDay("api3", 5);
lg.MarkOut("api3", "credit balance too low", t17 - 60);
const std::string grant3 = lg.Reconcile("api3", "ok", t17, t17);
Check("new credits seen 12 days after the configured day: the cycle starts today, not on that day",
grant3 == "new-grant" && lg.Entry("api3", t17).at("cycle") == "2026-10-17" && lg.Entry("api3", t17).at("spentUsd") == 0);
lg.SetDay("api4", 16);
lg.MarkOut("api4", "credit balance too low", t17 - 60);
const std::string grant4 = lg.Reconcile("api4", "ok", t17, t17);
Check("...and on the day when it is within the week", grant4 == "new-grant" && lg.Entry("api4", t17).at("cycle") == "2026-10-16"
&& lg.Entry("api4", at(2026, 11, 15)).at("cycle") == "2026-10-16"
&& lg.Entry("api4", at(2026, 11, 16)).at("cycle") == "2026-11-16");
}
const std::string knobs = DumpSpaced(SeatPool::ApiConfig({{"REVIEW_GATES_API_BUDGET_USD", " 1_0 "},
{"REVIEW_GATES_API_CONCURRENCY", "3.9"},
{"REVIEW_GATES_API_CYCLE_DAY", "40"},
{"REVIEW_GATES_SEAT_ORDER", "oauth-first"},
{"REVIEW_GATES_OPENROUTER_BUDGET_USD", "-1"},
{"REVIEW_GATES_API_LENS_COST_USD", "nan"}}));
Check("api config: float() parsing, int() truncation, the cycle day clamped, a negative or NaN value the default",
knobs == R"({"budgetUsd": 10.0, "lensCostUsd": 1.0, "concurrency": 3, "cycleDay": 31, "order": "oauth-first", "orBudgetUsd": 100.0, "orLensCostUsd": 0.1, "orConcurrency": 16})", knobs);
const double cost = SeatPool::OpenRouterCost("openrouter/xiaomi/mimo-v2.6-pro", {{"input", 1234}, {"cacheCreation", 10}, {"cacheRead", 99999}, {"output", 777}});
Check("openrouter cost: tokens x the built-in list prices, an unknown model free", cost == 0.001577 && SeatPool::OpenRouterCost("nope", {{"input", 1}}) == 0.0, Dyn::FloatRepr(cost));
Json described = SeatPool::NewSeat("1", "oauth", "t");
described["state"] = "ok";
described["windows"] = {{"five_hour", {{"utilization", 61.5}, {"resetsAt", 1}}}, {"seven_day", {{"utilization", 18}, {"resetsAt", 2}}}, {"seven_day_overage_included", {{"utilization", 22}, {"resetsAt", 0}}},
{"seven_day_fable", {{"utilization", 3}}}, {"other", {{"utilization", nullptr}}}};
described["binding"] = "five_hour";
Json paid = SeatPool::NewSeat("api1", "api", "k");
paid = With(paid, {{"state", "out"}, {"error", "budget"}, {"spentUsd", 12.345}, {"remainingUsd", 3}});
Check("describe: an OAuth seat's windows by label, a paid seat's spend and what is left on the key",
SeatPool::Describe(described) == "ok: 5h 61.5%, 7d 18%, fable 22%, fable 3% (binding five_hour)" && SeatPool::Describe(paid) == "out (budget), $12.35 spent this cycle, $3.00 left on the key",
std::format("{} | {}", SeatPool::Describe(described), SeatPool::Describe(paid)));
feat(seats): the env file's settings count, and a machine's own seats file ApiConfig and PoolConfig read the seat order, the budgets and the concurrency from the process environment alone, so a `.env` that said REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver reads those lines from the file under the process environment (config_env, 2026-10-17: a setting lives beside the seats it orders). SeatPool::SettingsOf does the same - the file's placement settings under the process environment, in a map handed to the placement readers alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the process environment untouched, so no other reader sees a line of the file: a path's or a binary's line reaches nothing, and two drivers reading one .env keep one board and one ledger. The placement settings are one list, SeatPool::PlacementSettings - the fifteen names those readers read - for the map, the run's log and what makes a working directory's .env a seats file; a REVIEW_GATES_* line of any other name stays out of the map and is named, by the run's log and on `seats`'s stderr. One dotenv grammar serves the tokens and the settings (Seats::DotenvLines). `run` and `seats` resolve the file from their own --env-file (SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else the working directory's .env - the operator's, as the Python driver and `seats` read it, not the repository under review's - when it is a file holding a seat or a placement setting, each with a value, or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application .env, its Vite or Supabase lines, one of blank placeholder lines or one that is no file is passed over, and the run says so - else the machine's own $XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env: one file for every repository on a machine, a repository's seats .env winning over it. Each file is read once and the settings and the tokens are taken from that read (SeatsFile::Text), so a file that can be read only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives its settings and its seats alike, and a failed read raises its own error where the tokens are laid over the environment: SeatPool::WithFileTokens, for `run` and `seats` alike, and LoadSeats takes the merged environment and no longer reads a file by path. An empty `seats --env-file` means no file now, as it did for `run`, and the `seats` report names the side that runs first (it said "api seats run first" whatever the order). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a path of their own that nothing creates, so a global file stays out of them; the grammar, the settings and their precedence, the lookup and the one read have their tests, a run with `--env` (a prefix) and with a repeated --env-file, and a `seats` with `--env`, take tokens and settings from the one file, an application's .env and a venv are passed over, a FIFO is read once, no line of the file reaches `stop`, every listed placement setting moves a reader, another REVIEW_GATES_* line is named and a .env of blank lines passed over, and the README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
// The seats file (2026-11-01): one dotenv grammar for the tokens and the
// REVIEW_GATES_* settings; the settings under the process environment in
// the map the placement readers take (SettingsOf), the process
// environment itself untouched; the file resolved as --env-file, then
// $SEATS_ENV_FILE, <cwd>/.env, the machine's own under $XDG_CONFIG_HOME,
// each read once.
{
const std::vector<std::pair<std::string, std::string>> lines =
Seats::DotenvLines("export REVIEW_GATES_SEAT_ORDER='oauth-first'\n REVIEW_GATES_API_BUDGET_USD = \"50\"\nCLAUDE_CODE_OAUTH_TOKEN1=x\n# a comment\nnot a line\n=\nREVIEW_GATES=nope\n");
const std::map<std::string, std::string> settings = Seats::SettingsFromEnvFile("export REVIEW_GATES_SEAT_ORDER='oauth-first'\n REVIEW_GATES_API_BUDGET_USD = \"50\"\nCLAUDE_CODE_OAUTH_TOKEN1=x\nREVIEW_GATES=nope\n");
Check("the dotenv grammar: `[export ]NAME=value`, blanks around `=` and the value, one pair of matching quotes stripped, everything else skipped; the settings are the REVIEW_GATES_ names; the tokens as before",
lines == std::vector<std::pair<std::string, std::string>> {{"REVIEW_GATES_SEAT_ORDER", "oauth-first"}, {"REVIEW_GATES_API_BUDGET_USD", "50"}, {"CLAUDE_CODE_OAUTH_TOKEN1", "x"}, {"REVIEW_GATES", "nope"}}
&& settings == std::map<std::string, std::string> {{"REVIEW_GATES_SEAT_ORDER", "oauth-first"}, {"REVIEW_GATES_API_BUDGET_USD", "50"}}
&& Seats::SeatTokensFromEnvFile("ANTHROPIC_API_KEY=bare\nANTHROPIC_API_KEY2=k\nANTHROPIC_API_KEY2_GRANT_DAY=17\nCLAUDE_CODE_OAUTH_TOKEN=t\nCLAUDE_CODE_OAUTH_TOKEN7=no\n")
== std::map<std::string, std::string> {{"ANTHROPIC_API_KEY2", "k"}, {"ANTHROPIC_API_KEY2_GRANT_DAY", "17"}, {"CLAUDE_CODE_OAUTH_TOKEN", "t"}});
feat(seats): the env file's settings count, and a machine's own seats file ApiConfig and PoolConfig read the seat order, the budgets and the concurrency from the process environment alone, so a `.env` that said REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver reads those lines from the file under the process environment (config_env, 2026-10-17: a setting lives beside the seats it orders). SeatPool::SettingsOf does the same - the file's placement settings under the process environment, in a map handed to the placement readers alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the process environment untouched, so no other reader sees a line of the file: a path's or a binary's line reaches nothing, and two drivers reading one .env keep one board and one ledger. The placement settings are one list, SeatPool::PlacementSettings - the fifteen names those readers read - for the map, the run's log and what makes a working directory's .env a seats file; a REVIEW_GATES_* line of any other name stays out of the map and is named, by the run's log and on `seats`'s stderr. One dotenv grammar serves the tokens and the settings (Seats::DotenvLines). `run` and `seats` resolve the file from their own --env-file (SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else the working directory's .env - the operator's, as the Python driver and `seats` read it, not the repository under review's - when it is a file holding a seat or a placement setting, each with a value, or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application .env, its Vite or Supabase lines, one of blank placeholder lines or one that is no file is passed over, and the run says so - else the machine's own $XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env: one file for every repository on a machine, a repository's seats .env winning over it. Each file is read once and the settings and the tokens are taken from that read (SeatsFile::Text), so a file that can be read only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives its settings and its seats alike, and a failed read raises its own error where the tokens are laid over the environment: SeatPool::WithFileTokens, for `run` and `seats` alike, and LoadSeats takes the merged environment and no longer reads a file by path. An empty `seats --env-file` means no file now, as it did for `run`, and the `seats` report names the side that runs first (it said "api seats run first" whatever the order). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a path of their own that nothing creates, so a global file stays out of them; the grammar, the settings and their precedence, the lookup and the one read have their tests, a run with `--env` (a prefix) and with a repeated --env-file, and a `seats` with `--env`, take tokens and settings from the one file, an application's .env and a venv are passed over, a FIFO is read once, no line of the file reaches `stop`, every listed placement setting moves a reader, another REVIEW_GATES_* line is named and a .env of blank lines passed over, and the README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
const fs::path envTd = fs::temp_directory_path() / std::format("crafter-review-envfile-{}", Platform::PublishedPid());
fs::remove_all(envTd);
fs::create_directories(envTd / "repo");
fs::create_directories(envTd / "xdg" / "crafter-review");
std::ofstream(envTd / "settings.env", std::ios::binary) << "REVIEW_GATES_SEAT_ORDER=oauth-first\nREVIEW_GATES_API_BUDGET_USD=50\nREVIEW_GATES_CLAUDE_BIN=./evil\nREVIEW_GATES_SEAT_POLICY=\n";
unsetenv("REVIEW_GATES_SEAT_ORDER");
unsetenv("REVIEW_GATES_CLAUDE_BIN");
unsetenv("REVIEW_GATES_SEAT_POLICY");
setenv("REVIEW_GATES_API_BUDGET_USD", "75", 1);
const SeatPool::Settings applied = SeatPool::SettingsOf(SeatPool::ResolveSeatsFile((envTd / "settings.env").string(), envTd / "repo"));
const Json api = SeatPool::ApiConfig(applied.env);
const SeatPool::SeatsFile missing = SeatPool::ResolveSeatsFile((envTd / "none.env").string(), envTd / "repo");
std::string raised;
try {
(void) missing.Text();
} catch (const Dyn::Error& error) {
raised = error.Kind();
}
Check("SettingsOf: the file's placement settings under the process environment - the process environment wins - in the map ApiConfig reads, a blank one no setting, its other REVIEW_GATES_* "
"lines named and never in the map, the process environment itself untouched (no line of the file reaches another reader); no file gives nothing; one it cannot read gives nothing and Text() raises the "
"read's own error",
applied.fromFile == std::vector<std::string> {"REVIEW_GATES_SEAT_ORDER"} && applied.ignored == std::vector<std::string> {"REVIEW_GATES_CLAUDE_BIN"}
&& !applied.env.contains("REVIEW_GATES_CLAUDE_BIN") && !applied.env.contains("REVIEW_GATES_SEAT_POLICY") && api.at("order") == "oauth-first" && api.at("budgetUsd") == 75.0
&& !Platform::GetEnv("REVIEW_GATES_SEAT_ORDER") && !Platform::GetEnv("REVIEW_GATES_CLAUDE_BIN") && Platform::GetEnv("REVIEW_GATES_API_BUDGET_USD") == "75"
&& SeatPool::SettingsOf(SeatPool::ResolveSeatsFile(std::string(), envTd / "repo")).fromFile.empty() && SeatPool::SettingsOf(missing).fromFile.empty() && raised == "FileNotFoundError",
std::format("{} {}", Dyn::Str(api), raised));
feat(seats): the env file's settings count, and a machine's own seats file ApiConfig and PoolConfig read the seat order, the budgets and the concurrency from the process environment alone, so a `.env` that said REVIEW_GATES_SEAT_ORDER=oauth-first ordered nothing: the Python driver reads those lines from the file under the process environment (config_env, 2026-10-17: a setting lives beside the seats it orders). SeatPool::SettingsOf does the same - the file's placement settings under the process environment, in a map handed to the placement readers alone (ApiConfig, PoolConfig, Seats::ConfigFromEnv) - and leaves the process environment untouched, so no other reader sees a line of the file: a path's or a binary's line reaches nothing, and two drivers reading one .env keep one board and one ledger. The placement settings are one list, SeatPool::PlacementSettings - the fifteen names those readers read - for the map, the run's log and what makes a working directory's .env a seats file; a REVIEW_GATES_* line of any other name stays out of the map and is named, by the run's log and on `seats`'s stderr. One dotenv grammar serves the tokens and the settings (Seats::DotenvLines). `run` and `seats` resolve the file from their own --env-file (SeatPool::ResolveSeatsFile): the --env-file, else $SEATS_ENV_FILE, else the working directory's .env - the operator's, as the Python driver and `seats` read it, not the repository under review's - when it is a file holding a seat or a placement setting, each with a value, or one that cannot be read as UTF-8 text (its error is then the command's) - a readable application .env, its Vite or Supabase lines, one of blank placeholder lines or one that is no file is passed over, and the run says so - else the machine's own $XDG_CONFIG_HOME/crafter-review/env, else ~/.config/crafter-review/env: one file for every repository on a machine, a repository's seats .env winning over it. Each file is read once and the settings and the tokens are taken from that read (SeatsFile::Text), so a file that can be read only once - a pipe, `--env-file <(gpg -d seats.env.gpg)`, a FIFO - gives its settings and its seats alike, and a failed read raises its own error where the tokens are laid over the environment: SeatPool::WithFileTokens, for `run` and `seats` alike, and LoadSeats takes the merged environment and no longer reads a file by path. An empty `seats --env-file` means no file now, as it did for `run`, and the `seats` report names the side that runs first (it said "api seats run first" whatever the order). Runs, Commands and KernelModes, the tests that run `run` and `seats`, point XDG_CONFIG_HOME, and HOST_XDG_CONFIG_HOME for the Flatpak, at a path of their own that nothing creates, so a global file stays out of them; the grammar, the settings and their precedence, the lookup and the one read have their tests, a run with `--env` (a prefix) and with a repeated --env-file, and a `seats` with `--env`, take tokens and settings from the one file, an application's .env and a venv are passed over, a FIFO is read once, no line of the file reaches `stop`, every listed placement setting moves a reader, another REVIEW_GATES_* line is named and a .env of blank lines passed over, and the README, the environment table and the procedure say the new rule.
2026-11-01 01:51:48 +01:00
// Every name on the list moves a placement reader (the list is the
// readers' names, not a guess at them).
const Json apiDefault = SeatPool::ApiConfig({});
const Json poolDefault = SeatPool::PoolConfig({}, "xhigh");
std::vector<std::string> idle;
for (const std::string_view name : SeatPool::PlacementSettings) {
const std::string value = name.ends_with("SEAT_ORDER") ? "oauth-first" : name.ends_with("SEAT_POLICY") ? "pack" : "3";
const std::map<std::string, std::string> one {{std::string(name), value}};
if (Dyn::Equal(SeatPool::ApiConfig(one), apiDefault) && Dyn::Equal(SeatPool::PoolConfig(one, "xhigh"), poolDefault)) idle.emplace_back(name);
}
Check("PlacementSettings: every name on the list moves ApiConfig or PoolConfig (which takes Seats::ConfigFromEnv)", idle.empty(), Dyn::Join(idle, ", "));
unsetenv("REVIEW_GATES_API_BUDGET_USD");
unsetenv("SEATS_ENV_FILE");
setenv("XDG_CONFIG_HOME", (envTd / "xdg").string().c_str(), 1);
const bool none = !SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo").path && !SeatPool::GlobalEnvFile();
std::ofstream(envTd / "xdg" / "crafter-review" / "env", std::ios::binary) << "REVIEW_GATES_SEAT_ORDER=oauth-first\n";
const std::optional<fs::path> global = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo").path;
// An application's .env (no seat, no placement setting), one whose only
// REVIEW_GATES_* line is no placement setting, one of blank placeholder
// lines (an empty token or setting is none), and a .env that is no
// file (a Python venv) are passed over and recorded so; one with a
// placement setting alone is a seats file, and so is one that is not
// UTF-8 (its error is the command's).
std::ofstream(envTd / "repo" / ".env", std::ios::binary) << "VITE_API_URL=http://localhost:54321\nSUPABASE_ANON_KEY=anon\n";
const SeatPool::SeatsFile appFile = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo");
std::ofstream(envTd / "repo" / ".env", std::ios::binary) << "REVIEW_GATES_SEAT_ORDER=api-first\n";
const std::optional<fs::path> settingsOnly = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo").path;
std::ofstream(envTd / "repo" / ".env", std::ios::binary) << "REVIEW_GATES_SCRATCH=/elsewhere\n";
const SeatPool::SeatsFile otherSetting = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo");
std::ofstream(envTd / "repo" / ".env", std::ios::binary) << "CLAUDE_CODE_OAUTH_TOKEN1=\nANTHROPIC_API_KEY1= \nOPENROUTER_KEY=\nREVIEW_GATES_SEAT_ORDER=\n";
const SeatPool::SeatsFile blanks = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo");
std::ofstream(envTd / "repo" / ".env", std::ios::binary) << std::string("\xff\n", 2);
const SeatPool::SeatsFile notTextFile = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo");
fs::remove(envTd / "repo" / ".env");
fs::create_directories(envTd / "repo" / ".env");
const SeatPool::SeatsFile venv = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo");
fs::remove_all(envTd / "repo" / ".env");
std::ofstream(envTd / "repo" / ".env", std::ios::binary) << "CLAUDE_CODE_OAUTH_TOKEN1=x\n";
const SeatPool::SeatsFile localFile = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo");
setenv("SEATS_ENV_FILE", (envTd / "settings.env").string().c_str(), 1);
const std::optional<fs::path> named = SeatPool::ResolveSeatsFile(std::nullopt, envTd / "repo").path;
const std::optional<fs::path> given = SeatPool::ResolveSeatsFile(std::string("explicit.env"), envTd / "repo").path;
const std::optional<fs::path> givenNone = SeatPool::ResolveSeatsFile(std::string(), envTd / "repo").path;
Check("the seats file resolves as --env-file (an empty one: none), else $SEATS_ENV_FILE, else <cwd>/.env when it is a file holding a seat or a placement setting with a value (an "
"application's .env, one whose only REVIEW_GATES_* line is no placement setting, one of blank lines, and a venv passed over and recorded so; one that is not UTF-8 taken, its read error kept), else $XDG_CONFIG_HOME/crafter-review/env, else none; the global file's settings reach SettingsOf",
none && global == envTd / "xdg" / "crafter-review" / "env" && appFile.path == global && appFile.passedOver == envTd / "repo" / ".env"
&& SeatPool::SettingsOf(appFile).fromFile == std::vector<std::string> {"REVIEW_GATES_SEAT_ORDER"} && settingsOnly == envTd / "repo" / ".env" && otherSetting.path == global && otherSetting.passedOver == envTd / "repo" / ".env" && blanks.path == global && blanks.passedOver == envTd / "repo" / ".env"
&& notTextFile.path == envTd / "repo" / ".env" && notTextFile.error && venv.path == global && venv.passedOver == envTd / "repo" / ".env" && !venv.error
&& localFile.path == envTd / "repo" / ".env" && !localFile.passedOver && named == envTd / "settings.env" && given == fs::path("explicit.env") && !givenNone);
unsetenv("SEATS_ENV_FILE");
unsetenv("XDG_CONFIG_HOME");
fs::remove_all(envTd);
}
fs::remove_all(td);
return Failures == 0 ? 0 : 1;
}