Crafter.Review/tests/StacksMemory/main.cpp

364 lines
27 KiB
C++
Raw Normal View History

// SPDX-License-Identifier: AGPL-3.0-only
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
// Stacks and memory: the slots config, slot.json, free memory, a pass's
// measured cost, the admission, the prompts, the preflight, `slots`.
// app_ready, the database calls, the memory readings and the methods of Run
// are replaced through the Run's seams.
#include <stdlib.h>
#include <unistd.h>
import std;
import Crafter.Review;
namespace fs = std::filesystem;
using namespace Crafter::Review;
namespace {
std::int32_t Failures = 0;
const std::string Fn(Plan::FunctionalLens);
void Check(std::string_view name, bool cond, std::string_view detail = {}) {
std::println("{}{}{}", cond ? " ok " : " FAIL ", name, !cond && !detail.empty() ? std::format(" ({})", detail) : std::string());
if (!cond) ++Failures;
}
void Write(const fs::path& path, std::string_view text) {
fs::create_directories(path.parent_path());
std::ofstream(path, std::ios::binary) << text;
}
bool AnyContains(const std::vector<std::string>& lines, std::string_view part) {
return std::ranges::any_of(lines, [&](const std::string& line) { return line.contains(part); });
}
Json With(Json value, const Json& changes) {
for (auto it = changes.begin(); it != changes.end(); ++it) value[it.key()] = it.value();
return value;
}
}
int main() {
std::println("stacks and memory: the slots config, slot.json, free memory, a pass's measured cost, the admission, the prompts, the preflight, `slots`");
Platform::SetMode(Platform::Mode::Native);
SetHome(fs::current_path());
const fs::path base = fs::temp_directory_path() / std::format("crafter-review-stacks-{}", Platform::PublishedPid());
fs::remove_all(base);
{
const fs::path td = base / "one";
fs::create_directories(td / ".review-gates");
const fs::path p = td / ".review-gates" / "config.json";
auto bad = [&](const Json& d) {
Write(p, DumpSpaced(d));
return Config::Load(td).problems;
};
// Every lens is off by default: a config under test turns the
// bug-prover on, or the one problem it would have is that none runs.
auto sl = [](const Json& kw) { return Json {{"slots", kw}, {"lenses", {{"repo-bug-prover", {{"run", true}}}}}}; };
Check("config: slots default to none; a dir inside the repository, start/stop with {n}, and the measured memMb with it; anything else is a problem",
Dyn::Equal(Config::Defaults().at("slots"), {{"dir", nullptr}, {"start", nullptr}, {"stop", nullptr}, {"memMb", nullptr}, {"startMb", nullptr}})
&& AnyContains(bad(sl({{"dir", "s"}, {"memMb", 1}, {"startMb", 0}})), "slots.startMb")
&& bad(sl({{"dir", ".local-stack/slots"}, {"start", "bash s.sh full --slot {n}"}, {"stop", "bash s.sh stop --slot {n}"}, {"memMb", 1500}})).empty()
&& AnyContains(bad(sl({{"dir", "/abs"}, {"memMb", 1}})), "inside the repository") && AnyContains(bad(sl({{"dir", "s"}, {"start", "bash s.sh"}, {"memMb", 1}})), "{n}")
&& AnyContains(bad(sl({{"dir", "s"}})), "slots.memMb") && AnyContains(bad(sl({{"dir", "s"}, {"memMb", true}})), "slots.memMb") && bad(sl({{"nope", 1}})).size() == 1);
Write(p, R"({"functional": {"concurrency": 4}, "lenses": {"repo-bug-prover": {"run": true}}})");
const Config::Loaded cr = Config::Load(td);
const Config::Loaded cr2 = Config::Load("/", p);
const Json& warnings = cr.config.at("_warnings");
const std::string w0 = warnings.empty() ? std::string() : warnings.at(0).get<std::string>();
Check("config: functional.concurrency is retired - no problem (a freeze or a run goes on), the key dropped, a warning that says what replaced it; the --config path kept; no _warnings in the dump",
cr.problems.empty() && cr2.problems.empty() && warnings.size() == 1 && w0.starts_with("functional.concurrency is no longer read - remove it from config.json:") && w0.contains("free memory")
&& w0.contains("slots") && !cr.config.at("functional").contains("concurrency") && cr2.config.at("_path") == p.string() && !Config::Redacted(cr.config).contains("_warnings")
&& Config::Load(td / "nowhere").config.at("_warnings").empty(),
w0);
const fs::path sd = td / ".local-stack" / "slots";
const std::vector<std::pair<std::string, Json>> slots = {
{"10", {{"appUrl", "http://localhost:21080"}, {"apiUrls", {"http://127.0.0.1:21021"}}, {"dbUrl", "postgresql://postgres:postgres@127.0.0.1:21022/postgres"}, {"login", ".local-stack/slots/10/u.json"}}},
{"2", {{"appUrl", "http://localhost:20280/"}, {"apiUrls", Json::array()}, {"dbUrl", nullptr}, {"login", nullptr}}},
{"1", {{"appUrl", "http://localhost:20180"}, {"dbUrl", "postgresql://u:p@db.example.supabase.co:5432/postgres"}}},
{"3", {{"apiUrls", Json::array()}}},
{"4", nullptr},
};
for (const auto& [n, body] : slots) {
fs::create_directories(sd / n);
if (!body.is_null()) Write(sd / n / "slot.json", DumpSpaced(body));
}
fs::create_directories(sd / "5");
Write(sd / "5" / "slot.json", "{bad");
Write(sd / "10" / "u.json", "{}");
const Json cfg = {{"slots", {{"dir", ".local-stack/slots"}, {"start", "up --slot {n}"}, {"stop", "down --slot {n}"}, {"memMb", 1500}}}};
const Config::Slots found = Config::SlotStacks(cfg, td);
std::vector<std::string> ids;
for (const Json& s : found.stacks) ids.push_back(s.at("id").get<std::string>());
Check("slot_stacks: the slots that are up in slot order (2, 10), the URL without its trailing slash; a hosted database, no appUrl and bad JSON are problems; a dir without slot.json is not up",
ids == std::vector<std::string>{"slot 2", "slot 10"} && found.stacks.at(0).at("appUrl") == "http://localhost:20280" && found.stacks.at(1).at("login") == ".local-stack/slots/10/u.json"
&& found.stacks.at(1).at("dbUrl").get<std::string>().ends_with(":21022/postgres") && std::ranges::all_of(found.stacks, [](const Json& s) { return s.at("slot") == true; })
&& found.problems.size() == 3 && AnyContains(found.problems, "slots/1/slot.json: dbUrl") && AnyContains(found.problems, "slots/3/slot.json: no appUrl")
&& AnyContains(found.problems, "slots/5/slot.json: not JSON"),
Dyn::Join(found.problems, " | "));
const std::vector<std::int64_t> threeFour = {3, 4};
const std::vector<std::int64_t> justOne = {1};
Check("slot_commands: the start and stop commands for the slots named; none without a command",
Config::SlotCommands(cfg, "start", threeFour) == std::vector<std::string>{"up --slot 3", "up --slot 4"} && Config::SlotCommands({{"slots", {{"stop", nullptr}}}}, "stop", justOne).empty());
// Memory: what is free, the reserve, a tree's PSS, the sampler, the
// calibration.
const fs::path mi = td / "meminfo";
Write(mi, "MemTotal: 131371024 kB\nMemFree: 3000000 kB\nMemAvailable: 82119216 kB\nHugePages_Total: 0\n");
Check("memory: MemAvailable and MemTotal in MB; the reserve is 10 % of MemTotal (at least 2048 MB) unless REVIEW_GATES_MEM_RESERVE_MB says otherwise; no file, no number",
Memory::Available(mi) == 80194 && Platform::MemInfo(mi).at("MemTotal") == 128292 && Memory::Reserve(std::nullopt, mi) == 12829 && Memory::Reserve(std::string("4096"), mi) == 4096
&& Memory::Reserve(std::nullopt, td / "none") == 2048 && !Memory::Available(td / "none"));
auto raised = [&](std::string_view value) {
try {
Memory::Reserve(std::string(value), mi);
return std::string("nothing");
} catch (const Dyn::Error& e) {
return std::format("{}: {}", e.Kind(), e.what());
}
};
Check("memory: a reserve that is no number raises (exit 1); one past int64 saturates; every number spelling is taken",
raised("x") == "ValueError: could not convert string to float: 'x'" && raised("nan") == "ValueError: cannot convert float NaN to integer"
&& raised("-inf") == "OverflowError: cannot convert float infinity to integer" && Memory::Reserve(std::string("1e30"), mi) == std::numeric_limits<std::int64_t>::max()
&& Memory::Reserve(std::string(" 4_096.9 "), mi) == 4096,
raised("x") + raised("nan"));
const std::int64_t me = getpid();
const std::vector<std::int64_t> tree = Platform::Tree(me, Platform::PpidMap());
const std::vector<std::int64_t> gone = {(1 << 22) + 7};
Check("memory: a process tree's PSS - this process measures above zero, a pid that is gone measures zero", Platform::PssMb(tree) > 1 && Platform::PssMb(gone) == 0 && std::ranges::contains(tree, me));
std::deque<double> vals = {100.0, 400.0, 250.0};
Memory::Sampler sm(3600, [&](std::int64_t, const auto&) {
const double v = vals.front();
vals.pop_front();
return v;
}, [] { return std::map<std::int64_t, std::vector<std::int64_t>> {}; });
sm.Watch("a", 1);
sm.Sample();
sm.Sample();
sm.Sample();
const std::optional<double> peak = sm.Done("a");
Check("Sampler: the peak of the samples of a watched pass, and nothing once it is done", peak == 400.0 && !sm.Done("a"));
Memory::Calibration cal(td / "memory.json", td, {{"none", 300}, {"browser", 900}});
const Memory::Calibration::Cost d0 = cal.CostOf("browser");
cal.Record("browser", 500.0);
cal.Record("browser", 1200.0);
for (std::size_t i = 0; i < Memory::Keep; ++i) cal.Record("browser", 700.0);
const Memory::Calibration other(td / "memory.json", td / "elsewhere", {{"none", 300}, {"browser", 900}});
Check("Calibration: the measured default before any peak; then the highest of the last 30 peaks of this repository and environment (an old outlier ages out); another repository keeps its own",
d0.megabytes == 900 && d0.source == "the default measured 2026-10-22; none measured in this repository yet" && cal.CostOf("browser").megabytes == 700.0 && cal.Peaks("browser").size() == Memory::Keep
&& other.CostOf("browser").megabytes == 900 && cal.CostOf("none").megabytes == 300);
}
// Stacks: exclusive, least recently returned first, the preferred one
// only.
{
Pool::Stacks st({{{"id", "s1"}}, {{"id", "s2"}}, {{"id", "s3"}}});
const Json a = st.Take();
const Json b = st.Take();
st.Give(a.at("id"));
const bool third = st.Take().at("id") == "s3";
const bool busy = !st.Can(std::string("s2"));
const bool back = st.Can(std::string("s1"));
const bool preferred = st.Take(std::string("s1")).at("id") == "s1";
const bool none = !st.Can();
st.Give("s2");
Check("Stacks: two takes are two stacks; a returned one goes to the back; prefer takes that one or reports it busy",
a.at("id") == "s1" && b.at("id") == "s2" && third && busy && back && preferred && none && st.Can());
}
// Admission: stacks, the memory budget, the machine's free memory now,
// nothing in flight.
{
std::vector<std::string> logs;
double free = 10'000;
Pool::Stacks st2({{{"id", "slot 1"}}});
Pool::Admission adm([](const Json& j) { return j.at("mb").get<double>(); }, [](const Json& j) { return Dyn::Truthy(Dyn::Get(j, "app")); }, &st2, 1000.0, 500,
[&]() -> std::optional<double> { return free; }, [&](const std::string& line) { logs.push_back(line); });
Json j1 = {{"label", "a"}, {"mb", 600}, {"app", true}};
const Json j2 = {{"label", "b"}, {"mb", 600}};
const Json j3 = {{"label", "c"}, {"mb", 300}, {"app", true}};
const bool first = adm.Can(j1);
adm.Take(j1);
Check("Admission: a pass with an application takes a stack of its own and its cost; the next one waits for the stack, one over the budget waits for memory",
first && j1.at("stack").at("id") == "slot 1" && j1.at("prevStack") == "slot 1" && !adm.Can(j3) && !adm.Can(j2) && adm.Held() == 600 && adm.Inflight() == 1);
const Json j4 = {{"label", "d"}, {"mb", 300}};
free = 700;
const bool low = adm.Can(j4);
free = 10'000;
Check("Admission: inside the budget but the machine has less than cost + reserve free right now -> wait; with room -> go", !low && adm.Can(j4));
adm.Give(j1);
Json big = {{"label", "e"}, {"mb", 5000}};
const bool alone = adm.Can(big);
adm.Take(big);
Check("Admission: given back, the stack and the memory are free again; with nothing in flight a pass over the budget runs alone, and the log says so once",
st2.Can() && adm.Held() == 5000 && alone && logs.size() == 1 && logs.front().contains("launched alone") && !j1.contains("stack"));
adm.Give(big);
Pool::Stacks st3({{{"id", "slot 1"}}, {{"id", "slot 2"}}});
Pool::Admission adm3([](const Json&) { return 1.0; }, [](const Json&) { return true; }, &st3);
Json taken = {{"label", "x"}};
adm3.Take(taken);
Json rj = {{"label", "r"}, {"resumeSession", "sid"}, {"prevStack", "slot 1"}};
const bool waits = !adm3.Can(rj);
adm3.Give(taken);
const bool can = adm3.Can(rj);
adm3.Take(rj);
Check("Admission: a resumed pass waits for the stack its transcript ran on, not for any", waits && can && rj.at("stack").at("id") == "slot 1");
}
// The prompts: the shared block names no stack in slot mode; the pass's
// own goes into its user message.
const Json fcfg = {{"appUrl", "http://localhost:8080"}, {"apiUrls", {"http://127.0.0.1:54321"}}, {"login", {{"notes", "TOTP enrolled"}}}};
const std::string abSlots = Prompt::AppBlock(fcfg, std::string(".local-stack/test-user.json"), std::string("/p/bin/crafter-review totp .local-stack/test-user.json"), true);
const std::string abOne = Prompt::AppBlock(fcfg, std::string(".local-stack/test-user.json"), std::string("/p/bin/crafter-review totp .local-stack/test-user.json"));
Check("app block with slots: a stack of this pass's own, STACK FOR THIS PASS named, no URL, origin, fixture or TOTP command in the shared block; the rules: yours alone, not reset between passes",
abSlots.contains("a stack of this pass's own") && abSlots.contains("STACK FOR THIS PASS") && !abSlots.contains("localhost:8080") && !abSlots.contains("54321") && !abSlots.contains("test-user.json")
&& !abSlots.contains("/p/bin/crafter-review") && abSlots.contains("TOTP enrolled") && abSlots.ends_with(Prompt::ShotRules) && abSlots.contains("not reset between passes") && abOne.contains("localhost:8080")
&& abOne.contains("test-user.json") && abOne.contains("not reset between passes") && !abOne.contains("STACK FOR THIS PASS"));
const Json stk = {{"id", "slot 2"}, {"appUrl", "http://localhost:20280"}, {"apiUrls", {"http://127.0.0.1:20221"}}, {"dbUrl", "postgresql://postgres:secret@127.0.0.1:20222/postgres"},
{"login", ".local-stack/slots/2/test-user.json"}};
const std::string stackLogin = stk.at("login").get<std::string>();
const std::string tx = Prompt::StackText(stk, Pool::TotpCommand(stackLogin));
const Json seven = {{"maxTurns", 7}};
const std::string up = Prompt::UserPrompt("TAIL", seven, tx);
const std::string up0 = Prompt::UserPrompt("TAIL", seven);
Check("user prompt: the pass's stack - its URL, origins, login and exact TOTP command, never its database URL - between the scope and the budget line; without one the message is what it was",
up.find("TAIL") < up.find("STACK FOR THIS PASS (slot 2, yours alone while this pass runs)") && up.find("STACK FOR THIS PASS (slot 2, yours alone while this pass runs)") < up.find("Tool-call budget")
&& up.contains("http://localhost:20280") && up.contains("20221") && up.contains(std::format("`{}`", *Pool::TotpCommand(stackLogin))) && !up.contains("20222") && !up.contains("secret")
&& up0 == std::format("TAIL{}\n\nReturn the structured output described in your instructions.\n", Prompt::BudgetLine(seven)) && Prompt::StackText(With(stk, {{"login", nullptr}}), std::nullopt).contains("no login fixture"));
Check("functional block hint: the stack is the pass's own and not reset between passes",
Prompt::FunctionalBlockHint.contains("yours alone while this pass runs, but it is not reset between passes") && !Prompt::FunctionalBlockHint.contains("at the same time"));
{
const fs::path td = base / "runner";
fs::create_directories(td);
PgWire::Broker broker;
broker.Start();
const Pool::Runner r({.repo = fs::current_path(), .scratch = td, .bin = "/bin/true", .effort = "xhigh", .log = [](const std::string&) {}, .dryRun = true,
.functional = {{"mcp", {{"command", "npx"}, {"args", nullptr}}}, {"extraTools", Json::array()}}, .loginFixture = ".local-stack/test-user.json",
.environments = {{Fn, "browser+supabase"}, {"repo-bug-prover.md", "supabase"}}, .dbUrl = "postgresql://postgres:postgres@127.0.0.1:54322/postgres", .broker = &broker});
const std::vector<std::string> here = {"."};
auto allowed = [](const std::vector<std::string>& c) { return c[static_cast<std::size_t>(std::ranges::find(c, "--allowedTools") - c.begin()) + 1]; };
const std::string al2 = allowed(r.Cmd({{"label", "f"}, {"model", "m"}, {"lensFile", Fn}, {"kind", "block"}, {"stack", stk}}, "/dev/null", "{}", here, 20, false, "sid"));
const std::string al1 = allowed(r.Cmd({{"label", "f"}, {"model", "m"}, {"lensFile", Fn}, {"kind", "block"}}, "/dev/null", "{}", here, 20, false, "sid"));
Check("runner: a pass on a slot may run the TOTP command of that slot's login alone; a pass without a stack keeps the run's fixture",
al2.contains(std::format("Bash({}:*)", *Pool::TotpCommand(stackLogin))) && !Dyn::Replace(al2, stackLogin, "").contains("test-user.json:*")
&& al1.contains(std::format("Bash({}:*)", *Pool::TotpCommand(".local-stack/test-user.json"))));
const std::string brokerEnv(PgWire::BrokerEnv);
auto dbOf = [&](const std::map<std::string, std::string>& e) {
const std::string v = e.at(brokerEnv);
return broker.UrlOf(v.substr(v.rfind('#') + 1)).value_or("");
};
Check("runner: the database a pass reaches is its stack's; a pass with the database alone reaches the run's first",
dbOf(r.ProcessEnv({{"lensFile", Fn}, {"stack", stk}}, {})) == stk.at("dbUrl").get<std::string>() && dbOf(r.ProcessEnv({{"lensFile", "repo-bug-prover.md"}}, {})).ends_with(":54322/postgres"));
broker.Close();
}
// The preflight: slots up, every stack answers, its login in its
// auth.users.
{
const fs::path td = base / "preflight";
fs::create_directories(td);
Run::Run g;
std::vector<std::string> logs;
g.repo = td;
g.log = [&](const std::string& line) { logs.push_back(line); };
g.cfg = {{"functional", {{"start", "make run"}, {"readyPath", "/"}, {"exclude", Json::array()}}},
{"supabase", {{"dbUrl", "postgresql://postgres:postgres@127.0.0.1:1/postgres"}, {"start", nullptr}}},
{"slots", {{"dir", ".local-stack/slots"}, {"start", "up --slot {n}"}, {"stop", nullptr}, {"memMb", 1500}}}};
g.environments = {{Fn, "browser+supabase"}, {"repo-bug-prover.md", "supabase"}};
g.a.dryRun = false;
g.a.harness = "claude";
g.trees = {"."};
auto pre = [&](const std::vector<std::string>& lenses) -> std::pair<std::string, std::string> {
try {
g.StackPreflight(lenses);
return {"ok", logs.empty() ? std::string() : logs.back()};
} catch (const Run::Preflight& e) {
return {"3", std::format("preflight: {}\n", e.what())};
}
};
g.slotMode = true;
g.stacks = std::make_unique<Pool::Stacks>(std::vector<Json> {{{"id", "slot 1 (dry run: none is up)"}, {"appUrl", "http://x"}, {"apiUrls", Json::array()}, {"dbUrl", nullptr}, {"login", nullptr}, {"slot", true}}});
const auto noneUp = pre({Fn});
g.slotProblems = {".local-stack/slots/3/slot.json: no appUrl"};
const auto broken = pre({Fn});
g.slotProblems.clear();
Write(td / "l" / "u.json", R"({"email": "A@x.test", "password": "p"})");
std::vector<Json> two;
for (const std::int32_t n : {1, 2}) {
two.push_back({{"id", std::format("slot {}", n)}, {"appUrl", std::format("http://localhost:2{}180", n)}, {"apiUrls", Json::array()},
{"dbUrl", std::format("postgresql://postgres:postgres@127.0.0.1:2{}122/postgres", n)}, {"login", "l/u.json"}, {"slot", true}});
}
g.stacks = std::make_unique<Pool::Stacks>(two);
std::vector<std::string> answered;
std::vector<std::string> probed;
Run::QueryResult::second_type users = {{"x"}};
g.appReady = [&](std::string_view url, std::string_view) {
answered.emplace_back(url);
return std::pair<bool, std::string>(true, std::format("HTTP 200 from {}", url));
};
g.probe = [&](std::string_view url) {
probed.emplace_back(url);
return std::pair<bool, std::string>(true, "postgres on 127.0.0.1 as postgres - PostgreSQL 17.6");
};
g.query = [&](std::string_view, std::string_view) { return Run::QueryResult({"?"}, users); };
g.bypassesRls = [](std::string_view) { return std::optional<bool>(true); };
g.migrationsDrift = [](const std::string&) { return std::pair<std::optional<std::string>, std::optional<std::string>>(std::nullopt, "its 2 applied migrations are the range HEAD's"); };
const auto good = pre({Fn});
const std::vector<std::string> appSeen = answered;
const std::vector<std::string> dbSeen = probed;
answered.clear();
probed.clear();
const auto onlyDb = pre({"repo-bug-prover.md"});
const std::vector<std::string> dbOnlySeen = probed;
users.clear();
const auto stale = pre({Fn});
users.push_back({"x"});
g.appReady = [](std::string_view url, std::string_view) { return std::pair<bool, std::string>(!url.contains(":22180"), std::format("connection refused by {}", url)); };
const auto down = pre({Fn});
Check("preflight: slots configured and none up stops the run and names `<invocation> slots`; a broken slot.json stops it",
noneUp.first == "3" && noneUp.second.contains("no slot is up under .local-stack/slots") && noneUp.second.contains(std::format("`{} slots`", Invocation())) && broken.first == "3"
&& broken.second.contains("slots/3/slot.json"),
noneUp.second + broken.second);
Check("preflight: a lens that drives the application checks every stack - its app, its database, its migrations; a lens with the database alone checks the first",
good.first == "ok" && appSeen.size() == 2 && dbSeen.size() == 2 && dbOnlySeen == std::vector<std::string>{two.front().at("dbUrl").get<std::string>()} && good.second.contains("2 slot(s) under .local-stack/slots")
&& g.dbChecked && g.dbChecked->starts_with("the database of each of the 2 stacks answers") && onlyDb.first == "ok",
good.second);
Check("preflight: a stack whose login is not in its auth.users stops the run (a fixture left from another bring-up); an app that does not answer names its slot and the start command",
stale.first == "3" && stale.second.contains("slot 1: its login l/u.json (a@x.test) is not in that stack's auth.users") && down.first == "3" && down.second.contains("slot 2: the application does not answer")
&& down.second.contains("(up --slot {n})"),
stale.second + down.second);
// exec_job: the stack the admission gave, the measured peak into the
// calibration.
const fs::path fx = fs::current_path() / "tests" / "Goldens" / "fixture" / "commits";
g.a.artifact = fx.string();
g.a.dryRun = false;
g.a.effort = "xhigh";
g.a.noSalvage = true;
g.hash = Artifact::ArtifactHash("commits", fx);
g.testMode = true;
g.schemas = {{"block", "{}"}, {"whole", "{}"}};
g.treeDrift = [] { return std::optional<std::string>(); };
g.promptFor = [](const Json&) { return std::pair<fs::path, std::string>("/dev/null", "PROMPT"); };
g.calibration = std::make_unique<Memory::Calibration>(td / "memory.json", td, std::map<std::string, std::int64_t> {{"none", 300}, {"browser+supabase", 900}});
g.runPass = [](const Json& job, const std::string&, const fs::path&, std::string_view, std::int64_t, std::int64_t, bool) {
return std::pair<Json, Json>({{"label", job.at("label")}, {"status", "ok"}, {"memPeakMb", 812.5}}, nullptr);
};
const auto [rec, raw] = g.ExecJob({{"label", "f1"}, {"lens", "repo-functional"}, {"lensFile", Fn}, {"kind", "block"}, {"model", "m"}, {"stack", g.stacks->All().at(1)}}, 1);
Check("exec_job: the record names the pass's stack, and its measured peak becomes its environment's calibration", rec.at("stack") == "slot 2" && g.calibration->CostOf("browser+supabase").megabytes == 812.5);
// `slots`: as many as free memory holds, no more than the range's
// browser passes, the commands for the missing ones.
Write(td / ".local-stack" / "slots" / "1" / "slot.json", R"({"appUrl": "http://localhost:20180", "apiUrls": [], "dbUrl": null, "login": null})");
Json cfg = Config::Load(td).config;
cfg["slots"] = {{"dir", ".local-stack/slots"}, {"start", "up --slot {n}"}, {"stop", nullptr}, {"memMb", 1500}, {"startMb", 6000}};
cfg["lenses"]["repo-functional"]["run"] = true;
cfg["lenses"]["repo-functional"]["environment"] = "browser+supabase";
std::vector<std::string> out;
const Run::Machine machine = {.available = [] { return std::optional<std::int64_t>(20'000); }, .reserve = [] { return std::int64_t {4000}; }};
auto sink = [&](const std::string& line) { out.push_back(line); };
const auto [want, cmds] = Run::SlotsPlan(cfg, td, std::nullopt, td, sink, machine);
const auto [want2, cmds2] = Run::SlotsPlan(cfg, td, fx, td, sink, machine);
std::vector<std::string> expected;
for (std::int32_t n = 2; n < 8; ++n) expected.push_back(std::format("up --slot {}", n));
// 20000 free - 4000 reserve - 1 slot up x 812.5 = 15187.5 for (1500 +
// 812.5) each -> 6 more.
Check("slots: room = free - reserve - the passes of the slots up, each new slot at memMb plus one measured pass -> 6 more, numbered after the ones up; with the fixture range no more than its 3 browser block passes",
want == 7 && cmds == expected && want2 == 3 && cmds2 == std::vector<std::string>{"up --slot 2", "up --slot 3"} && AnyContains(out, "room for 6 more by memory"), Dyn::Join(out, " | "));
Check("slots: no more start at once than free memory holds at slots.startMb each (15187 MB / 6000 -> 2), and one at a time without it", AnyContains(out, "at most 2 at once (slots.startMb 6000 MB each"),
Dyn::Join(out, " | "));
}
fs::remove_all(base);
return Failures == 0 ? 0 : 1;
}