test(pipeline): the checks of the pure pipeline
Stream, Normalize, Merge, Dispositions, Config, CodeKeys and the pure half of CommitsMode, each a test of its own (131 checks). The fixture rewrites are checked in (code-keys/<name>/, commits3/), so the suite reads fixtures and edits nothing. The allowlist, command-line and MCP checks of CommitsMode come with the pool they test. Among them: this binary's `<exe> sql` spelling counts as a SQL call, and a result line with a raw U+2028 in a JSON string is still the result line. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
741e3a656d
commit
71dba5690f
12 changed files with 3778 additions and 1 deletions
180
tests/CodeKeys/main.cpp
Normal file
180
tests/CodeKeys/main.cpp
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Code keys: a static code pass is keyed on its code, a functional pass on
|
||||
// its code AND the range. Every rewrite of the commits fixture is checked in
|
||||
// under tests/Goldens/fixture/code-keys/<rewrite>/.
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
using Keys = std::map<std::string, std::string>;
|
||||
|
||||
Keys CodeKeys(const fs::path& d, const std::string& lens = "repo-bug-prover.md", std::string_view env = {}) {
|
||||
const Inventory::Built built = Inventory::Build("commits", d.string());
|
||||
const Plan::CommitCode cc = Plan::CommitCodeOf(Artifact::PatchTexts(d));
|
||||
Keys out;
|
||||
for (const Json& u : built.out.at("units")) {
|
||||
const std::string id = u.at("id").get<std::string>();
|
||||
if (Plan::KindFor(lens, u) != "code") continue;
|
||||
if (lens == Fn && (id.ends_with(":outside") || id.ends_with(":absent") || id.ends_with("#hdr"))) continue;
|
||||
const Json job = {{"kind", "block"}, {"unit", id}, {"unitKind", "code"}};
|
||||
out[id] = Plan::BlockReuseKey(job, lens, Plan::UnitText(built.lines, u), cc, {}, "c", 1, env).value_or("");
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
std::optional<std::string> WholeKey(const fs::path& d, const std::string& lens = "repo-better-solution.md", std::string_view env = {}) {
|
||||
return Plan::WholeCodeKey({{"kind", "whole"}, {"unit", nullptr}}, lens, Plan::CommitCodeOf(Artifact::PatchTexts(d)), {}, "c", 1, env);
|
||||
}
|
||||
|
||||
std::set<std::string> Changed(const Keys& now, const Keys& before) {
|
||||
std::set<std::string> out;
|
||||
for (const auto& [id, key] : before) {
|
||||
if (now.at(id) != key) out.insert(id);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
bool AllChanged(const Keys& now, const Keys& before) {
|
||||
return Changed(now, before).size() == before.size();
|
||||
}
|
||||
|
||||
std::string Show(const std::set<std::string>& ids) {
|
||||
Json out = Json::array();
|
||||
for (const std::string& id : ids) out.push_back(id);
|
||||
return Dyn::ReprValue(out);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::println("reuse keys: a static code pass is keyed on its code; a functional pass on its code AND the range (fixtures/commits and rewrites of it)");
|
||||
const fs::path fixture = fs::current_path() / "tests" / "Goldens" / "fixture";
|
||||
const fs::path src = fixture / "commits";
|
||||
auto rewrite = [&](std::string_view name) { return fixture / "code-keys" / name; };
|
||||
const Keys base = CodeKeys(src);
|
||||
const Keys fbase = CodeKeys(src, Fn);
|
||||
const Keys sbase = CodeKeys(src, "repo-security.md");
|
||||
std::set<std::string> baseValues;
|
||||
std::set<std::string> sbaseValues;
|
||||
for (const auto& [id, key] : base) baseValues.insert(key);
|
||||
for (const auto& [id, key] : sbase) sbaseValues.insert(key);
|
||||
const bool keysDiffer = std::ranges::all_of(base, [&](const auto& entry) { return sbase.contains(entry.first) && sbase.at(entry.first) != entry.second; });
|
||||
Check("the security lens keys the same code units as the bug-prover, every key its own (the lens file is in the key): a verdict of one is never evidence for the other",
|
||||
std::ranges::equal(std::views::keys(sbase), std::views::keys(base)) && keysDiffer && sbaseValues.size() == baseValues.size());
|
||||
std::set<std::string> slots;
|
||||
for (const auto& [id, key] : base) {
|
||||
if (id.ends_with(":outside") || id.ends_with(":absent")) slots.insert(id);
|
||||
}
|
||||
std::set<std::string> fbaseValues;
|
||||
for (const auto& [id, key] : fbase) fbaseValues.insert(key);
|
||||
const std::set<std::string> functionalUnits = {"p1:hello.py#1", "p2:hello.py#1", "p2:hello.py#2"};
|
||||
std::set<std::string> shared;
|
||||
std::ranges::set_intersection(baseValues, fbaseValues, std::inserter(shared, shared.begin()));
|
||||
Check("fixture: every static code unit has a key (4 slots, hunks, headers), the functional units are the hunks alone, no doc unit, no shared key",
|
||||
std::ranges::all_of(base, [](const auto& entry) { return !entry.second.empty(); }) && slots.size() == 4 && base.contains("p2:hello.py#hdr")
|
||||
&& std::set<std::string>(std::views::keys(fbase).begin(), std::views::keys(fbase).end()) == functionalUnits
|
||||
&& std::ranges::none_of(base, [](const auto& entry) { return entry.first.contains(".md#"); }) && shared.empty());
|
||||
|
||||
const fs::path rw = rewrite("rewrite");
|
||||
Check("a rewrite that leaves the code alone (ids, counter, blobs, line numbers, git version, a reworded message) -> every key the same, static and functional",
|
||||
CodeKeys(rw) == base && CodeKeys(rw, Fn) == fbase && WholeKey(rw) == WholeKey(src) && WholeKey(rw, Fn) == WholeKey(src, Fn)
|
||||
&& Artifact::ArtifactHash("commits", rw) != Artifact::ArtifactHash("commits", src));
|
||||
const fs::path code = rewrite("code");
|
||||
std::set<std::string> diff = Changed(CodeKeys(code), base);
|
||||
std::set<std::string> expected = slots;
|
||||
expected.insert("p2:hello.py#1");
|
||||
Check("one changed line in p2:hello.py#1 -> static: that hunk and the four slots, nothing else", diff == expected, Show(diff));
|
||||
Check("...functional: EVERY unit is asked again (the range is in its key)", AllChanged(CodeKeys(code, Fn), fbase));
|
||||
Check("...and both whole passes", WholeKey(code) != WholeKey(src) && WholeKey(code, Fn) != WholeKey(src, Fn));
|
||||
const Keys ctx = CodeKeys(rewrite("context"));
|
||||
Check("a changed context line re-keys its hunk", ctx.at("p2:hello.py#1") != base.at("p2:hello.py#1") && ctx.at("p2:hello.py#2") == base.at("p2:hello.py#2"));
|
||||
const fs::path nobody = rewrite("nobody");
|
||||
Check("p2 loses its body -> p2:absent alone (static); nothing functional", Changed(CodeKeys(nobody), base) == std::set<std::string>{"p2:absent"} && CodeKeys(nobody, Fn) == fbase);
|
||||
const fs::path bse = rewrite("base");
|
||||
Check("another base -> every key changes, static, functional and both wholes", AllChanged(CodeKeys(bse), base) && AllChanged(CodeKeys(bse, Fn), fbase) && WholeKey(bse) != WholeKey(src));
|
||||
const fs::path mode = rewrite("mode");
|
||||
expected = slots;
|
||||
expected.insert("p2:hello.py#hdr");
|
||||
Check("a mode change re-keys the file header (and the slots), not the hunks; the functional hunks follow the range", Changed(CodeKeys(mode), base) == expected
|
||||
&& AllChanged(CodeKeys(mode, Fn), fbase));
|
||||
const fs::path docs = rewrite("docs");
|
||||
Check("a reworded README sentence -> no key changes anywhere (documentation is nobody's code)", CodeKeys(docs) == base && CodeKeys(docs, Fn) == fbase && WholeKey(docs) == WholeKey(src));
|
||||
const fs::path nodoc = rewrite("nodoc");
|
||||
Check("p2 drops its README hunk -> p2's two slots and the whole passes are asked again, no hunk", Changed(CodeKeys(nodoc), base) == std::set<std::string>{"p2:absent", "p2:outside"}
|
||||
&& WholeKey(nodoc) != WholeKey(src) && CodeKeys(nodoc, Fn) == fbase);
|
||||
|
||||
// The environment in the keys (plan.ENVIRONMENTS item 1).
|
||||
const Keys sb = CodeKeys(src, "repo-bug-prover.md", "supabase");
|
||||
const Keys fb = CodeKeys(src, Fn, "browser+supabase");
|
||||
Check("environment: a lens in its default environment keeps the key every stored verdict was built with (none for the static lenses, browser for the functional lens)",
|
||||
CodeKeys(src, "repo-bug-prover.md", "none") == base && CodeKeys(src, Fn, "browser") == fbase && WholeKey(src, "repo-better-solution.md", "none") == WholeKey(src)
|
||||
&& WholeKey(src, Fn, "browser") == WholeKey(src, Fn) && Plan::EnvDeps("repo-bug-prover.md").empty() && Plan::EnvDeps(Fn, "browser").empty()
|
||||
&& Plan::EnvDeps(Fn, "browser+supabase") == std::vector<std::string>{"env=browser+supabase"});
|
||||
std::set<std::string> everyKey;
|
||||
for (const Plan::Environment& environment : Plan::Environments) {
|
||||
for (const auto& [id, key] : CodeKeys(src, "repo-bug-prover.md", environment.name)) everyKey.insert(key);
|
||||
}
|
||||
Check("environment: any other environment re-keys every unit, block and whole, static and functional - each environment its own keys, none shared",
|
||||
AllChanged(sb, base) && AllChanged(fb, fbase) && WholeKey(src, "repo-better-solution.md", "supabase") != WholeKey(src)
|
||||
&& WholeKey(src, Fn, "browser+supabase") != WholeKey(src, Fn) && everyKey.size() == Plan::Environments.size() * base.size());
|
||||
expected = slots;
|
||||
expected.insert("p2:hello.py#1");
|
||||
Check("environment: the code rules hold inside an environment - a rewrite that leaves the code alone keeps every supabase key, a changed line re-keys that hunk and the slots alone",
|
||||
CodeKeys(rw, "repo-bug-prover.md", "supabase") == sb && CodeKeys(rw, Fn, "browser+supabase") == fb
|
||||
&& WholeKey(rw, "repo-better-solution.md", "supabase") == WholeKey(src, "repo-better-solution.md", "supabase")
|
||||
&& Changed(CodeKeys(code, "repo-bug-prover.md", "supabase"), sb) == expected);
|
||||
|
||||
const Plan::CommitCode cc = Plan::CommitCodeOf(Artifact::PatchTexts(src));
|
||||
Check("whole key: both wholes have keys and differ; a block never; the bug-prover has none", WholeKey(src) && WholeKey(src, Fn) && WholeKey(src) != WholeKey(src, Fn)
|
||||
&& !WholeKey(src, "repo-bug-prover.md") && !Plan::WholeCodeKey({{"kind", "block"}, {"unit", "x"}}, "repo-better-solution.md", cc, {}, "c", 1));
|
||||
const Json job = {{"kind", "whole"}, {"unit", nullptr}};
|
||||
const std::vector<std::string> w1 = {"w1"};
|
||||
Json withModel = job;
|
||||
withModel["model"] = "m";
|
||||
const std::set<std::optional<std::string>> wholes = {
|
||||
WholeKey(src), Plan::WholeCodeKey(job, "repo-better-solution.md", cc, w1, "c", 1), Plan::WholeCodeKey(job, "repo-better-solution.md", cc, {}, "c2", 1),
|
||||
Plan::WholeCodeKey(job, "repo-better-solution.md", cc, {}, "c", 2), Plan::WholeCodeKey(withModel, "repo-better-solution.md", cc, {}, "c", 1),
|
||||
};
|
||||
Check("whole key: dispositions, the charter, the prompt version and the model are in it", wholes.size() == 5);
|
||||
Plan::CommitCode two;
|
||||
two.base = "b";
|
||||
two.range = "r";
|
||||
two.code = {{3, Plan::Sha("")}, {4, Plan::Sha("")}};
|
||||
two.body = {{3, true}, {4, true}};
|
||||
two.docs = {{3, {"h"}}, {4, {"h"}}};
|
||||
two.upto = {{3, "u"}, {4, "u"}};
|
||||
Plan::CommitCode renamed = two;
|
||||
renamed.docs = {{3, {"h2"}}, {4, {"h"}}};
|
||||
Check("slot keys: two documentation-only commits do not share a slot key (the position is in it); a renamed doc file re-asks the slots of its commit",
|
||||
Plan::CodeKeyParts("p3:absent", two) != Plan::CodeKeyParts("p4:absent", two)
|
||||
&& Plan::CodeKeyParts("p3:outside", two) != Plan::CodeKeyParts("p3:outside", renamed) && Plan::CodeKeyParts("p3:absent", two) != Plan::CodeKeyParts("p3:absent", renamed));
|
||||
Check("a hunk is keyed on its file and text, not on its number or its commit; the functional ident is its own", Plan::CodeKeyParts("p2:hello.py#2", cc) == Plan::CodeKeyParts("p7:hello.py#5", cc)
|
||||
&& Plan::CodeKeyParts("p2:hello.py#2", cc).first != Plan::CodeKeyParts("p2:other.py#2", cc).first
|
||||
&& Plan::CodeKeyParts("p2:hello.py#hdr", cc).first != Plan::CodeKeyParts("p2:hello.py#1", cc).first);
|
||||
const std::vector<std::string> parts = {"repo-bug-prover.md", "U", Plan::Sha("T"), Plan::Sha("d"), "w1", "cs", "1", "", "max"};
|
||||
Check("reuse key: the pre-2026-10-02 formula with the effort slot frozen at max", Plan::ReuseKey({{"kind", "block"}, {"unit", "U"}}, "repo-bug-prover.md", "T", {"d"}, {"w1"}, "cs", 1)
|
||||
== Plan::Sha(Dyn::Join(parts, "|")).substr(0, 16) && Plan::ReuseKeyEffortSlot == "max");
|
||||
Check("code_norm leaves a hunk body alone", Plan::CodeNorm("@@ -1,2 +1,2 @@ f\n-- \n+index 1..2\n+@@ -1 +1 @@") == "@@ f\n-- \n+index 1..2\n+@@ -1 +1 @@");
|
||||
const Json e = {{"unit", "p2:hello.py#2"}, {"coverage", {{"item", "p2:hello.py#2"}, {"verdict", "clear"}, {"basis", "b"}}}, {"findings", Json::array()},
|
||||
{"notes", {{{"note", "n"}, {"items", {"p2:hello.py#2", "p2:hello.py#1"}}}}}};
|
||||
const Json r = Plan::Rebind(e, "p2:hello.py#3");
|
||||
Json v = {{"verdict", "SEND-READY"}, {"findings", r.at("findings")}, {"notes", r.at("notes")}, {"coverage", {r.at("coverage")}}};
|
||||
const std::vector<std::string> ids = {"p2:hello.py#3"};
|
||||
const Json n = Normalize::NormalizeOutput(v, {}, "repo-bug-prover.md", "H", Json::array({{{"id", "p2:hello.py#3"}}}), ids);
|
||||
Check("rebind: the stored result under the unit's id of today, and it covers the renumbered unit", r.at("unit") == "p2:hello.py#3"
|
||||
&& r.at("notes").at(0).at("items") == Json::array({"p2:hello.py#3"}) && e.at("coverage").at("item") == "p2:hello.py#2" && n.at("verdict") == "SEND-READY"
|
||||
&& n.at("coverageMissing").empty());
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
271
tests/CommitsMode/main.cpp
Normal file
271
tests/CommitsMode/main.cpp
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Commits mode: the inventory, the gates, the plans (the functional lens's
|
||||
// n/a), the prompts, over the commits fixture. The allowlist, command line
|
||||
// and MCP checks sit with the pool they test.
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
std::set<std::string> Set(const std::vector<std::string>& values) {
|
||||
return {values.begin(), values.end()};
|
||||
}
|
||||
|
||||
std::string Show(const std::set<std::string>& values) {
|
||||
Json out = Json::array();
|
||||
for (const std::string& value : values) out.push_back(value);
|
||||
return Dyn::ReprValue(out);
|
||||
}
|
||||
|
||||
const Json& Unit(const Json& units, std::string_view id) {
|
||||
for (const Json& unit : units) {
|
||||
if (unit.at("id") == id) return unit;
|
||||
}
|
||||
throw std::runtime_error(std::format("no unit {}", id));
|
||||
}
|
||||
|
||||
const Json& Job(const std::vector<Json>& jobs, std::string_view kind, std::string_view unit = {}) {
|
||||
for (const Json& job : jobs) {
|
||||
if (job.at("kind") == kind && (unit.empty() || job.at("unit") == unit)) return job;
|
||||
}
|
||||
throw std::runtime_error(std::format("no {} job {}", kind, unit));
|
||||
}
|
||||
|
||||
std::vector<std::pair<std::string, std::vector<std::string>>> Gates(const std::vector<Plan::Gate>& gates) {
|
||||
std::vector<std::pair<std::string, std::vector<std::string>>> out;
|
||||
for (const Plan::Gate& gate : gates) out.emplace_back(std::string(gate.title), std::vector<std::string>(gate.lenses.begin(), gate.lenses.end()));
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<std::string> Titles(const std::vector<Plan::Gate>& gates) {
|
||||
std::vector<std::string> out;
|
||||
for (const Plan::Gate& gate : gates) out.emplace_back(gate.title);
|
||||
return out;
|
||||
}
|
||||
|
||||
// re.findall(r'(?i)\b(exploit\w*|attack\w*|...)\b', text): the words that
|
||||
// start with one of the stems.
|
||||
std::vector<std::string> Offensive(std::string_view text) {
|
||||
constexpr std::array<std::string_view, 10> Stems = {"exploit", "attack", "payload", "malicious", "weaponi", "hack", "pwn", "backdoor", "intrusion", "penetrat"};
|
||||
const std::u32string decoded = Dyn::Decode(text);
|
||||
std::vector<std::string> found;
|
||||
std::size_t at = 0;
|
||||
while (at < decoded.size()) {
|
||||
if (!Dyn::IsWord(decoded[at])) {
|
||||
++at;
|
||||
continue;
|
||||
}
|
||||
std::size_t end = at;
|
||||
while (end < decoded.size() && Dyn::IsWord(decoded[end])) ++end;
|
||||
const std::u32string_view word = std::u32string_view(decoded).substr(at, end - at);
|
||||
for (const std::string_view stem : Stems) {
|
||||
if (word.size() < stem.size()) continue;
|
||||
bool match = true;
|
||||
for (std::size_t i = 0; i < stem.size(); ++i) match = match && Dyn::IgnoreCaseLetter(word[i]) == stem[i];
|
||||
if (match) {
|
||||
found.push_back(Dyn::Encode(word));
|
||||
break;
|
||||
}
|
||||
}
|
||||
at = end;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::println("commits mode: inventory, gates, plans (the functional lens's n/a), prompts (fixtures/commits)");
|
||||
SetHome(fs::current_path());
|
||||
const fs::path fixture = fs::current_path() / "tests" / "Goldens" / "fixture";
|
||||
const fs::path d = fixture / "commits";
|
||||
const Inventory::Built built = Inventory::Build("commits", d.string());
|
||||
const Json& units = built.out.at("units");
|
||||
const std::vector<std::string>& lines = built.lines;
|
||||
std::vector<std::string> ids;
|
||||
for (const Json& u : units) ids.push_back(u.at("id").get<std::string>());
|
||||
const std::set<std::string> idSet = Set(ids);
|
||||
Check("body-less p1: no msg/trailers units, subject/diffstat/hunk present", !idSet.contains("p1:msg") && !idSet.contains("p1:trailers") && idSet.contains("p1:subject")
|
||||
&& idSet.contains("p1:diffstat") && idSet.contains("p1:hello.py#1"));
|
||||
Check("p2: msg unit, no trailers, three hunks over two files", idSet.contains("p2:msg") && !idSet.contains("p2:trailers") && idSet.contains("p2:README.md#1")
|
||||
&& idSet.contains("p2:hello.py#1") && idSet.contains("p2:hello.py#2"));
|
||||
Check("the three whole slots", idSet.contains("whole:order") && idSet.contains("whole:absent") && idSet.contains("whole:behaviour") && !idSet.contains("whole:recipients"));
|
||||
Check("body-less commit: diffstat unit starts at the --- line", Plan::UnitText(lines, Unit(units, "p1:diffstat")).starts_with("---"));
|
||||
std::string all;
|
||||
for (const std::string& name : Artifact::PatchFiles(d)) all += *Platform::ReadFile(d / name);
|
||||
Check("artifact_hash = sha256 of the sorted concatenation; patch_head = the From line of the last patch", Artifact::ArtifactHash("commits", d) == Sha256Hex(all)
|
||||
&& Artifact::PatchHead(d) == std::string(40, '2'));
|
||||
const auto g = Gates(Plan::GatesFor());
|
||||
std::vector<std::string> gateNames;
|
||||
for (const auto& [title, lenses] : g) gateNames.push_back(Dyn::Split(title, " — ")[0]);
|
||||
Check("gates: 1 approach, 2 functional, 3 proof - the bug-prover and the security lens side by side (2026-10-22), the owner stays Gate 4",
|
||||
gateNames == std::vector<std::string>{"Gate 1", "Gate 2", "Gate 3"} && g[0].second == std::vector<std::string>{"repo-better-solution.md"} && g[1].second == std::vector<std::string>{Fn}
|
||||
&& g[2].second == std::vector<std::string>{"repo-bug-prover.md", "repo-security.md"});
|
||||
const std::vector<std::string> onlyFn = {Fn};
|
||||
const std::vector<std::string> security = {"repo-security.md"};
|
||||
std::set<std::string> lensSet;
|
||||
for (const Plan::Lens& lens : Plan::Lenses) lensSet.emplace(lens.file);
|
||||
using GateList = std::vector<std::pair<std::string, std::vector<std::string>>>;
|
||||
Check("mode_lenses = the four; --only keeps, --without drops, an emptied gate disappears; Gate 3 with one of its lenses dropped keeps the other",
|
||||
Set(Plan::ModeLenses()) == lensSet && Gates(Plan::GatesFor(onlyFn)) == GateList{{"Gate 2 — Functional", {Fn}}}
|
||||
&& Titles(Plan::GatesFor({}, onlyFn)) == std::vector<std::string>{"Gate 1 — Approach", "Gate 3 — Proof"} && Plan::GatesFor(onlyFn, onlyFn).empty()
|
||||
&& Gates(Plan::GatesFor({}, security)).back() == std::pair<std::string, std::vector<std::string>>{"Gate 3 — Proof", {"repo-bug-prover.md"}}
|
||||
&& Gates(Plan::GatesFor(security)) == GateList{{"Gate 3 — Proof", {"repo-security.md"}}});
|
||||
const std::vector<std::string> threeOff = {"repo-better-solution.md", "repo-bug-prover.md", "repo-security.md"};
|
||||
const std::vector<std::string> bugOff = {"repo-bug-prover.md"};
|
||||
Check("lenses off by config: gates_for drops the lens and its gate; --only cannot bring it back", Titles(Plan::GatesFor({}, {}, onlyFn)) == std::vector<std::string>{"Gate 1 — Approach", "Gate 3 — Proof"}
|
||||
&& Plan::GatesFor(onlyFn, {}, onlyFn).empty() && Gates(Plan::GatesFor({}, {}, threeOff)) == GateList{{"Gate 2 — Functional", {Fn}}}
|
||||
&& Gates(Plan::GatesFor({}, {}, bugOff)).back() == std::pair<std::string, std::vector<std::string>>{"Gate 3 — Proof", {"repo-security.md"}});
|
||||
const std::vector<std::string> allLenses(lensSet.begin(), lensSet.end());
|
||||
const std::set<std::string> nrAll = Set(Plan::NotReviewed(units, lines, Plan::ModeLenses()));
|
||||
Check("not reviewed (every lens runs): the text units and the documentation units", nrAll == std::set<std::string>{"p1:subject", "p1:diffstat", "p2:subject", "p2:msg", "p2:diffstat", "p2:README.md#hdr", "p2:README.md#1"},
|
||||
Show(nrAll));
|
||||
std::vector<std::string> statics;
|
||||
for (const Plan::Lens& lens : Plan::Lenses) {
|
||||
if (lens.file != Fn) statics.emplace_back(lens.file);
|
||||
}
|
||||
std::set<std::string> plusBehaviour = nrAll;
|
||||
plusBehaviour.insert("whole:behaviour");
|
||||
std::set<std::string> plusStatic = nrAll;
|
||||
plusStatic.insert({"whole:order", "whole:absent", "p1:outside", "p1:absent", "p2:outside", "p2:absent", "p1:hello.py#hdr", "p2:hello.py#hdr"});
|
||||
Check("not reviewed is derived from the lenses that run: the functional lens off adds whole:behaviour; only the functional lens adds the static slots and the file headers (a mechanical n/a is no reading)",
|
||||
Set(Plan::NotReviewed(units, lines, statics)) == plusBehaviour && Set(Plan::NotReviewed(units, lines, onlyFn)) == plusStatic, Show(Set(Plan::NotReviewed(units, lines, onlyFn))));
|
||||
std::set<std::string> codeIds;
|
||||
for (const std::string& id : ids) {
|
||||
if (Plan::UnitKindOf(id) == "code" && !id.contains(".md#")) codeIds.insert(id);
|
||||
}
|
||||
auto unitsOf = [](const std::vector<Json>& jobs) {
|
||||
std::set<std::string> out;
|
||||
for (const Json& job : jobs) out.insert(job.at("unit").get<std::string>());
|
||||
return out;
|
||||
};
|
||||
const Plan::LensPlan approach = Plan::PlanLens("repo-better-solution.md", units, lines);
|
||||
std::vector<std::string> expected = {approach.whole.at("expectedIds").at(0).get<std::string>(), approach.whole.at("expectedIds").at(1).get<std::string>()};
|
||||
std::ranges::sort(expected);
|
||||
Check("repo-better-solution: whole pass over order+absent, every code unit incl. outside/absent, no doc unit", !approach.whole.is_null()
|
||||
&& expected == std::vector<std::string>{"whole:absent", "whole:order"} && approach.whole.at("expectedIds").size() == 2 && unitsOf(approach.blocks) == codeIds
|
||||
&& std::ranges::all_of(approach.blocks, [](const Json& j) { return j.at("kind") == "block"; }));
|
||||
const Plan::LensPlan prover = Plan::PlanLens("repo-bug-prover.md", units, lines);
|
||||
Check("repo-bug-prover: no whole pass, the same code units", prover.whole.is_null() && unitsOf(prover.blocks) == codeIds && codeIds.contains("p2:hello.py#hdr"));
|
||||
const Plan::LensPlan sec = Plan::PlanLens("repo-security.md", units, lines);
|
||||
const Plan::Lens& s = Plan::LensOf("repo-security.md");
|
||||
const Plan::Lens& b = Plan::LensOf("repo-bug-prover.md");
|
||||
const bool sameShape = s.gate == b.gate && s.whole == b.whole && std::ranges::equal(s.slots, b.slots) && std::ranges::equal(s.block, b.block) && s.model == b.model && s.reuse == b.reuse
|
||||
&& s.wholeReuse == b.wholeReuse && s.reads == b.reads && s.key == b.key && s.empirical == b.empirical && s.perCommitSlots == b.perCommitSlots && s.env == b.env;
|
||||
Check("repo-security: the bug-prover's shape - no whole pass, the same code units, static blocks keyed on the code, three reads, none by default, not empirical",
|
||||
sec.whole.is_null() && unitsOf(sec.blocks) == codeIds && sameShape && s.gate == 3
|
||||
&& std::ranges::all_of(sec.blocks, [](const Json& j) { return j.at("maxTurns") == 60 && j.at("timeoutMin") == 40 && j.at("model") == Plan::ModelFor("opus"); }));
|
||||
const std::vector<std::string> exclude = {"**/*.test.*", "docs/**"};
|
||||
const Plan::LensPlan functional = Plan::PlanLens(Fn, units, lines, {}, exclude);
|
||||
std::map<std::string, std::string> kinds;
|
||||
for (const Json& j : functional.blocks) kinds[j.at("unit").get<std::string>()] = j.at("kind").get<std::string>();
|
||||
const std::map<std::string, std::string> wantKinds = {{"p1:hello.py#hdr", "auto-na"}, {"p1:hello.py#1", "block"}, {"p2:hello.py#hdr", "auto-na"}, {"p2:hello.py#1", "block"}, {"p2:hello.py#2", "block"}};
|
||||
Check("repo-functional: whole pass over whole:behaviour only; hunks as blocks; file headers n/a; no per-commit slots; no doc unit", !functional.whole.is_null()
|
||||
&& functional.whole.at("expectedIds") == Json::array({"whole:behaviour"}) && kinds == wantKinds);
|
||||
const Json& functionalBlock = Job(functional.blocks, "block");
|
||||
Check("functional budgets: block 90/60, whole 220/120; static block 60/40, whole 180/105", functionalBlock.at("maxTurns") == 90 && functionalBlock.at("timeoutMin") == 60
|
||||
&& functional.whole.at("maxTurns") == 220 && functional.whole.at("timeoutMin") == 120 && prover.blocks.at(0).at("maxTurns") == 60 && prover.blocks.at(0).at("timeoutMin") == 40
|
||||
&& approach.whole.at("maxTurns") == 180 && approach.whole.at("timeoutMin") == 105);
|
||||
|
||||
// A third commit touching a test file: excluded for the functional lens,
|
||||
// a block for the static ones.
|
||||
const Inventory::Built three = Inventory::Build("commits", (fixture / "commits3").string());
|
||||
const std::vector<std::string> testsGlob = {"tests/**"};
|
||||
std::map<std::string, std::pair<std::string, std::string>> f3;
|
||||
for (const Json& j : Plan::PlanLens(Fn, three.out.at("units"), three.lines, {}, testsGlob).blocks) {
|
||||
if (j.at("unit").get<std::string>().starts_with("p3:")) f3[j.at("unit").get<std::string>()] = {j.at("kind").get<std::string>(), Dyn::Get(j, "basis").is_string() ? j.at("basis").get<std::string>() : ""};
|
||||
}
|
||||
std::map<std::string, std::string> s3;
|
||||
for (const Json& j : Plan::PlanLens("repo-bug-prover.md", three.out.at("units"), three.lines).blocks) {
|
||||
if (j.at("unit").get<std::string>().starts_with("p3:")) s3[j.at("unit").get<std::string>()] = j.at("kind").get<std::string>();
|
||||
}
|
||||
Check("functional.exclude: the test hunk is a mechanical n/a naming the glob; the static lens still reads it", f3["p3:tests/test_hello.py#1"].first == "auto-na"
|
||||
&& f3["p3:tests/test_hello.py#1"].second.contains("tests/**") && s3["p3:tests/test_hello.py#1"] == "block" && !f3.contains("p3:outside") && s3.contains("p3:outside"));
|
||||
const std::vector<std::string> testTs = {"**/*.test.ts"};
|
||||
const std::vector<std::string> github = {".github/**"};
|
||||
Check("excluded_by: ** prefixes match at the root too", Plan::ExcludedBy("a.test.ts", testTs) == "**/*.test.ts" && Plan::ExcludedBy("src/a.test.ts", testTs)
|
||||
&& !Plan::ExcludedBy("src/a.ts", testTs) && Plan::ExcludedBy(".github/x.yml", github));
|
||||
|
||||
// Prompts.
|
||||
const std::string st = Prompt::SystemText();
|
||||
Check("system text: commit range; the browser tools belong to a lens with an APPLICATION UNDER TEST block, not to the functional lens by name", st.contains("commit range")
|
||||
&& st.contains("mcp__playwright__") && st.contains("APPLICATION UNDER TEST block") && !st.contains("The functional lens alone") && !st.contains("The functional lens acts"));
|
||||
const std::string h = Artifact::ArtifactHash("commits", d);
|
||||
const std::vector<std::string> dot = {"."};
|
||||
const std::string rl = Prompt::RunLayer(".review-gates/push/x", "abc123", "2026-10-15", dot, h, lines, units, {}, "myrepo");
|
||||
Check("run layer: INPUTS (tree `.`, base, no review record), the artifact with line numbers, the inventory", rl.contains("myrepo") && rl.contains("`.` is the working directory")
|
||||
&& !Dyn::Lower(rl).contains("ledger") && rl.contains("\nARTIFACT (frozen, hash ") && rl.contains("\n\nINVENTORY - ") && rl.contains("\n0001| "));
|
||||
const std::vector<std::string> worktree = {".review-gates/trees/r-1234"};
|
||||
const std::vector<std::string> captures = {"captures/"};
|
||||
const std::string rl2 = Prompt::RunLayer("x", "", "2026-10-15", worktree, h, lines, units, captures, "r");
|
||||
Check("run layer: the context paths and a worktree named", rl2.contains("captures/") && rl2.contains(".review-gates/trees/r-1234") && rl2.contains("base-commit line"));
|
||||
const std::string totp = "/app/bin/crafter-review totp cypress/fixtures/created-user.json";
|
||||
const std::string ab = Prompt::AppBlock({{"appUrl", "http://localhost:8080"}, {"apiUrls", {"http://127.0.0.1:54321"}}, {"login", {{"notes", "TOTP enrolled"}}}, {"notes", "leave billing alone"}},
|
||||
"cypress/fixtures/created-user.json", totp);
|
||||
Check("app block: this lens drives it (no lens named); the URL, the origins, the fixture path (not its content), the exact TOTP command, the notes, the shared-stack rules",
|
||||
ab.contains("this lens drives it") && !ab.contains("every other lens") && ab.contains("http://localhost:8080") && ab.contains("127.0.0.1:54321")
|
||||
&& ab.contains("cypress/fixtures/created-user.json") && ab.contains(std::format("`{}`", totp)) && ab.contains("TOTP enrolled") && ab.contains("leave billing alone")
|
||||
&& ab.contains("not reset between passes") && ab.contains("browser_take_screenshot") && ab.contains("browser_snapshot before every action"));
|
||||
const std::string ab2 = Prompt::AppBlock({{"appUrl", "http://localhost:1"}});
|
||||
Check("app block without a fixture says so", ab2.contains("no fixture was found"));
|
||||
const std::string tailApproach = Prompt::Tail(approach.whole, units, lines);
|
||||
const std::string tailFunctional = Prompt::Tail(functional.whole, units, lines);
|
||||
Check("whole tails: the approach text lists order+absent; the functional text lists behaviour and names the APPLICATION UNDER TEST", tailApproach.contains("for THIS repository")
|
||||
&& tailApproach.contains("whole:order") && tailFunctional.contains("as a USER") && tailFunctional.contains("whole:behaviour") && !tailFunctional.contains("whole:order"));
|
||||
Check("block tails: the functional hint on a functional block, none on a static hunk, the repo hints on the slots",
|
||||
Prompt::Tail(functionalBlock, units, lines).contains("judges ONE hunk in the running application") && !Prompt::Tail(Job(prover.blocks, "block", "p2:hello.py#1"), units, lines).contains("judges ONE hunk")
|
||||
&& Prompt::Tail(Job(prover.blocks, "block", "p1:absent"), units, lines).contains("body-less commit") && Prompt::Tail(Job(prover.blocks, "block", "p1:outside"), units, lines).contains("users and callers"));
|
||||
const std::string ll = Prompt::LensLayer(Fn, "CHARTER TEXT", "DISPOSITIONS BLOCK", ab, "SUMMARY");
|
||||
const std::string sp = Prompt::SystemPrompt(st, rl, ll);
|
||||
const std::string up = Prompt::UserPrompt(tailFunctional, {{"maxTurns", 220}});
|
||||
Check("lens layer order: charter, application block, dispositions, the whole summary; system prompt = common + run + lens; user prompt = tail + budget line, no run layer",
|
||||
ll.find("CHARTER TEXT") < ll.find("APPLICATION UNDER TEST") && ll.find("APPLICATION UNDER TEST") < ll.find("DISPOSITIONS BLOCK") && ll.find("DISPOSITIONS BLOCK") < ll.find("SUMMARY")
|
||||
&& sp.starts_with(std::format("{}{}{}{}", st, Prompt::SepRun, rl, Prompt::SepLens)) && sp.ends_with(ll) && up.contains("Tool-call budget for this pass: 220")
|
||||
&& !up.contains(rl.substr(0, 200)));
|
||||
const std::string sql = "/app/bin/crafter-review sql";
|
||||
const std::string dbb = Prompt::DbBlock({{"dbUrl", "postgresql://postgres:hunter2@127.0.0.1:54322/postgres"}, {"notes", "the scan tables are seeded"}}, sql, "postgres", false,
|
||||
"it answers (x); its 3 applied migrations are the range HEAD's", true);
|
||||
const std::string dba = Prompt::DbBlock({{"notes", ""}}, sql, "postgres", true);
|
||||
Check("db block: the exact helper command and its heredoc, one transaction rolled back, nothing shared, the refused statements, the API roles, the limits, the evidence, the owner's notes - never the URL or its password",
|
||||
dbb.starts_with("DATABASE UNDER TEST") && dbb.contains(std::format("`{} <<'SQL'`", sql)) && dbb.contains("rolls back at its end") && dbb.contains("not by the other passes")
|
||||
&& dbb.contains("COMMIT") && dbb.contains("ROLLBACK TO are fine") && dbb.contains("set local role authenticated") && dbb.contains("request.jwt.claims") && dbb.contains("service_role")
|
||||
&& dbb.contains("statement_timeout 20 s") && dbb.contains("NO DATABASE") && dbb.contains("the scan tables are seeded") && !dbb.contains("hunter2") && !dbb.contains("54322")
|
||||
&& !dbb.contains("reaches the application") && dba.contains("Nothing you write here reaches the application")
|
||||
&& dbb.contains("Checked by the driver before this gate: it answers (x); its 3 applied migrations are the range HEAD's.") && dba.contains("Checked by the driver before this gate: nothing (a dry run).")
|
||||
&& dbb.contains("the role `postgres`, which bypasses row-level security.") && dba.contains("the role `postgres`.")
|
||||
&& Prompt::DbBlock(Json::object(), sql, "reviewer", false, std::nullopt, false).contains("the role `reviewer`, which row-level security applies to."));
|
||||
const std::string lld = Prompt::LensLayer(Fn, "CHARTER TEXT", "DISPOSITIONS BLOCK", ab, "SUMMARY", dbb);
|
||||
const std::vector<std::string> layered = {std::format("YOUR CHARTER (process/lenses/{}), verbatim:\n\nCHARTER TEXT", Fn), ab, "DISPOSITIONS BLOCK",
|
||||
"The whole pass of this lens on this hash concluded (the constraints your block pass rests on):\nSUMMARY"};
|
||||
Check("lens layer with the database: charter, application block, database block, dispositions, summary; without one it is byte for byte what it was",
|
||||
lld.find("CHARTER TEXT") < lld.find("APPLICATION UNDER TEST") && lld.find("APPLICATION UNDER TEST") < lld.find("DATABASE UNDER TEST")
|
||||
&& lld.find("DATABASE UNDER TEST") < lld.find("DISPOSITIONS BLOCK") && lld.find("DISPOSITIONS BLOCK") < lld.find("SUMMARY")
|
||||
&& Prompt::LensLayer(Fn, "CHARTER TEXT", "DISPOSITIONS BLOCK", ab, "SUMMARY", "") == ll && ll == Dyn::Join(layered, "\n\n"));
|
||||
Check("system text: a lens with a DATABASE UNDER TEST block runs SQL there, rolled back, and has the exact command it prints",
|
||||
st.contains("DATABASE UNDER TEST block runs SQL in that database, in transactions the pipeline rolls back") && st.contains("exact SQL command that block prints"));
|
||||
Check("prompt version 1; the charters exist and the common text has the placeholder filled", PromptVersion == 1
|
||||
&& std::ranges::all_of(Plan::Lenses, [](const Plan::Lens& lens) { return fs::exists(fs::current_path() / "process" / "lenses" / lens.file); }) && !st.contains("{{"));
|
||||
// The security charter is the owner's defensive review of their own code
|
||||
// (2026-10-22: a charter that reads as a request to do harm is declined by
|
||||
// the process, and a declined pass is NOT RETURNED).
|
||||
const std::string charter = Prompt::Charter("repo-security.md");
|
||||
const std::vector<std::string> banned = Offensive(charter);
|
||||
Check("the security charter: no offensive vocabulary; the throwaway local copy, the smallest step, no tooling built, refutations name the control, the per-commit slots, one residual-risk on the null path",
|
||||
banned.empty() && charter.contains("throwaway local copy") && charter.contains("smallest step") && charter.contains("no tooling built") && charter.contains("Refutations name the control")
|
||||
&& charter.contains("`pK:absent`") && charter.contains("`pK:outside`") && charter.contains("exactly one\n `residual-risk`") && charter.starts_with("# Lens: repo-security")
|
||||
&& charter.contains("Calibration (append dated entries at the bottom)"), Dyn::Join(banned, ", "));
|
||||
Check("the security charter names what the other lenses own and tells its blocks to route it as a note, never to suppress it", charter.contains("`repo-better-solution`")
|
||||
&& charter.contains("`repo-bug-prover`") && charter.contains("`repo-functional`") && charter.contains("instead of suppressing it"));
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
147
tests/Config/main.cpp
Normal file
147
tests/Config/main.cpp
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// The consumer config: the defaults, the merge, unknown keys, validation,
|
||||
// the lenses (run, environment), authors, the login fixture, the example.
|
||||
|
||||
#include <stdlib.h>
|
||||
import std;
|
||||
import Crafter.Review;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter::Review;
|
||||
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool Any(const std::vector<std::string>& problems, std::string_view part) {
|
||||
return std::ranges::any_of(problems, [&](const std::string& p) { return p.contains(part); });
|
||||
}
|
||||
|
||||
std::string Show(const std::vector<std::string>& problems) {
|
||||
Json out = Json::array();
|
||||
for (const std::string& p : problems) out.push_back(p);
|
||||
return Dyn::ReprValue(out);
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::println("config: defaults, merge, unknown keys, validation, lenses (run, environment), authors, login fixture, the example");
|
||||
// A git call of a test reads no global config: this machine's signs
|
||||
// every commit with the owner's key.
|
||||
setenv("GIT_CONFIG_GLOBAL", "/dev/null", 1);
|
||||
setenv("GIT_CONFIG_SYSTEM", "/dev/null", 1);
|
||||
SetHome(fs::current_path());
|
||||
const std::string fn(Plan::FunctionalLens);
|
||||
const fs::path td = fs::temp_directory_path() / std::format("crafter-review-config-{}", Platform::PublishedPid());
|
||||
fs::remove_all(td);
|
||||
fs::create_directories(td);
|
||||
auto load = [&] { return Config::Load(td); };
|
||||
Config::Loaded loaded = load();
|
||||
Check("no file: the defaults, no problem, _path None", loaded.problems.empty() && loaded.config.at("aiTrailers") == "forbid"
|
||||
&& loaded.config.at("functional").at("appUrl") == "http://localhost:8080" && loaded.config.at("_path").is_null());
|
||||
fs::create_directories(td / ".review-gates");
|
||||
const fs::path p = td / ".review-gates" / "config.json";
|
||||
auto put = [&](const Json& value) { std::ofstream(p, std::ios::binary) << DumpSpaced(value); };
|
||||
auto bad = [&](const Json& value) {
|
||||
put(value);
|
||||
return load().problems;
|
||||
};
|
||||
put({{"base", "origin/dev"}, {"aiTrailers", "allow"}, {"allowDirty", {"supabase/config.toml"}},
|
||||
{"functional", {{"exclude", {"**/*.test.ts"}}, {"readyPath", "/health"}, {"extraTools", {"Bash(redis-cli:*)", "mcp__x__y"}}}}});
|
||||
loaded = load();
|
||||
Check("the file merges over the defaults, nested too", loaded.problems.empty() && loaded.config.at("base") == "origin/dev" && loaded.config.at("aiTrailers") == "allow"
|
||||
&& loaded.config.at("functional").at("readyPath") == "/health" && loaded.config.at("functional").at("appUrl") == "http://localhost:8080" && loaded.config.at("_path") == p.string(),
|
||||
Show(loaded.problems));
|
||||
Check("rangeFrom 'base' without a base branch is a problem; 'sideways' is one too", Any(bad({{"rangeFrom", "base"}}), "needs a base")
|
||||
&& Any(bad({{"rangeFrom", "sideways"}, {"base", "origin/dev"}}), "rangeFrom must be"));
|
||||
std::vector<std::string> problems = bad({{"bogus", 1}, {"functional", {{"nope", 2}, {"appUrl", "ftp://x"}}}, {"aiTrailers", "maybe"}, {"authors", {"nomail"}}});
|
||||
Check("unknown keys and bad values are each a problem", problems.size() == 5 && Any(problems, "bogus") && Any(problems, "functional.nope") && Any(problems, "appUrl")
|
||||
&& 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));
|
||||
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"));
|
||||
put(Json::object());
|
||||
loaded = load();
|
||||
bool allRun = true;
|
||||
for (const Json& entry : loaded.config.at("lenses")) allRun = allRun && entry.at("run") == true;
|
||||
bool defaultsMatch = true;
|
||||
std::set<std::string> names;
|
||||
for (const Plan::Lens& lens : Plan::Lenses) {
|
||||
names.insert(Plan::LensName(lens.file));
|
||||
defaultsMatch = defaultsMatch && Config::Defaults().at("lenses").at(Plan::LensName(lens.file)).at("environment") == lens.env;
|
||||
}
|
||||
const Json defaults = Config::Defaults();
|
||||
std::set<std::string> defaultNames;
|
||||
for (auto it = defaults.at("lenses").begin(); it != defaults.at("lenses").end(); ++it) defaultNames.insert(it.key());
|
||||
const Json environments = {{"repo-better-solution.md", "none"}, {fn, "browser"}, {"repo-bug-prover.md", "none"}, {"repo-security.md", "none"}};
|
||||
Check("lenses: the defaults run all four with none/browser/none/none - plan.LENSES' env; the table names every lens of plan.LENSES", loaded.problems.empty() && allRun
|
||||
&& Config::LensesOff(loaded.config).empty() && Dyn::Equal(Config::Environments(loaded.config), environments) && defaultNames == names && defaultsMatch, Show(loaded.problems));
|
||||
put({{"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"environment", "browser"}}}}}});
|
||||
loaded = load();
|
||||
Check("lenses: run=false merges and lists the lens off in gate order; a static lens may take the browser", loaded.problems.empty()
|
||||
&& Config::LensesOff(loaded.config) == std::vector<std::string>{fn} && Config::Environments(loaded.config).at("repo-bug-prover.md") == "browser"
|
||||
&& loaded.config.at("lenses").at("repo-functional").at("environment") == "browser", Show(loaded.problems));
|
||||
problems = bad({{"lenses", {{"nope", {{"run", true}}}, {"repo-better-solution", {{"run", "yes"}, {"environment", "http"}}}}}});
|
||||
Check("lenses: an unknown lens is an unknown key; run must be a bool; environment must be one of none, browser, supabase, browser+supabase", problems.size() == 3
|
||||
&& Any(problems, "lenses.nope") && Any(problems, "run must be") && Any(problems, "one of none, browser, supabase, browser+supabase"), Show(problems));
|
||||
problems = bad({{"lenses", {{"repo-functional", {{"environment", "none"}}}}}});
|
||||
Check("the functional lens running with no application under test is a problem; off with none is fine; an unknown environment is reported once",
|
||||
problems.size() == 1 && problems[0].contains("empirical") && bad({{"lenses", {{"repo-functional", {{"run", false}, {"environment", "none"}}}}}}).empty()
|
||||
&& bad({{"lenses", {{"repo-functional", {{"environment", "http"}}}}}}).size() == 1, Show(problems));
|
||||
problems = bad({{"lenses", {{"repo-functional", {{"environment", "supabase"}}}}}});
|
||||
Check("the database alone is no application under test for the functional lens; browser+supabase is; a static lens may take either, or the database alone",
|
||||
problems.size() == 1 && problems[0].contains("empirical") && problems[0].contains("browser, browser+supabase")
|
||||
&& bad({{"lenses", {{"repo-functional", {{"environment", "browser+supabase"}}}, {"repo-bug-prover", {{"environment", "supabase"}}},
|
||||
{"repo-better-solution", {{"environment", "browser+supabase"}}}}}}).empty(), Show(problems));
|
||||
loaded = load();
|
||||
Check("supabase: the defaults are the local stack's Postgres (54322), no start/stop, no notes",
|
||||
Dyn::Equal(loaded.config.at("supabase"), {{"dbUrl", "postgresql://postgres:postgres@127.0.0.1:54322/postgres"}, {"start", nullptr}, {"stop", nullptr}, {"notes", ""}}));
|
||||
problems = bad({{"supabase", {{"dbUrl", "postgresql://postgres:secret@db.abcdefgh.supabase.co:5432/postgres"}}}});
|
||||
Check("supabase.dbUrl: a hosted database is refused (this machine only), and the message does not echo the password", problems.size() == 1
|
||||
&& problems[0].contains("not this machine") && !problems[0].contains("secret"), Show(problems));
|
||||
Check("supabase.dbUrl: http:// is no database URL; localhost and ::1 are this machine; start/stop must be command lines; an unknown key is reported",
|
||||
Any(bad({{"supabase", {{"dbUrl", "http://127.0.0.1:54321"}}}}), "postgres://") && bad({{"supabase", {{"dbUrl", "postgres://u:p@localhost:6543/x"}}}}).empty()
|
||||
&& bad({{"supabase", {{"dbUrl", "postgresql://u@[::1]:5432/x"}}}}).empty() && Any(bad({{"supabase", {{"start", {"bash", "x"}}}}}), "supabase.start")
|
||||
&& Any(bad({{"supabase", {{"nope", 1}}}}), "supabase.nope"));
|
||||
Check("functional.appUrl is checked only while a lens that runs drives the application (plan.ENVIRONMENTS item 4)",
|
||||
Any(bad({{"functional", {{"appUrl", "ftp://x"}}}}), "appUrl")
|
||||
&& bad({{"functional", {{"appUrl", "ftp://x"}}}, {"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"environment", "supabase"}}}}}}).empty()
|
||||
&& Any(bad({{"functional", {{"appUrl", "ftp://x"}}}, {"lenses", {{"repo-functional", {{"run", false}}}, {"repo-bug-prover", {{"environment", "browser+supabase"}}}}}}), "appUrl"));
|
||||
problems = bad({{"functional", {{"extraTools", {"Bash(psql:*)", "Bash(psql -h 127.0.0.1:*)", "Bash(psqlx:*)", "Bash(npm run e2e:*)", "mcp__x__y"}}}}});
|
||||
Check("functional.extraTools: an entry pool.DENY overrides is a problem (psql commits to the shared database; npm is a package manager); a mere prefix twin is not",
|
||||
problems.size() == 1 && problems[0].contains("Bash(psql:*)") && problems[0].contains("Bash(psql -h 127.0.0.1:*)") && problems[0].contains("Bash(npm run e2e:*)")
|
||||
&& !problems[0].contains("psqlx") && problems[0].contains("supabase environment"), Show(problems));
|
||||
Json withPassword = load().config;
|
||||
withPassword["supabase"] = {{"dbUrl", "postgresql://postgres:hunter2@127.0.0.1:54322/postgres"}, {"start", nullptr}, {"stop", nullptr}, {"notes", ""}};
|
||||
const Json rc = Config::Redacted(withPassword);
|
||||
Check("redacted: the dump's copy of the config masks the database password and drops _path", rc.at("supabase").at("dbUrl") == "postgresql://postgres:***@127.0.0.1:54322/postgres"
|
||||
&& !DumpSpaced(rc).contains("hunter2") && !rc.contains("_path"));
|
||||
problems = bad({{"lenses", {{"repo-functional", false}}}});
|
||||
Check("a bare false under lenses is a shape problem, never an exception; lenses_off and environments fall back to the defaults", problems.size() == 1
|
||||
&& problems[0].contains("must be an object") && Config::LensesOff(load().config).empty() && Config::Environments(load().config).at(fn) == "browser", Show(problems));
|
||||
Json allOff = Json::object();
|
||||
for (auto it = defaults.at("lenses").begin(); it != defaults.at("lenses").end(); ++it) allOff[it.key()] = {{"run", false}};
|
||||
problems = bad({{"lenses", allOff}});
|
||||
Check("every lens off is a problem", problems.size() == 1 && problems[0].contains("every lens is off"), Show(problems));
|
||||
const Json ex = Config::Example();
|
||||
Check("the example validates and is what process/templates/config.json holds", Config::Validate(ex).empty()
|
||||
&& Dyn::Equal(Dyn::LoadJsonFile(fs::current_path() / "process" / "templates" / "config.json"), ex));
|
||||
const fs::path repo = td / "r";
|
||||
fs::create_directories(repo);
|
||||
Process::Run({"git", "-C", repo.string(), "init", "-q"});
|
||||
Process::Run({"git", "-C", repo.string(), "config", "user.email", "me@example.com"});
|
||||
Check("authors: the configured list, else git config user.email", Config::Authors({{"authors", {"a@b.c"}}}, repo) == std::vector<std::string>{"a@b.c"}
|
||||
&& Config::Authors({{"authors", nullptr}}, repo) == std::vector<std::string>{"me@example.com"});
|
||||
std::ofstream(td / "u.json", std::ios::binary) << "{}";
|
||||
Check("login_fixture: the first candidate that exists", Config::LoginFixture({{"functional", {{"login", {{"fixtures", {"nope.json", "u.json"}}}}}}}, td) == "u.json"
|
||||
&& !Config::LoginFixture({{"functional", {{"login", {{"fixtures", Json::array()}}}}}}, td));
|
||||
fs::remove_all(td);
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
46
tests/Dispositions/main.cpp
Normal file
46
tests/Dispositions/main.cpp
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Dispositions: in force, carried, lapsed, and the prompt block.
|
||||
|
||||
import std;
|
||||
import Crafter.Review;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter::Review;
|
||||
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::println("dispositions: in-force / carried / lapsed, the prompt block");
|
||||
const fs::path td = fs::temp_directory_path() / std::format("crafter-review-dispositions-{}", Platform::PublishedPid());
|
||||
fs::create_directories(td);
|
||||
std::ofstream(td / "0001-a.patch", std::ios::binary) << "x\nANCHOR TEXT\ny\nANCHOR TEXT\n";
|
||||
const std::vector<Json> e = {
|
||||
{{"id", "a"}, {"hash", "H1"}},
|
||||
{{"id", "b"}, {"hash", "H0"}, {"carry", true}, {"anchors", {{{"file", "0001-a.patch"}, {"text", "ANCHOR TEXT"}, {"count", 2}}}}},
|
||||
{{"id", "c"}, {"hash", "H0"}, {"carry", true}, {"anchors", {{{"file", "0001-a.patch"}, {"text", "ANCHOR TEXT"}, {"count", 1}}}}},
|
||||
{{"id", "d"}, {"hash", "H0"}},
|
||||
};
|
||||
std::map<std::string, std::string> rows;
|
||||
for (const Json& row : Dispositions::Classify(e, "H1", td)) rows[row.at("id").get<std::string>()] = row.at("status").get<std::string>();
|
||||
const std::map<std::string, std::string> expected = {{"a", "in-force"}, {"b", "carried"}, {"c", "lapsed"}, {"d", "lapsed"}};
|
||||
Check("hash match in-force, count match carried, count mismatch lapsed, no carry lapsed", rows == expected);
|
||||
const std::vector<Json> inf = Dispositions::InForce(e, Dispositions::Classify(e, "H1", td));
|
||||
Check("in_force = in-force + carried", inf.size() == 2 && inf[0].at("id") == "a" && inf[1].at("id") == "b");
|
||||
const std::string blk = Dispositions::Block(inf, Dispositions::Classify(e, "H1", td), "repo-bug-prover.md", "H1");
|
||||
Check("block text names the carried entry, nothing outside the register discharges, Gate 4 named", blk.contains("CARRIED to this hash") && blk.contains("- a [")
|
||||
&& blk.contains("Nothing outside this list") && blk.contains("Gate 4") && !blk.contains("ledger"));
|
||||
const std::vector<Json> e2 = {{{"id", "r"}, {"hash", "H1"}, {"kind", "refuted"}, {"lenses", {"repo-functional.md"}}}};
|
||||
const std::vector<Json> rowsR = {{{"id", "r"}, {"status", "in-force"}}};
|
||||
Check("refuted entry hidden from other lenses, shown to its own", Dispositions::Block(e2, rowsR, "repo-bug-prover.md", "H1").contains("(none)")
|
||||
&& Dispositions::Block(e2, rowsR, Plan::FunctionalLens, "H1").contains("- r ["));
|
||||
fs::remove_all(td);
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
From 1111111111111111111111111111111111111111 Mon Sep 17 00:00:00 2001
|
||||
From: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
|
||||
Date: Wed, 24 Sep 2026 10:00:00 +0200
|
||||
Subject: [PATCH 1/3] add the greeting module
|
||||
|
||||
---
|
||||
hello.py | 9 +++++++++
|
||||
1 file changed, 9 insertions(+)
|
||||
create mode 100644 hello.py
|
||||
|
||||
diff --git a/hello.py b/hello.py
|
||||
new file mode 100644
|
||||
index 0000000..a1b2c3d
|
||||
--- /dev/null
|
||||
+++ b/hello.py
|
||||
@@ -0,0 +1,9 @@
|
||||
+"""Greetings."""
|
||||
+
|
||||
+
|
||||
+def greet():
|
||||
+ return "hello"
|
||||
+
|
||||
+
|
||||
+def farewell():
|
||||
+ return "bye"
|
||||
|
||||
base-commit: 0000000000000000000000000000000000000000
|
||||
--
|
||||
2.51.0
|
||||
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
From 2222222222222222222222222222222222222222 Mon Sep 17 00:00:00 2001
|
||||
From: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
|
||||
Date: Wed, 24 Sep 2026 10:05:00 +0200
|
||||
Subject: [PATCH 2/3] greet by name and document it
|
||||
|
||||
The greeting takes a name now and the farewell mirrors it; the README
|
||||
documents both calls. Callers that pass nothing still get the old text.
|
||||
|
||||
The format follows the greeting spec [1].
|
||||
|
||||
[1] https://example.org/greeting-spec
|
||||
---
|
||||
README.md | 4 ++++
|
||||
hello.py | 8 ++++++--
|
||||
2 files changed, 10 insertions(+), 2 deletions(-)
|
||||
create mode 100644 README.md
|
||||
|
||||
diff --git a/README.md b/README.md
|
||||
new file mode 100644
|
||||
index 0000000..d4e5f6a
|
||||
--- /dev/null
|
||||
+++ b/README.md
|
||||
@@ -0,0 +1,4 @@
|
||||
+# hello
|
||||
+
|
||||
+Call `greet(name)` or `farewell(name)`; without a name both return the
|
||||
+plain text.
|
||||
diff --git a/hello.py b/hello.py
|
||||
index a1b2c3d..b7c8d9e 100644
|
||||
--- a/hello.py
|
||||
+++ b/hello.py
|
||||
@@ -1,9 +1,13 @@
|
||||
"""Greetings."""
|
||||
|
||||
|
||||
-def greet():
|
||||
- return "hello"
|
||||
+def greet(name=None):
|
||||
+ if name is None:
|
||||
+ return "hello"
|
||||
+ return "hello " + name
|
||||
|
||||
|
||||
@@ -7,3 +11,5 @@ def greet():
|
||||
-def farewell():
|
||||
- return "bye"
|
||||
+def farewell(name=None):
|
||||
+ if name is None:
|
||||
+ return "bye"
|
||||
+ return "bye " + name
|
||||
|
||||
base-commit: 0000000000000000000000000000000000000000
|
||||
--
|
||||
2.51.0
|
||||
|
||||
24
tests/Goldens/fixture/commits3/0003-add-tests.patch
Normal file
24
tests/Goldens/fixture/commits3/0003-add-tests.patch
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
From 3333333333333333333333333333333333333333 Mon Sep 17 00:00:00 2001
|
||||
From: Jorijn van der Graaf <jorijnvdgraaf@catcrafts.net>
|
||||
Date: Wed, 24 Sep 2026 10:10:00 +0200
|
||||
Subject: [PATCH 3/3] add tests
|
||||
|
||||
Tests for greet.
|
||||
---
|
||||
tests/test_hello.py | 3 +++
|
||||
1 file changed, 3 insertions(+)
|
||||
create mode 100644 tests/test_hello.py
|
||||
|
||||
diff --git a/tests/test_hello.py b/tests/test_hello.py
|
||||
new file mode 100644
|
||||
index 0000000..1111111
|
||||
--- /dev/null
|
||||
+++ b/tests/test_hello.py
|
||||
@@ -0,0 +1,3 @@
|
||||
+from hello import greet
|
||||
+def test_greet():
|
||||
+ assert greet("x") == "hello x"
|
||||
|
||||
base-commit: 0000000000000000000000000000000000000000
|
||||
--
|
||||
2.51.0
|
||||
File diff suppressed because it is too large
Load diff
76
tests/Merge/main.cpp
Normal file
76
tests/Merge/main.cpp
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Merging a lens's parts: the dedupe, the cross-unit rule, the strongest
|
||||
// verdict, missing units; the matrix with units nobody reviews.
|
||||
|
||||
import std;
|
||||
import Crafter.Review;
|
||||
using namespace Crafter::Review;
|
||||
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// {item: verdict} of a merged result.
|
||||
std::map<std::string, std::string> Verdicts(const Json& merged) {
|
||||
std::map<std::string, std::string> out;
|
||||
for (const Json& c : merged.at("coverage")) out[c.at("item").get<std::string>()] = c.at("verdict").get<std::string>();
|
||||
return out;
|
||||
}
|
||||
|
||||
Json With(Json value, const Json& changes) {
|
||||
value.update(changes);
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::println("merge_parts: dedupe, cross-unit rule, strongest verdict, missing units; the matrix with not-reviewed units");
|
||||
const std::string lens = "repo-bug-prover.md";
|
||||
const Json note = {{"kind", "info"}, {"items", {"A", "B"}}, {"note", "shared"}};
|
||||
const Json found = {{"finding", "x"}, {"items", {"A", "B"}}, {"discharged", true}, {"dischargedBy", "w1"}};
|
||||
const std::vector<Json> parts = {
|
||||
{{"partLabel", "a"}, {"expectedIds", {"A"}}, {"findings", {found}}, {"notes", {note, note}}, {"coverage", {{{"item", "A"}, {"verdict", "clear"}}}}},
|
||||
{{"partLabel", "b"}, {"expectedIds", {"B"}}, {"findings", {found}}, {"notes", {note}}, {"coverage", {{{"item", "B"}, {"verdict", "clear"}}}}},
|
||||
};
|
||||
Json m = Normalize::MergeParts("L", lens, parts, {});
|
||||
Check("one note and one finding after the merge, verdict unchanged", m.at("notes").size() == 1 && m.at("findings").size() == 1 && m.at("verdict") == "SEND-READY");
|
||||
const Json pa = {{"partLabel", "L:block:A"}, {"partKind", "block"}, {"partUnit", "A"}, {"verdict", "FIX FIRST"},
|
||||
{"findings", {{{"finding", "B contradicts A"}, {"evidence", "e"}, {"proposedAction", "p"}, {"items", {"A", "B"}}, {"discharged", false}}}},
|
||||
{"notes", Json::array()}, {"coverage", {{{"item", "A"}, {"verdict", "defect"}, {"basis", "b"}}}}, {"coverageMissing", Json::array()}, {"undischarged", 1}};
|
||||
const Json pb = {{"partLabel", "L:block:B"}, {"partKind", "block"}, {"partUnit", "B"}, {"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()},
|
||||
{"coverage", {{{"item", "B"}, {"verdict", "clear"}, {"basis", "checked"}}}}, {"coverageMissing", Json::array()}};
|
||||
m = Normalize::MergeParts("L", lens, std::vector<Json>{pa, pb}, {});
|
||||
Json basisOfB;
|
||||
for (const Json& c : m.at("coverage")) {
|
||||
if (c.at("item") == "B") basisOfB = c;
|
||||
}
|
||||
const bool raised = std::ranges::any_of(m.at("normalizeLogs"), [](const Json& l) { return l.get<std::string>().contains("raised to defect"); });
|
||||
Check("B raised to defect by the finding citing it; lens FIX FIRST", basisOfB.at("verdict") == "defect" && basisOfB.at("basis").get<std::string>().starts_with("cited by finding")
|
||||
&& m.at("verdict") == "FIX FIRST" && raised);
|
||||
Json dischargedFinding = pa.at("findings").at(0);
|
||||
dischargedFinding["discharged"] = true;
|
||||
dischargedFinding["dischargedBy"] = "w1";
|
||||
const Json pc = With(pa, {{"findings", {dischargedFinding}}, {"coverage", {{{"item", "A"}, {"verdict", "clear"}, {"basis", "discharged"}}}}, {"verdict", "SEND-READY"}, {"undischarged", 0}});
|
||||
Check("a discharged finding does not raise B", Verdicts(Normalize::MergeParts("L", lens, std::vector<Json>{pc, pb}, {}))["B"] == "clear");
|
||||
const std::vector<std::string> missingA = {"A"};
|
||||
const Json m3 = Normalize::MergeParts("L", lens, std::vector<Json>{pb}, missingA);
|
||||
Check("missing unit -> coverageMissing + contract violation + verdict NOT RETURNED", m3.at("coverageMissing") == Json::array({"A"}) && !m3.at("contractViolations").empty()
|
||||
&& m3.at("verdict") == "NOT RETURNED");
|
||||
const Json pd = With(pb, {{"coverage", {{{"item", "B"}, {"verdict", "immaterial"}, {"basis", "w"}}}}});
|
||||
Check("strongest verdict per unit wins (immaterial > clear)", Verdicts(Normalize::MergeParts("L", lens, std::vector<Json>{pb, pd}, {}))["B"] == "immaterial");
|
||||
const Json result = Normalize::MergeParts("L", lens, std::vector<Json>{pa, pb}, {});
|
||||
const std::vector<std::string> notReviewed = {"p1:msg"};
|
||||
const Json mx = Normalize::Matrix(Json::array({{{"id", "A"}}, {{"id", "B"}}, {{"id", "C"}}, {{"id", "p1:msg"}}}), std::vector<Json>{result}, notReviewed);
|
||||
std::set<std::string> defects;
|
||||
for (auto it = mx.at("defects").begin(); it != mx.at("defects").end(); ++it) defects.insert(it.key());
|
||||
Check("matrix: a unit without an entry is MISSING and a hole; a not-reviewed unit is listed, never a hole; defects per unit",
|
||||
mx.at("matrix").at("C").at("L").at("verdict") == "MISSING" && mx.at("holes") == Json::array({"C"}) && mx.at("notReviewed") == Json::array({"p1:msg"})
|
||||
&& defects == std::set<std::string>{"A", "B"}, DumpCompactSorted(mx.at("holes")));
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
70
tests/Normalize/main.cpp
Normal file
70
tests/Normalize/main.cpp
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// Normalising a pass: the synthesized finding, prefix resolution, discharge,
|
||||
// contests, the vocabulary.
|
||||
|
||||
import std;
|
||||
import Crafter.Review;
|
||||
using namespace Crafter::Review;
|
||||
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
bool AnyKind(const Json& violations, std::string_view part) {
|
||||
return std::ranges::any_of(violations, [&](const Json& v) { return v.at("kind").get<std::string>().contains(part); });
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::println("normalize: synthesized finding, prefix resolution, discharge, contests, vocabulary");
|
||||
const Json inv = Json::array({{{"id", "p1:x.py#1"}}, {{"id", "p1:src/components/a.tsx#1"}}, {{"id", "p1:src/components/a.tsx#2"}}});
|
||||
std::vector<std::string> ids;
|
||||
for (const Json& u : inv) ids.push_back(u.at("id").get<std::string>());
|
||||
const std::string lens = "repo-bug-prover.md";
|
||||
const Json first = Json::array({inv[0]});
|
||||
auto run = [&](Json v, const std::vector<Json>& inForce, std::string_view lensFile = "repo-bug-prover.md") {
|
||||
return Normalize::NormalizeOutput(v, inForce, lensFile, "h", first, ids);
|
||||
};
|
||||
const Json n = run({{"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, {"coverage", {{{"item", "p1:x.py#1"}, {"verdict", "defect"}, {"basis", "x"}}}}}, {});
|
||||
Check("defect without finding -> synthesized undischarged finding, FIX FIRST", n.at("verdict") == "FIX FIRST" && !n.at("findings").empty()
|
||||
&& Dyn::Truthy(Dyn::Get(n.at("findings").at(0), "synthesized")) && n.at("undischarged") == 1);
|
||||
Check("declared SEND-READY over it -> contract violation", AnyKind(n.at("contractViolations"), "failed closed"));
|
||||
Check("prefix: a unique prefix resolves, an ambiguous one stays", Normalize::ResolveId("p1:x", ids) == ids[0]
|
||||
&& Normalize::ResolveId("p1:src/components/a.tsx#", ids) == "p1:src/components/a.tsx#");
|
||||
const Json n2 = run({{"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, {"coverage", {{{"item", "p1:x"}, {"verdict", "clear"}, {"basis", "b"}}}}}, {});
|
||||
Check("abbreviated coverage id resolved -> no MISSING", n2.at("coverageMissing").empty() && n2.at("coverage").at(0).at("item") == ids[0]);
|
||||
const Json n3 = run({{"verdict", "SEND-READY"}, {"findings", Json::array()}, {"notes", Json::array()}, {"coverage", Json::array()}}, {});
|
||||
Check("no coverage entry -> coverageMissing (NOT RETURNED)", n3.at("coverageMissing") == Json::array({ids[0]}) && !n3.at("contractViolations").empty());
|
||||
const std::vector<Json> d = {{{"id", "w1"}, {"kind", "waiver-owner"}, {"hash", "h"}}};
|
||||
auto finding = [&](const Json& extra) {
|
||||
Json f = {{"finding", "f"}, {"evidence", "e"}, {"proposedAction", "p"}, {"items", {ids[0]}}};
|
||||
f.update(extra);
|
||||
return f;
|
||||
};
|
||||
const Json n4 = run({{"verdict", "SEND-READY"}, {"findings", {finding({{"dischargedBy", "w1"}})}}, {"notes", Json::array()},
|
||||
{"coverage", {{{"item", ids[0]}, {"verdict", "clear"}, {"basis", "discharged by w1"}}}}}, d);
|
||||
Check("dischargedBy an in-force id -> discharged, SEND-READY", n4.at("verdict") == "SEND-READY" && n4.at("findings").at(0).at("discharged") == true);
|
||||
const Json n5 = run({{"verdict", "SEND-READY"}, {"findings", {finding({{"dischargedBy", "nope"}})}}, {"notes", Json::array()},
|
||||
{"coverage", {{{"item", ids[0]}, {"verdict", "clear"}, {"basis", "b"}}}}}, d);
|
||||
Check("dischargedBy an unknown id -> undischarged, FIX FIRST", n5.at("verdict") == "FIX FIRST" && n5.at("findings").at(0).at("discharged") == false);
|
||||
const Json n6 = run({{"verdict", "FIX FIRST"}, {"findings", {finding({{"contests", {{"id", "w1"}, {"why", "w"}}}})}}, {"notes", Json::array()},
|
||||
{"coverage", {{{"item", ids[0]}, {"verdict", "defect"}, {"basis", "b"}}}}}, d);
|
||||
Check("contest of an unsettled entry blocks", n6.at("verdict") == "FIX FIRST" && !n6.at("contests").empty() && n6.at("contests").at(0).at("outcome").get<std::string>().contains("CONTESTED"));
|
||||
const std::vector<Json> d2 = {{{"id", "w1"}, {"kind", "waiver-owner"}, {"hash", "h"}, {"reaffirmed", {{{"hash", "h"}}}}}};
|
||||
const Json n7 = run({{"verdict", "FIX FIRST"}, {"findings", {finding({{"contests", {{"id", "w1"}, {"why", "w"}}}})}}, {"notes", Json::array()},
|
||||
{"coverage", {{{"item", ids[0]}, {"verdict", "clear"}, {"basis", "b"}}}}}, d2);
|
||||
Check("contest of a reaffirmed entry is recorded, finding discharged (silent veto logged)", n7.at("verdict") == "SEND-READY" && n7.at("findings").at(0).at("discharged") == true
|
||||
&& AnyKind(n7.at("contractViolations"), "silent veto"));
|
||||
const Json n8 = run({{"verdict", "SEND-READY"}, {"findings", Json::array()},
|
||||
{"notes", {{{"kind", "unverified"}, {"note", "x"}, {"evidence", "e"}, {"action", "a"}, {"items", {ids[0]}}}}},
|
||||
{"coverage", {{{"item", ids[0]}, {"verdict", "n/a"}, {"basis", "unverified: login failed"}}}}}, {}, Plan::FunctionalLens);
|
||||
Check("functional: n/a with an unverified note is a pass of the contract (not clear, not a hole in the pass)", n8.at("verdict") == "SEND-READY" && n8.at("coverageMissing").empty()
|
||||
&& std::ranges::contains(Schema::NoteKinds, std::string_view("unverified")) && !std::ranges::contains(Schema::NoteKinds, std::string_view("disposition-review")));
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
97
tests/Stream/main.cpp
Normal file
97
tests/Stream/main.cpp
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
||||
|
||||
// The claude stream: its envelopes.
|
||||
|
||||
import std;
|
||||
import Crafter.Review;
|
||||
namespace fs = std::filesystem;
|
||||
using namespace Crafter::Review;
|
||||
|
||||
namespace {
|
||||
std::int32_t Failures = 0;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// The result line mutated, or dropped when the mutation returns nullopt.
|
||||
std::string Variant(const std::string& text, const std::function<std::optional<Json>(Json)>& mutate) {
|
||||
std::vector<std::string> out;
|
||||
for (const std::string& line : Dyn::SplitLines(text)) {
|
||||
if (line.starts_with('{') && Dyn::Replace(line, " ", "").contains("\"type\":\"result\"")) {
|
||||
const std::optional<Json> changed = mutate(Json::parse(line));
|
||||
if (!changed) continue;
|
||||
out.push_back(DumpSpaced(*changed));
|
||||
} else {
|
||||
out.push_back(line);
|
||||
}
|
||||
}
|
||||
return Dyn::Join(out, "\n");
|
||||
}
|
||||
|
||||
std::function<std::optional<Json>(Json)> Failed(const Json& update) {
|
||||
return [update](Json o) -> std::optional<Json> {
|
||||
o.update(update);
|
||||
o.erase("structured_output");
|
||||
return o;
|
||||
};
|
||||
}
|
||||
|
||||
std::string Status(const std::string& text, std::optional<std::int32_t> code, std::string_view err = {}) {
|
||||
return Stream::Classify(Stream::ParseStream(text), code, err).first;
|
||||
}
|
||||
}
|
||||
|
||||
int main() {
|
||||
std::println("stream envelopes");
|
||||
const std::string text = Dyn::ReadText(fs::current_path() / "tests" / "Goldens" / "fixture" / "stream-success.jsonl");
|
||||
const Json p = Stream::ParseStream(text);
|
||||
Check("success: classify ok", Stream::Classify(p, 0, "") == std::pair<std::string, std::string>{"ok", ""});
|
||||
const std::optional<Json> so = Stream::StructuredOutput(p.at("result"));
|
||||
Check("success: structured_output parsed", so && so->at("verdict") == "SEND-READY" && so->at("coverage").at(0).at("item") == "R1");
|
||||
Check("success: init parsed", Dyn::Truthy(p.at("init")) && Dyn::Get(p.at("init"), "mcp_servers") == Json::array());
|
||||
Check("success: usage from the result line + firstTurn", p.at("turns") == 2 && p.at("firstTurn").at("cache_creation_input_tokens").get<std::int64_t>() > 0
|
||||
&& p.at("usage").at("output").get<std::int64_t>() > 0 && !p.at("costUsd").is_null() && p.at("denials") == Json::array());
|
||||
const Json w = Stream::Windows(p.at("rateLimit"));
|
||||
Check("success: rate windows as percent", w.contains("five_hour") && w.at("five_hour").at("utilization").get<double>() > 0 && w.at("five_hour").at("utilization").get<double>() < 100
|
||||
&& Dyn::Truthy(w.at("seven_day").at("resetsAt")), DumpCompactSorted(w));
|
||||
Check("error_max_turns -> error", Status(Variant(text, Failed({{"subtype", "error_max_turns"}, {"is_error", true}, {"result", "Reached max turns (1)"}})), 1) == "error");
|
||||
Check("429 -> limit", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 429}, {"result", "API Error: 429 rate_limit_error"}})), 1) == "limit");
|
||||
Check("401 -> auth", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 401}, {"result", "API Error: 401 authentication_error"}})), 1) == "auth");
|
||||
const std::string noResult = Variant(text, [](Json) -> std::optional<Json> { return std::nullopt; });
|
||||
Check("403 in stderr, no result -> auth", Status(noResult, 1, "Error: 403 Forbidden") == "auth");
|
||||
Check("no result line -> error", Status(noResult, 1) == "error");
|
||||
Check("no result line + usage limit text -> limit", Status(noResult, 1, "You're out of extra usage") == "limit");
|
||||
Check("timeout flag / exit 124 -> timeout", Stream::Classify(p, std::nullopt, "", true).first == "timeout" && Stream::Classify(p, 124, "").first == "timeout");
|
||||
const std::string doubled = Variant(text, [](Json o) -> std::optional<Json> {
|
||||
o["structured_output"] = DumpSpaced(o.at("structured_output"));
|
||||
return o;
|
||||
});
|
||||
Check("double-encoded structured_output decoded", Stream::StructuredOutput(Stream::ParseStream(doubled).at("result")) == so);
|
||||
const std::string inResult = Variant(text, [](Json o) -> std::optional<Json> {
|
||||
const Json inner = o.at("structured_output");
|
||||
o.erase("structured_output");
|
||||
o["result"] = DumpSpaced(inner);
|
||||
return o;
|
||||
});
|
||||
Check("structured output inside result text decoded", Stream::StructuredOutput(Stream::ParseStream(inResult).at("result")) == so);
|
||||
const std::string noFindings = Variant(text, [](Json o) -> std::optional<Json> {
|
||||
o["structured_output"] = {{"verdict", "SEND-READY"}};
|
||||
return o;
|
||||
});
|
||||
Check("missing findings array -> error (no structured output)", Status(noFindings, 0) == "error");
|
||||
Check("credit balance too low -> credits", Status(Variant(text, Failed({{"subtype", "error_during_execution"}, {"is_error", true}, {"api_error_status", 400},
|
||||
{"result", "API Error: 400 Your credit balance is too low to access the Anthropic API."}})), 1) == "credits");
|
||||
|
||||
// The helper's spelling is a SQL call wherever it stands, and a stream
|
||||
// line is cut at \n only: a raw U+2028 in a JSON string stays inside its
|
||||
// line.
|
||||
Check("this binary's helper is a SQL call, alone, after a path, in a compound command; another verb is not",
|
||||
Stream::IsSqlCall("/app/bin/crafter-review sql <<'SQL'\nselect 1;\nSQL") && Stream::IsSqlCall("cd . && crafter-review sql 'select 1'")
|
||||
&& Stream::IsSqlCall("/x/bin/crafter.review-exe-v/crafter-review sql") && !Stream::IsSqlCall("/app/bin/crafter-review sqlx") && !Stream::IsSqlCall("/app/bin/crafter-review totp f.json"));
|
||||
const std::string split = "{\"type\":\"result\",\"subtype\":\"success\",\"is_error\":false,\"result\":\"a\u2028b\",\"structured_output\":{\"verdict\":\"SEND-READY\",\"findings\":[],\"notes\":[],\"coverage\":[]}}";
|
||||
Check("a result line with a raw U+2028 in a string is still the result line", Stream::Classify(Stream::ParseStream(split), 0, "").first == "ok");
|
||||
return Failures ? 1 : 0;
|
||||
}
|
||||
Loading…
Reference in a new issue