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.
307 lines
26 KiB
C++
307 lines
26 KiB
C++
// 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);
|
|
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",
|
|
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, "; "));
|
|
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": 28, "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)));
|
|
|
|
// 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"}, {"CLAUDE_CODE_OAUTH_TOKEN", "t"}});
|
|
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",
|
|
Dyn::Str(api) + " " + raised);
|
|
// 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;
|
|
}
|