fix: three errors that ended with the wrong exit code

Found compiling the environment-variable table for AGENTS.md:
- A REVIEW_GATES_MEM_RESERVE_MB that is no number threw
  std::invalid_argument, which nothing caught: the process aborted where
  it should end with a ValueError, exit 1. nan and inf were an unchecked
  cast. They raise ValueError / OverflowError now; a value past int64
  saturates.
- An env file that is not UTF-8: `run` ended with exit 1 where its
  preflight should stop it (exit 3), and `seats` parsed the raw bytes. Both
  read it as strict UTF-8 text now.
- A broker address with a port past 65535 was cut to 16 bits; it is an
  OverflowError now, uncaught (exit 1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Jorijn van der Graaf 2026-10-31 16:35:26 +01:00
commit 53b7d6de31
Signed by: jorijnvdgraaf
GPG key ID: 2937E59CDCC1BCFB
9 changed files with 58 additions and 14 deletions

View file

@ -212,10 +212,13 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"ok","total
json.out.substr(0, 600) + json.err);
const Process::Captured none = Self({"seats"}, base / "plain", withFake);
const Process::Captured noFile = Self({"seats", "--env-file", "missing.env"}, base / "plain", withFake);
Check("seats: no seat configured is exit 1 with what it looked for; a missing env file is exit 1 with the read error",
Write(base / "plain" / "bad.env", std::string("\xff\n", 2));
const Process::Captured badFile = Self({"seats", "--env-file", "bad.env"}, base / "plain", withFake);
Check("seats: no seat configured is exit 1 with what it looked for; a missing env file and one that is not UTF-8 are exit 1 with the read error",
none.returnCode == 1 && none.out.starts_with("no seat configured (CLAUDE_CODE_OAUTH_TOKEN1..6 / ANTHROPIC_API_KEY1..6; --local probes your own login)")
&& noFile.returnCode == 1 && noFile.err.contains("FileNotFoundError: [Errno 2] No such file or directory: 'missing.env'"),
none.out + noFile.err);
&& noFile.returnCode == 1 && noFile.err.contains("FileNotFoundError: [Errno 2] No such file or directory: 'missing.env'") && badFile.returnCode == 1
&& badFile.err.contains("UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"),
none.out + noFile.err + badFile.err);
// browser: its usage (an install fetches from npm).
const Process::Captured noAction = Self({"browser"}, base);

View file

@ -238,6 +238,14 @@ int main() {
&& out[0] == std::format("{} - this pass has no database (the driver gives one to a pass with the supabase environment)", PgWire::NoDatabase)
&& out[1] == std::format("{} - the driver's SQL broker does not answer (invalid literal for int() with base 10: 'x')", PgWire::NoDatabase)
&& out[2].ends_with("does not answer ([Errno 111] Connection refused)"), Show(out));
std::string overflow;
try {
PgWire::CallBroker("127.0.0.1:70000#t", "x", false, collect);
} catch (const Dyn::Error& e) {
overflow = std::format("{}: {}", e.Kind(), e.what());
}
Check("call_broker: a port past 65535 is an OverflowError, which call_broker does not catch (exit 1)",
overflow == "OverflowError: getsockaddrarg: port must be 0-65535.", overflow);
broker.Close();
out.clear();
Check("broker: closed, its port answers no more", PgWire::CallBroker(address, "select 1", false, collect) == 2 && out.at(0).ends_with("([Errno 111] Connection refused)"), Show(out));

View file

@ -358,6 +358,12 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\"
dump.is_object() ? Text(dump.at("stoppedAt")) : Read(base / std::format("{}.err", name))));
}
// An env file that is not UTF-8: the preflight stops the run.
Write(base / "bad.env", std::string("\xff\n", 2));
const Process::Captured badEnv = run({"--no-reuse", "--no-salvage", "--env-file", (base / "bad.env").string()});
Check("preflight: an env file that is not UTF-8 stops the run, exit 3, with the decoder's own words",
badEnv.returnCode == 3 && badEnv.err.contains("preflight: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"), badEnv.err);
// A stop between two gates: the next gate's pool starts aborted and
// runs nothing of it.
{

View file

@ -104,6 +104,19 @@ int main() {
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};