fix: what an audit of the documented behaviour found
An independent check of every behaviour AGENTS.md was to describe: - `--units ,` and `--only ,` skipped their empty items and so had no filter left: a typo started a run of every unit. They are usage errors now (exit 3). - Two fixes the plan lists were missing: a dry run now clears the prompts a dry run before it left in the dir they share (the report counted them), and `config` reports an absolute functional.mcp.command that is no program here - /usr/bin/npx inside the Flatpak, whose /usr is the Sdk's. - A patch that is not UTF-8 stops `run` at preflight (exit 3), as the plan has it; it exited 1. - An exception no command caught aborted the process (134), e.g. a home that holds no lenses: it ends with its message and exit 1. An error escaping `run` or `freeze` now also clears the signal handler's run and sweeps what is left below the subreaper. - `config -h` printed "unrecognized arguments" (exit 3): it prints the usage, exit 0. - An API seat's .claude.json is linked into place, so a second writer never replaces the first's file and what claude wrote to it since. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
53b7d6de31
commit
32a0de1a07
7 changed files with 114 additions and 11 deletions
|
|
@ -220,6 +220,15 @@ echo '{"type":"result","subtype":"success","is_error":false,"result":"ok","total
|
|||
&& badFile.err.contains("UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte"),
|
||||
none.out + noFile.err + badFile.err);
|
||||
|
||||
// What escapes a command ends it with exit 1, never an abort; config's
|
||||
// help.
|
||||
const Process::Captured noHome = Self({"doc", "prepush"}, base, {{"CRAFTER_REVIEW_HOME", (base / "nowhere").string()}});
|
||||
const Process::Captured configHelp = Self({"config", "-h"}, base);
|
||||
Check("an error no command catches ends it with exit 1 and its message, never an abort; config -h prints its usage, exit 0",
|
||||
noHome.returnCode == 1 && noHome.err.starts_with("crafter-review: ") && noHome.err.contains("holds no process/lenses") && configHelp.returnCode == 0
|
||||
&& configHelp.out.starts_with("usage: crafter-review config [-h]"),
|
||||
noHome.err + configHelp.out + configHelp.err);
|
||||
|
||||
// browser: its usage (an install fetches from npm).
|
||||
const Process::Captured noAction = Self({"browser"}, base);
|
||||
const Process::Captured badAction = Self({"browser", "remove"}, base);
|
||||
|
|
|
|||
|
|
@ -64,6 +64,12 @@ int main() {
|
|||
&& Any(problems, "aiTrailers") && Any(problems, "authors"), Show(problems));
|
||||
problems = bad({{"functional", {{"extraTools", {"rm -rf /"}}, {"mcp", {{"args", "x"}}}}}});
|
||||
Check("extraTools must be Bash(<prefix>:*) or mcp__...; mcp.args a list", problems.size() == 2, Show(problems));
|
||||
// A path found on the host is not there inside the Flatpak.
|
||||
const std::vector<std::string> missingCommand = bad({{"functional", {{"mcp", {{"command", "/nonexistent/bin/npx"}}}}}});
|
||||
Check("functional.mcp.command: an absolute path that is no program here is a problem; a name, or a program that is there, is not",
|
||||
missingCommand.size() == 1 && missingCommand[0].starts_with("functional.mcp.command '/nonexistent/bin/npx' is no program here")
|
||||
&& bad({{"functional", {{"mcp", {{"command", "/bin/sh"}}}}}}).empty() && bad({{"functional", {{"mcp", {{"command", "npx"}}}}}}).empty(),
|
||||
Show(missingCommand));
|
||||
std::ofstream(p, std::ios::binary) << "{not json";
|
||||
Check("not JSON -> one problem", load().problems.size() == 1);
|
||||
Check("--config to a missing file is a problem", Any(Config::Load(td, td / "none.json").problems, "no such file"));
|
||||
|
|
|
|||
|
|
@ -264,6 +264,16 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\"
|
|||
dry.returnCode == 2 && all > gate1 && gate1 > 1 && !securityUnit.empty() && !proverUnit.empty() && calls() == 0 && Count(planned, "lens", "repo-functional") == 0,
|
||||
dry.out.substr(dry.out.size() > 2000 ? dry.out.size() - 2000 : 0) + dry.err);
|
||||
|
||||
// Dry runs with no full run between them share one dir: the next
|
||||
// drops the last one's prompts.
|
||||
const fs::path dryDir = scratchRoot / std::format("{}-run-1-dry", h8);
|
||||
Write(dryDir / "prompts" / "stale.txt", "a prompt of another range");
|
||||
const Process::Captured again = run({"--dry-run"});
|
||||
std::int64_t prompts = 0;
|
||||
for (const fs::directory_entry& entry : fs::directory_iterator(dryDir / "prompts")) prompts += entry.path().extension() == ".txt" ? 1 : 0;
|
||||
Check("dry run: a second dry run on a hash counts its own prompts, the one before's gone", again.returnCode == 2 && !fs::exists(dryDir / "prompts" / "stale.txt") && prompts == all,
|
||||
std::to_string(prompts));
|
||||
|
||||
// Killed in its last gate: one security pass never answers, and the
|
||||
// driver is SIGKILLed once every other pass has.
|
||||
const fs::path scratchA = scratchRoot / std::format("{}-run-1", h8);
|
||||
|
|
@ -364,6 +374,33 @@ echo "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\"
|
|||
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 filter that names nothing is refused: skipping its empty items
|
||||
// must not leave no filter, which is a full run.
|
||||
const Process::Captured noUnits = run({"--units", ","});
|
||||
const Process::Captured noLenses = run({"--only", " , "});
|
||||
Check("--units and --only that name nothing are usage errors (exit 3) - never a full run",
|
||||
noUnits.returnCode == 3 && noUnits.err.contains("argument --units: names no unit id (',')") && noLenses.returnCode == 3
|
||||
&& noLenses.err.contains("argument --only: names no lens file (' , ')"),
|
||||
noUnits.err + noLenses.err);
|
||||
|
||||
// A patch that is not UTF-8 is no artifact: a preflight stop.
|
||||
const fs::path badArtifact = repo / ".review-gates" / "push" / "bad";
|
||||
fs::copy(artifact, badArtifact, fs::copy_options::recursive);
|
||||
for (const fs::directory_entry& entry : fs::directory_iterator(badArtifact)) {
|
||||
if (entry.path().extension() == ".patch") std::ofstream(entry.path(), std::ios::binary | std::ios::app) << std::string("\xff\n", 2);
|
||||
}
|
||||
const Process::Captured badPatch = Process::Run({Platform::SelfExe().string(), "run", "--artifact", ".review-gates/push/bad", "--base", baseSha, "--trees", ".", "--dispositions",
|
||||
".review-gates/push/bad/gate-state/dispositions.json", "--claude-bin", fake.string(), "--scratch", scratchRoot.string(), "--dry-run"},
|
||||
repo, std::chrono::seconds(60), {}, Env);
|
||||
Check("preflight: a patch that is not UTF-8 stops the run, exit 3, saying so",
|
||||
badPatch.returnCode == 3 && badPatch.err.contains("the artifact's patches are not UTF-8 ('utf-8' codec can't decode byte 0xff"), badPatch.err);
|
||||
|
||||
// An error that escapes a run ends it with its message, exit 1,
|
||||
// through the run's cleanup.
|
||||
const Process::Captured badReserve = run({"--no-reuse", "--no-salvage"}, {{"REVIEW_GATES_MEM_RESERVE_MB", "x"}});
|
||||
Check("an error escaping a full run (a reserve that is no number) ends it with its ValueError, exit 1",
|
||||
badReserve.returnCode == 1 && badReserve.err.contains("crafter-review: ValueError: could not convert string to float: 'x'"), badReserve.err);
|
||||
|
||||
// A stop between two gates: the next gate's pool starts aborted and
|
||||
// runs nothing of it.
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue