feat(seatpool): the seat pool and the board several drivers share
The seats of .env and the ledger, the probe, placement, settling a finished pass, the child's credentials, the OpenRouter seats, and the board every driver on the machine shares, in one shape and under one lock. A driver is alive while its lifetime lock is held; with no lock file natively its pid and start time decide, and in the sandbox it is taken for alive. :Http is the HTTP the driver needs, on FetchHTTP1: a status outside 2xx is HttpError, every other failure carries a fixed text. :Platform gains MakeDirs, with the last directory's own mode, and errno texts for its writes and locks. The tests cover the seat pool and several drivers at once, less the checks that drive the pool's Scheduler, Admission and Stacks. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
8ffbd6cf5d
commit
c63c898833
10 changed files with 2562 additions and 8 deletions
167
tests/SeatPool/main.cpp
Normal file
167
tests/SeatPool/main.cpp
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// 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({}, envFile, "", &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({}, std::nullopt, "")) == "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 pool's default projection is 0.25% of the 5-hour window per process (measured 0.08, 2026-10-15); the env overrides",
|
||||
Dyn::Equal(SeatPool::PoolConfig({}).at("lensCostPct"), 0.25) && Dyn::Equal(SeatPool::PoolConfig({{"REVIEW_GATES_LENS_COST_PCT", "5"}}).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")));
|
||||
|
||||
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)));
|
||||
|
||||
fs::remove_all(td);
|
||||
return Failures == 0 ? 0 : 1;
|
||||
}
|
||||
Loading…
Reference in a new issue