Crafter.Review/tests/StacksMemory/main.cpp

364 lines
27 KiB
C++
Raw Permalink 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.
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort The config defines the modes (modes.<name>): the kind of artifact the mode reviews - reply, series or commits, the three shipped partitions; a kind is a mode of its own name - the lenses it runs, in its kind's gate order, and the effort it runs at, else the kind's default (max for a reply or a series, xhigh for a commit range). `run --mode` names any of them, the dump records mode and kind, and everything that decided by the mode's name - the partition and hash, the prompts, the schemas, the disposition and reuse-key rules, what the CLI requires - decides by the kind. Every lens stays off until a mode lists it; lenses.<name>.run is retired with a warning that says so, and a lens of another kind, a name that is no lens, a lens listed twice and an unknown effort are config problems. The defaults run no lens; the template ships the three kinds as modes with every lens of their kind at the kind's effort, to trim (the owner's ruling of 2026-11-01), and init says so as it writes it - a kept config is the repository's own, and only one that lists no lens gets a step. The readers of the retired key follow: the CI smoke config, the shipped skill, the procedure (its description of the config as well as its mechanics), the manifest template, the run's messages and the comments that named the three modes. The run lock, the run numbers and the dump names are the kind's, so one full run per hash holds across the modes of a kind and their rounds count on together; the dump says which mode ran. The database broker starts for a lens the mode runs, as every other reader of the lenses off reads them. A named mode runs through the CLI in the kernel-modes test.
2026-11-01 01:31:40 +01:00
auto sl = [](const Json& kw) { return Json {{"slots", kw}, {"modes", {{"commits", {{"lenses", {"repo-bug-prover"}}}}}}}; };
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);
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort The config defines the modes (modes.<name>): the kind of artifact the mode reviews - reply, series or commits, the three shipped partitions; a kind is a mode of its own name - the lenses it runs, in its kind's gate order, and the effort it runs at, else the kind's default (max for a reply or a series, xhigh for a commit range). `run --mode` names any of them, the dump records mode and kind, and everything that decided by the mode's name - the partition and hash, the prompts, the schemas, the disposition and reuse-key rules, what the CLI requires - decides by the kind. Every lens stays off until a mode lists it; lenses.<name>.run is retired with a warning that says so, and a lens of another kind, a name that is no lens, a lens listed twice and an unknown effort are config problems. The defaults run no lens; the template ships the three kinds as modes with every lens of their kind at the kind's effort, to trim (the owner's ruling of 2026-11-01), and init says so as it writes it - a kept config is the repository's own, and only one that lists no lens gets a step. The readers of the retired key follow: the CI smoke config, the shipped skill, the procedure (its description of the config as well as its mechanics), the manifest template, the run's messages and the comments that named the three modes. The run lock, the run numbers and the dump names are the kind's, so one full run per hash holds across the modes of a kind and their rounds count on together; the dump says which mode ran. The database broker starts for a lens the mode runs, as every other reader of the lenses off reads them. A named mode runs through the CLI in the kernel-modes test.
2026-11-01 01:31:40 +01:00
Write(p, R"({"functional": {"concurrency": 4}, "modes": {"commits": {"lenses": ["repo-bug-prover"]}}})");
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}};
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort The config defines the modes (modes.<name>): the kind of artifact the mode reviews - reply, series or commits, the three shipped partitions; a kind is a mode of its own name - the lenses it runs, in its kind's gate order, and the effort it runs at, else the kind's default (max for a reply or a series, xhigh for a commit range). `run --mode` names any of them, the dump records mode and kind, and everything that decided by the mode's name - the partition and hash, the prompts, the schemas, the disposition and reuse-key rules, what the CLI requires - decides by the kind. Every lens stays off until a mode lists it; lenses.<name>.run is retired with a warning that says so, and a lens of another kind, a name that is no lens, a lens listed twice and an unknown effort are config problems. The defaults run no lens; the template ships the three kinds as modes with every lens of their kind at the kind's effort, to trim (the owner's ruling of 2026-11-01), and init says so as it writes it - a kept config is the repository's own, and only one that lists no lens gets a step. The readers of the retired key follow: the CI smoke config, the shipped skill, the procedure (its description of the config as well as its mechanics), the manifest template, the run's messages and the comments that named the three modes. The run lock, the run numbers and the dump names are the kind's, so one full run per hash holds across the modes of a kind and their rounds count on together; the dump says which mode ran. The database broker starts for a lens the mode runs, as every other reader of the lenses off reads them. A named mode runs through the CLI in the kernel-modes test.
2026-11-01 01:31:40 +01:00
cfg["modes"]["commits"]["lenses"].push_back("repo-functional");
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;
}