StoreRules (23 checks), StacksMemory (24) and ReviewFixes (19), each a test of its own. Run and SlotsPlan now have seams that start as the real ones: appReady, probe, query, bypassesRls, migrationsAtHead, migrationsDrift, treeDrift, promptFor, runPass, resumable, instrumentPreflight, and a Machine for SlotsPlan. MigrationsAtHead is split out of MigrationsDrift for it; the HEAD side is still worked out once per tree and base. The Postgres stand-in and the raw broker call move to tests/Common, shared by PgWire and ReviewFixes. ReviewFixes runs this binary as `sql`, and a real server that resets every connection makes the startup message either fail to go out or never get its answer - a DbError either way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
348 lines
26 KiB
C++
348 lines
26 KiB
C++
// 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;
|
|
};
|
|
auto sl = [](const Json& kw) { return Json {{"slots", kw}}; };
|
|
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({{"slots", {{"nope", 1}}}}).size() == 1);
|
|
Write(p, R"({"functional": {"concurrency": 4}})");
|
|
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"));
|
|
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"]["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;
|
|
}
|