`crafter-review sql` hands the SQL to the driver's broker named in REVIEW_GATES_DB; the broker runs it as one transaction, rolled back, and streams the lines back with the exit code last. The client speaks protocol 3.0 over Crafter.Network's TCP client: cleartext, MD5 and SCRAM-SHA-256, one Parse per statement, the transaction status as the backstop. The splitter, the refusals, the table layout and every message work on code points; connect and socket errors read "[Errno 111] Connection refused" or "timed out". `crafter-review totp` prints "123456 (valid 17s)". No sleep after the last connect attempt; TOTP reads the clock once; a broker token that is no string gets the NO DATABASE answer and the exit mark. The goldens add the splitter, the comment stripping, the fingerprint, the refusals, the first words, the headline, the cell, table, error and notice layouts, the run's refusals and limits and 40 TOTP codes (2774 checks now). tests/PgWire runs the helper over a Postgres stand-in and compares its output with the goldens, line for line, plus the broker's protocol. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
486 lines
33 KiB
C++
486 lines
33 KiB
C++
// SPDX-License-Identifier: AGPL-3.0-only
|
|
// SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts®
|
|
|
|
// The pure pipeline against the goldens: fixture/pipeline.json holds the
|
|
// recorded answers over the fixtures next to it — the inventory, the plans,
|
|
// the keys, the prompts, the normalized results, the stream, the config.
|
|
// Each result is compared as DumpFile writes it, so a key out of place fails
|
|
// as surely as a wrong value: the dump, the shared files and the prompts are
|
|
// made of these. A deliberate change edits the goldens in the same commit.
|
|
|
|
import std;
|
|
import Crafter.Review;
|
|
namespace fs = std::filesystem;
|
|
using namespace Crafter::Review;
|
|
|
|
namespace {
|
|
std::int32_t Failures = 0;
|
|
std::int32_t Checks = 0;
|
|
|
|
void Check(bool cond, std::string_view what) {
|
|
++Checks;
|
|
if (!cond) {
|
|
std::println(std::cerr, "FAIL: {}", what);
|
|
++Failures;
|
|
}
|
|
}
|
|
|
|
// Where two texts part, for the failure message.
|
|
std::string Parting(std::string_view got, std::string_view want) {
|
|
std::size_t at = 0;
|
|
while (at < got.size() && at < want.size() && got[at] == want[at]) ++at;
|
|
auto window = [&](std::string_view text) { return Dyn::Repr(text.substr(at > 40 ? at - 40 : 0, 120)); };
|
|
return std::format("at byte {}: got {} want {}", at, window(got), window(want));
|
|
}
|
|
|
|
void Same(std::string_view got, std::string_view want, std::string_view what) {
|
|
Check(got == want, got == want ? std::string(what) : std::format("{}: {}", what, Parting(got, want)));
|
|
}
|
|
|
|
void SameJson(const Json& got, const Json& want, std::string_view what) {
|
|
Same(DumpFile(got), DumpFile(want), what);
|
|
}
|
|
|
|
std::vector<std::string> Strings(const Json& array) {
|
|
std::vector<std::string> out;
|
|
if (array.is_null()) return out;
|
|
for (const Json& item : array) out.push_back(item.get<std::string>());
|
|
return out;
|
|
}
|
|
|
|
Json ToJson(const std::vector<std::string>& values) {
|
|
Json out = Json::array();
|
|
for (const std::string& value : values) out.push_back(value);
|
|
return out;
|
|
}
|
|
|
|
Json Optional(const std::optional<std::string>& value) {
|
|
return value ? Json(*value) : Json();
|
|
}
|
|
|
|
std::optional<std::string> OptionalString(const Json& value) {
|
|
if (value.is_null()) return std::nullopt;
|
|
return value.get<std::string>();
|
|
}
|
|
|
|
Json CommitCodeJson(const Plan::CommitCode& cc) {
|
|
Json body = Json::object();
|
|
Json code = Json::object();
|
|
Json docs = Json::object();
|
|
Json message = Json::object();
|
|
Json upto = Json::object();
|
|
for (const auto& [k, v] : cc.body) body[std::to_string(k)] = v;
|
|
for (const auto& [k, v] : cc.code) code[std::to_string(k)] = v;
|
|
for (const auto& [k, v] : cc.docs) docs[std::to_string(k)] = ToJson(v);
|
|
for (const auto& [k, v] : cc.message) message[std::to_string(k)] = v;
|
|
for (const auto& [k, v] : cc.upto) upto[std::to_string(k)] = v;
|
|
return {{"body", body}, {"code", code}, {"docs", docs}, {"msg", message}, {"range", cc.range}, {"upto", upto}, {"base", cc.base}};
|
|
}
|
|
|
|
void CheckSeries(const std::string& name, const Json& golden) {
|
|
const std::string dir = golden.at("dir").get<std::string>();
|
|
const Inventory::Built built = Inventory::Build("commits", dir);
|
|
SameJson(built.out, golden.at("inventory"), std::format("{}: inventory", name));
|
|
SameJson(ToJson(built.lines), golden.at("lines"), std::format("{}: lines", name));
|
|
Same(Artifact::ArtifactHash("commits", dir), golden.at("hash").get<std::string>(), std::format("{}: artifact hash", name));
|
|
const Plan::CommitCode cc = Plan::CommitCodeOf(Artifact::PatchTexts(dir));
|
|
SameJson(CommitCodeJson(cc), golden.at("commitCode"), std::format("{}: commit_code", name));
|
|
const Json& units = built.out.at("units");
|
|
for (auto it = golden.at("codeKeyParts").begin(); it != golden.at("codeKeyParts").end(); ++it) {
|
|
const auto [ident, deps] = Plan::CodeKeyParts(it.key(), cc);
|
|
SameJson(Json::array({ident, ToJson(deps)}), it.value(), std::format("{}: code_key_parts {}", name, it.key()));
|
|
}
|
|
std::map<std::string, Json> blocksByLens;
|
|
std::map<std::string, Json> wholeByLens;
|
|
const std::vector<std::string> exclude = {"**/*.test.*", "docs/**", "tests/**"};
|
|
for (const Plan::Lens& lens : Plan::Lenses) {
|
|
const Plan::LensPlan plan = Plan::PlanLens(lens.file, units, built.lines, {}, exclude);
|
|
Json blocks = Json::array();
|
|
for (const Json& block : plan.blocks) blocks.push_back(block);
|
|
blocksByLens[std::string(lens.file)] = blocks;
|
|
wholeByLens[std::string(lens.file)] = plan.whole;
|
|
}
|
|
for (auto it = golden.at("keys").begin(); it != golden.at("keys").end(); ++it) {
|
|
const std::vector<std::string> tag = Dyn::Split(it.key(), "|");
|
|
const std::string& lensFile = tag[0];
|
|
const std::string environment = tag[1] == "None" ? std::string() : tag[1];
|
|
const std::vector<std::string> ids = tag[2].empty() ? std::vector<std::string>() : Dyn::Split(tag[2], ",");
|
|
Json keys = Json::object();
|
|
for (const Json& job : blocksByLens[lensFile]) {
|
|
if (job.at("kind") != "block") continue;
|
|
const std::string unit = job.at("unit").get<std::string>();
|
|
const Json* found = nullptr;
|
|
for (const Json& u : units) {
|
|
if (u.at("id") == unit) found = &u;
|
|
}
|
|
keys[unit] = Optional(Plan::BlockReuseKey(job, lensFile, Plan::UnitText(built.lines, *found), cc, ids, std::string(64, 'c'), 1, environment));
|
|
}
|
|
SameJson(keys, it.value(), std::format("{}: block keys {}", name, it.key()));
|
|
const Json& whole = wholeByLens[lensFile];
|
|
const Json wholeKey = whole.is_null() ? Json() : Optional(Plan::WholeCodeKey(whole, lensFile, cc, ids, std::string(64, 'c'), 1, environment));
|
|
SameJson(wholeKey, golden.at("wholeKeys").at(it.key()), std::format("{}: whole key {}", name, it.key()));
|
|
}
|
|
if (!golden.contains("plans")) return;
|
|
Same(Artifact::PatchHead(dir).value_or("None"), golden.at("head").is_null() ? "None" : golden.at("head").get<std::string>(), std::format("{}: patch_head", name));
|
|
SameJson(ToJson(Artifact::PatchFiles(dir)), golden.at("patchFiles"), std::format("{}: patch_files", name));
|
|
for (const auto& [k, text] : Artifact::PatchTexts(dir)) {
|
|
const Plan::PatchParts parts = Plan::PartsOf(text);
|
|
SameJson(Json::array({parts.hasBody, parts.code, parts.base, ToJson(parts.docs), parts.message}), golden.at("patchParts").at(std::to_string(k)),
|
|
std::format("{}: patch_parts {}", name, k));
|
|
}
|
|
for (std::size_t i = 0; i < units.size(); ++i) {
|
|
const Json& want = golden.at("units").at(i);
|
|
const std::string id = units[i].at("id").get<std::string>();
|
|
const std::string text = Plan::UnitText(built.lines, units[i]);
|
|
Same(Plan::UnitKind(units[i]), want.at("kind").get<std::string>(), std::format("{}: unit_kind {}", name, id));
|
|
Check(Plan::IsDocUnit(id) == want.at("doc").get<bool>(), std::format("{}: is_doc_unit {}", name, id));
|
|
SameJson(Optional(Plan::UnitPath(id)), want.at("path"), std::format("{}: unit_path {}", name, id));
|
|
Same(text, want.at("text").get<std::string>(), std::format("{}: unit_text {}", name, id));
|
|
Same(Plan::CodeNorm(text), want.at("norm").get<std::string>(), std::format("{}: code_norm {}", name, id));
|
|
}
|
|
std::vector<std::string> all;
|
|
std::vector<std::string> statics;
|
|
for (const Plan::Lens& lens : Plan::Lenses) {
|
|
all.emplace_back(lens.file);
|
|
if (lens.file != Plan::FunctionalLens) statics.emplace_back(lens.file);
|
|
}
|
|
const std::vector<std::string> functional = {std::string(Plan::FunctionalLens)};
|
|
SameJson(ToJson(Plan::NotReviewed(units, built.lines, all)), golden.at("notReviewed").at("all"), std::format("{}: not_reviewed all", name));
|
|
SameJson(ToJson(Plan::NotReviewed(units, built.lines, statics)), golden.at("notReviewed").at("static"), std::format("{}: not_reviewed static", name));
|
|
SameJson(ToJson(Plan::NotReviewed(units, built.lines, functional, exclude)), golden.at("notReviewed").at("functional"), std::format("{}: not_reviewed functional", name));
|
|
std::vector<Json> jobs;
|
|
for (const Plan::Lens& lens : Plan::Lenses) {
|
|
for (const bool excluded : {false, true}) {
|
|
const Plan::LensPlan plan = Plan::PlanLens(lens.file, units, built.lines, {}, excluded ? std::span<const std::string>(exclude) : std::span<const std::string>());
|
|
Json blocks = Json::array();
|
|
for (const Json& block : plan.blocks) blocks.push_back(block);
|
|
const std::string key = std::format("{}{}", lens.file, excluded ? ":exclude" : "");
|
|
SameJson(Json{{"whole", plan.whole}, {"blocks", blocks}}, golden.at("plans").at(key), std::format("{}: plan_lens {}", name, key));
|
|
if (!excluded) {
|
|
if (!plan.whole.is_null()) jobs.push_back(plan.whole);
|
|
jobs.insert(jobs.end(), plan.blocks.begin(), plan.blocks.end());
|
|
}
|
|
}
|
|
}
|
|
SameJson(Plan::Estimate(jobs, 123457, {{"repo-better-solution", 4100}, {"repo-functional", 9000}}), golden.at("estimate"), std::format("{}: estimate", name));
|
|
}
|
|
|
|
void CheckPlan(const Json& g) {
|
|
for (const Json& entry : g.at("unitIds")) {
|
|
const std::string id = entry.at("id").get<std::string>();
|
|
Same(Plan::UnitKindOf(id), entry.at("kind").get<std::string>(), std::format("unit_kind {}", Dyn::Repr(id)));
|
|
Check(Plan::IsDocUnit(id) == entry.at("doc").get<bool>(), std::format("is_doc_unit {}", Dyn::Repr(id)));
|
|
SameJson(Optional(Plan::UnitPath(id)), entry.at("path"), std::format("unit_path {}", Dyn::Repr(id)));
|
|
}
|
|
for (const Json& entry : g.at("excludedBy")) {
|
|
const std::vector<std::string> globs = Strings(entry.at(1));
|
|
SameJson(Optional(Plan::ExcludedBy(entry.at(0).get<std::string>(), globs)), entry.at(2), std::format("excluded_by {}", DumpCompactSorted(entry)));
|
|
}
|
|
for (const Json& entry : g.at("gates")) {
|
|
const Json& args = entry.at(0);
|
|
const std::vector<std::string> only = args.contains("only") ? Strings(args.at("only")) : std::vector<std::string>();
|
|
const std::vector<std::string> without = args.contains("without") ? Strings(args.at("without")) : std::vector<std::string>();
|
|
const std::vector<std::string> off = args.contains("off") ? Strings(args.at("off")) : std::vector<std::string>();
|
|
Json got = Json::array();
|
|
for (const Plan::Gate& gate : Plan::GatesFor(only, without, off)) {
|
|
Json lenses = Json::array();
|
|
for (const std::string_view lens : gate.lenses) lenses.push_back(lens);
|
|
got.push_back(Json::array({gate.title, lenses}));
|
|
}
|
|
SameJson(got, entry.at(1), std::format("gates_for {}", DumpCompactSorted(args)));
|
|
}
|
|
for (const Json& entry : g.at("codeNorm")) {
|
|
Same(Plan::CodeNorm(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("code_norm {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
}
|
|
SameJson(Optional(Plan::ReuseKey({{"kind", "block"}, {"unit", "U"}}, "repo-bug-prover.md", "T", {"d", "b"}, {"w2", "w1"}, "cs", 1)), g.at("reuseKey"), "reuse_key");
|
|
for (const Json& entry : g.at("readsOf")) Check(Plan::ReadsOf(entry.at(0)) == entry.at(1).get<std::int64_t>(), std::format("reads_of {}", DumpCompactSorted(entry.at(0))));
|
|
for (const Json& entry : g.at("readIn")) {
|
|
Check(Plan::ReadIn(entry.at(0), entry.at(1).get<std::string>(), entry.at(2).get<std::int64_t>()) == entry.at(3).get<bool>(), std::format("read_in {}", DumpCompactSorted(entry)));
|
|
}
|
|
SameJson(Plan::Rebind(g.at("rebind").at(0), "p2:hello.py#3"), g.at("rebind").at(1), "rebind");
|
|
}
|
|
|
|
void CheckPrompts(const Json& g, const Json& series) {
|
|
const Json& p = g.at("prompts");
|
|
Same(Prompt::SepRun, p.at("separators").at(0).get<std::string>(), "SEP_RUN");
|
|
Same(Prompt::SepLens, p.at("separators").at(1).get<std::string>(), "SEP_LENS");
|
|
Same(Prompt::SepTail, p.at("separators").at(2).get<std::string>(), "SEP_TAIL");
|
|
Same(Prompt::GenericVocab, p.at("genericVocab").get<std::string>(), "GENERIC_VOCAB");
|
|
Same(Prompt::WholeFirstRepo, p.at("wholeText").at("first-repo").get<std::string>(), "WHOLE_TEXT first-repo");
|
|
Same(Prompt::WholeFunctional, p.at("wholeText").at("functional").get<std::string>(), "WHOLE_TEXT functional");
|
|
Same(Prompt::FunctionalBlockHint, p.at("blockHint").get<std::string>(), "FUNCTIONAL_BLOCK_HINT");
|
|
Same(Prompt::ShotRules, p.at("shotRules").get<std::string>(), "SHOT_RULES");
|
|
Same(Prompt::HintOutside, p.at("kindHints").at("outside").get<std::string>(), "KIND_HINTS_REPO outside");
|
|
Same(Prompt::HintAbsent, p.at("kindHints").at("absent").get<std::string>(), "KIND_HINTS_REPO absent");
|
|
Same(Prompt::SystemText(), p.at("systemText").get<std::string>(), "system_text");
|
|
for (auto it = p.at("charterSha").begin(); it != p.at("charterSha").end(); ++it) {
|
|
Same(Plan::Sha(Prompt::Charter(it.key())), it.value().get<std::string>(), std::format("charter sha {}", it.key()));
|
|
}
|
|
for (const Json& entry : p.at("runLayer")) {
|
|
const Json& s = series.at(entry.at("series").get<std::string>());
|
|
const Json& args = entry.at("args");
|
|
const std::vector<std::string> trees = Strings(args.at("trees"));
|
|
const std::vector<std::string> context = Strings(args.at("context_paths"));
|
|
const std::vector<std::string> lines = Strings(s.at("lines"));
|
|
Same(Prompt::RunLayer(args.at("artifact_rel").get<std::string>(), args.at("base").get<std::string>(), args.at("date").get<std::string>(), trees,
|
|
s.at("hash").get<std::string>(), lines, s.at("inventory").at("units"), context, args.at("repo_name").get<std::string>()),
|
|
entry.at("text").get<std::string>(), std::format("run_layer {} {}", entry.at("series").get<std::string>(), DumpCompactSorted(args)));
|
|
}
|
|
for (const Json& entry : p.at("appBlock")) {
|
|
Same(Prompt::AppBlock(entry.at("functional"), OptionalString(entry.at("fixture")), OptionalString(entry.at("totp")), entry.at("slots").get<bool>()),
|
|
entry.at("text").get<std::string>(), std::format("app_block {}", DumpCompactSorted(entry.at("functional"))));
|
|
}
|
|
for (const Json& entry : p.at("stackText")) {
|
|
Same(Prompt::StackText(entry.at("stack"), OptionalString(entry.at("totp"))), entry.at("text").get<std::string>(), "stack_text");
|
|
}
|
|
for (const Json& entry : p.at("dbBlock")) {
|
|
const std::optional<bool> bypass = entry.at("bypass").is_null() ? std::nullopt : std::optional<bool>(entry.at("bypass").get<bool>());
|
|
Same(Prompt::DbBlock(entry.at("supabase"), "/app/bin/crafter-review sql", OptionalString(entry.at("user")), entry.at("withApp").get<bool>(),
|
|
OptionalString(entry.at("checked")), bypass),
|
|
entry.at("text").get<std::string>(), std::format("db_block {}", DumpCompactSorted(entry.at("supabase"))));
|
|
}
|
|
for (const Json& entry : p.at("lensLayer")) {
|
|
const std::string lensFile = entry.at("lensFile").get<std::string>();
|
|
Same(Prompt::LensLayer(lensFile, Prompt::Charter(lensFile), "DISPOSITIONS BLOCK", OptionalString(entry.at("app")).value_or(""),
|
|
OptionalString(entry.at("summary")).value_or(""), OptionalString(entry.at("db")).value_or("")),
|
|
entry.at("text").get<std::string>(), std::format("lens_layer {}", lensFile));
|
|
}
|
|
for (const Json& entry : p.at("tails")) {
|
|
const Json& s = series.at(entry.at("series").get<std::string>());
|
|
const Json& plan = s.at("plans").at(entry.at("plan").get<std::string>());
|
|
const Json* job = plan.at("whole").is_object() && plan.at("whole").at("label") == entry.at("label") ? &plan.at("whole") : nullptr;
|
|
for (const Json& block : plan.at("blocks")) {
|
|
if (block.at("label") == entry.at("label")) job = █
|
|
}
|
|
const std::vector<std::string> lines = Strings(s.at("lines"));
|
|
Same(Prompt::Tail(*job, s.at("inventory").at("units"), lines, OptionalString(entry.at("summary")).value_or("")), entry.at("text").get<std::string>(),
|
|
std::format("tail {} {}", entry.at("series").get<std::string>(), entry.at("label").get<std::string>()));
|
|
}
|
|
for (const Json& entry : p.at("userPrompt")) {
|
|
Same(Prompt::UserPrompt(entry.at("tail").get<std::string>(), entry.at("job"), OptionalString(entry.at("stack")).value_or("")), entry.at("text").get<std::string>(),
|
|
std::format("user_prompt {}", DumpCompactSorted(entry.at("job"))));
|
|
}
|
|
Same(Prompt::SystemPrompt("S", "R", "L"), p.at("systemPrompt").get<std::string>(), "system_prompt");
|
|
}
|
|
|
|
void CheckDispositions(const Json& g) {
|
|
const Json& d = g.at("dispositions");
|
|
const std::vector<Json> entries(d.at("entries").begin(), d.at("entries").end());
|
|
const std::vector<Json> rows = Dispositions::Classify(entries, "H1", d.at("dir").get<std::string>());
|
|
SameJson(Json(rows), d.at("rows"), "dispositions classify");
|
|
const std::vector<Json> inForce = Dispositions::InForce(entries, rows);
|
|
SameJson(Json(inForce), d.at("inForce"), "dispositions in_force");
|
|
for (auto it = d.at("blocks").begin(); it != d.at("blocks").end(); ++it) {
|
|
Same(Dispositions::Block(inForce, rows, it.key(), "H1"), it.value().get<std::string>(), std::format("dispositions block {}", it.key()));
|
|
}
|
|
Same(Dispositions::Block({}, {}, Plan::FunctionalLens, "H9"), d.at("empty").get<std::string>(), "dispositions block (none)");
|
|
const std::vector<Json> two(entries.begin() + 1, entries.begin() + 3);
|
|
SameJson(Json(Dispositions::Classify(two, "H9", fs::path(d.at("dir").get<std::string>()) / "0001-a.patch")), d.at("rowsFile"), "dispositions classify (a file)");
|
|
}
|
|
|
|
void CheckNormalize(const Json& g) {
|
|
std::int32_t n = 0;
|
|
for (const Json& entry : g.at("normalize")) {
|
|
Json v = entry.at("v");
|
|
const std::vector<Json> inForce(entry.at("inForce").begin(), entry.at("inForce").end());
|
|
const std::vector<std::string> allIds = Strings(entry.at("allIds"));
|
|
const Json result = Normalize::NormalizeOutput(v, inForce, entry.at("lensFile").get<std::string>(), entry.at("hash").get<std::string>(), entry.at("inventory"), allIds);
|
|
SameJson(result, entry.at("result"), std::format("normalize case {}", n));
|
|
SameJson(v, entry.at("vAfter"), std::format("normalize case {}: the output's notes after", n));
|
|
++n;
|
|
}
|
|
for (const Json& entry : g.at("resolveId")) {
|
|
SameJson(Normalize::ResolveId(entry.at(0), Strings(entry.at(1))), entry.at(2), std::format("resolve_id {}", DumpCompactSorted(entry.at(0))));
|
|
}
|
|
n = 0;
|
|
for (const Json& entry : g.at("merge")) {
|
|
const std::vector<Json> parts(entry.at("parts").begin(), entry.at("parts").end());
|
|
SameJson(Normalize::MergeParts(entry.at("lens").get<std::string>(), entry.at("lensFile").get<std::string>(), parts, Strings(entry.at("missing")), entry.at("stopped")),
|
|
entry.at("result"), std::format("merge_parts case {}", n++));
|
|
}
|
|
n = 0;
|
|
for (const Json& entry : g.at("matrix")) {
|
|
const std::vector<Json> results(entry.at("results").begin(), entry.at("results").end());
|
|
SameJson(Normalize::Matrix(entry.at("inventory"), results, Strings(entry.at("notReviewed"))), entry.at("result"), std::format("matrix case {}", n++));
|
|
}
|
|
for (const Json& entry : g.at("dedupe")) {
|
|
const std::vector<Json> items(entry.at(0).begin(), entry.at(0).end());
|
|
SameJson(Json(Normalize::Dedupe(items)), entry.at(1), "dedupe");
|
|
}
|
|
}
|
|
|
|
void CheckStream(const Json& g) {
|
|
const Json& s = g.at("stream");
|
|
std::map<std::string, Json> parsed;
|
|
for (auto it = s.at("streams").begin(); it != s.at("streams").end(); ++it) {
|
|
parsed[it.key()] = Stream::ParseStream(it.value().get<std::string>());
|
|
SameJson(parsed[it.key()], s.at("parsed").at(it.key()), std::format("parse_stream {}", it.key()));
|
|
const std::optional<Json> structured = Stream::StructuredOutput(parsed[it.key()].at("result"));
|
|
SameJson(structured ? *structured : Json(), s.at("structured").at(it.key()), std::format("structured_output {}", it.key()));
|
|
}
|
|
for (const Json& entry : s.at("classify")) {
|
|
const std::optional<std::int32_t> code = entry.at(1).is_null() ? std::nullopt : std::optional<std::int32_t>(entry.at(1).get<std::int32_t>());
|
|
const auto [status, reason] = Stream::Classify(parsed.at(entry.at(0).get<std::string>()), code, entry.at(2).get<std::string>(), entry.at(3).get<bool>());
|
|
SameJson(Json::array({status, reason}), entry.at(4), std::format("classify {} {}", entry.at(0).get<std::string>(), DumpCompactSorted(entry.at(2))));
|
|
}
|
|
for (const Json& entry : s.at("windows")) SameJson(Stream::Windows(entry.at(0)), entry.at(1), std::format("windows {}", DumpCompactSorted(entry.at(0))));
|
|
for (const Json& entry : s.at("structuredOf")) {
|
|
const std::optional<Json> structured = Stream::StructuredOutput(entry.at(0));
|
|
SameJson(structured ? *structured : Json(), entry.at(1), std::format("structured_output {}", DumpCompactSorted(entry.at(0))));
|
|
}
|
|
for (const Json& entry : g.at("patterns")) {
|
|
const std::string text = entry.at(0).get<std::string>();
|
|
Check(Stream::LimitText(text) == entry.at(1).get<bool>(), std::format("LIMIT_RE {}", Dyn::Repr(text)));
|
|
Check(Stream::AuthText(text) == entry.at(2).get<bool>(), std::format("AUTH_RE {}", Dyn::Repr(text)));
|
|
Check(Stream::CreditText(text) == entry.at(3).get<bool>(), std::format("CREDIT_RE {}", Dyn::Repr(text)));
|
|
Check(Stream::IsSqlCall(text) == entry.at(4).get<bool>(), std::format("SQL_CALL {}", Dyn::Repr(text)));
|
|
Check(Stream::HasAnsweredLine(text) == entry.at(5).get<bool>(), std::format("ANSWERED_LINE {}", Dyn::Repr(text)));
|
|
}
|
|
}
|
|
|
|
void CheckState(const Json& g) {
|
|
const Json& s = g.at("state");
|
|
std::int32_t n = 0;
|
|
for (const Json& entry : s.at("merge")) {
|
|
SameJson(State::MergeState(entry.at("disk"), entry.at("sets"), Strings(entry.at("drops")), entry.at("read")), entry.at("result"), std::format("merge_state case {}", n++));
|
|
}
|
|
n = 0;
|
|
for (const Json& entry : s.at("usage")) {
|
|
const std::vector<Json> records(entry.at(0).begin(), entry.at(0).end());
|
|
SameJson(State::UsageTotals(records), entry.at(1), std::format("usage_totals case {}", n++));
|
|
}
|
|
for (const Json& entry : s.at("describe")) Same(State::Describe(entry.at(0)), entry.at(1).get<std::string>(), std::format("_describe {}", DumpCompactSorted(entry.at(0))));
|
|
n = 0;
|
|
for (const Json& entry : s.at("summary")) Same(State::Summary(entry.at(0)), entry.at(1).get<std::string>(), std::format("summary case {}", n++));
|
|
}
|
|
|
|
std::string Spelled(std::string text, const fs::path& repo) {
|
|
text = Dyn::Replace(text, repo.string(), "<repo>");
|
|
return Dyn::Replace(text, std::format("`{} slots`", Invocation()), "`<invocation> slots`");
|
|
}
|
|
|
|
void CheckConfig(const Json& g) {
|
|
const Json& c = g.at("config");
|
|
const fs::path scratch = fs::temp_directory_path() / std::format("crafter-review-goldens-{}", Platform::PublishedPid());
|
|
for (const Json& entry : c.at("cases")) {
|
|
const std::string name = entry.at("name").get<std::string>();
|
|
const fs::path repo = scratch / name;
|
|
fs::remove_all(repo);
|
|
fs::create_directories(repo);
|
|
if (!entry.at("body").is_null()) {
|
|
fs::create_directories(repo / ".review-gates");
|
|
std::ofstream(repo / ".review-gates" / "config.json", std::ios::binary) << entry.at("body").get<std::string>();
|
|
}
|
|
const Config::Loaded loaded = Config::Load(repo);
|
|
std::vector<std::string> problems;
|
|
for (const std::string& problem : loaded.problems) problems.push_back(Spelled(problem, repo));
|
|
SameJson(ToJson(problems), entry.at("problems"), std::format("config {}: problems", name));
|
|
SameJson(Json::parse(Spelled(DumpFile(Config::Redacted(loaded.config)), repo)), entry.at("config"), std::format("config {}: redacted", name));
|
|
const Json& path = Dyn::Get(loaded.config, "_path");
|
|
SameJson(path.is_null() ? Json() : Json(Spelled(path.get<std::string>(), repo)), entry.at("path"), std::format("config {}: _path", name));
|
|
Check(Dyn::Has(loaded.config, "_path") == entry.at("hasPath").get<bool>(), std::format("config {}: _path present", name));
|
|
std::vector<std::string> warnings;
|
|
for (const Json& warning : Dyn::Or(Dyn::Get(loaded.config, "_warnings"), Dyn::EmptyList())) warnings.push_back(Spelled(warning.get<std::string>(), repo));
|
|
SameJson(ToJson(warnings), entry.at("warnings"), std::format("config {}: warnings", name));
|
|
SameJson(ToJson(Config::LensesOff(loaded.config)), entry.at("lensesOff"), std::format("config {}: lenses_off", name));
|
|
SameJson(Config::Environments(loaded.config), entry.at("environments"), std::format("config {}: environments", name));
|
|
}
|
|
{
|
|
const fs::path repo = scratch / "missing";
|
|
fs::create_directories(repo);
|
|
std::vector<std::string> problems;
|
|
for (const std::string& problem : Config::Load(repo, repo / "none.json").problems) problems.push_back(Spelled(problem, repo));
|
|
SameJson(ToJson(problems), c.at("missing"), "config --config to a missing file");
|
|
}
|
|
fs::remove_all(scratch);
|
|
SameJson(Config::Defaults(), c.at("defaults"), "config DEFAULTS");
|
|
SameJson(Config::Example(), c.at("example"), "config example()");
|
|
SameJson(ToJson(Config::Validate(Config::Example())), c.at("exampleProblems"), "config validate(example())");
|
|
for (const Json& entry : c.at("denied")) Check(Tools::Denied(entry.at(0).get<std::string>()) == entry.at(1).get<bool>(), std::format("denied {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
const Config::Slots slots = Config::SlotStacks({{"slots", {{"dir", "slots"}}}}, c.at("slotStacks").at("dir").get<std::string>());
|
|
SameJson(slots.stacks, c.at("slotStacks").at("stacks"), "slot_stacks stacks");
|
|
SameJson(ToJson(slots.problems), c.at("slotStacks").at("problems"), "slot_stacks problems");
|
|
const std::array<std::int64_t, 2> oneThree = {1, 3};
|
|
const std::array<std::int64_t, 1> one = {1};
|
|
const std::array<std::int64_t, 1> two = {2};
|
|
SameJson(ToJson(Config::SlotCommands({{"slots", {{"start", "up {n} {n}"}}}}, "start", oneThree)), c.at("slotCommands").at(0), "slot_commands start");
|
|
SameJson(ToJson(Config::SlotCommands({{"slots", Json::object()}}, "stop", one)), c.at("slotCommands").at(1), "slot_commands none");
|
|
SameJson(ToJson(Config::SlotCommands(Json::object(), "start", two)), c.at("slotCommands").at(2), "slot_commands no slots");
|
|
for (const Json& entry : c.at("checkUrl")) SameJson(Optional(PgWire::CheckUrl(entry.at(0))), entry.at(1), std::format("check_url {}", DumpCompactSorted(entry.at(0))));
|
|
for (const Json& entry : c.at("redact")) Same(PgWire::Redact(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("redact {}", entry.at(0).get<std::string>()));
|
|
for (const Json& entry : c.at("where")) Same(PgWire::Where(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("where {}", entry.at(0).get<std::string>()));
|
|
}
|
|
|
|
PgWire::Fields FieldsOf(const Json& dict) {
|
|
PgWire::Fields out;
|
|
for (auto it = dict.begin(); it != dict.end(); ++it) out[it.key().at(0)] = it.value().get<std::string>();
|
|
return out;
|
|
}
|
|
|
|
std::vector<std::vector<std::optional<std::string>>> RowsOf(const Json& rows) {
|
|
std::vector<std::vector<std::optional<std::string>>> out;
|
|
for (const Json& row : rows) {
|
|
std::vector<std::optional<std::string>> cells;
|
|
for (const Json& cell : row) cells.push_back(OptionalString(cell));
|
|
out.push_back(std::move(cells));
|
|
}
|
|
return out;
|
|
}
|
|
|
|
void CheckPgWire(const Json& g) {
|
|
const Json& p = g.at("pgwire");
|
|
for (const Json& entry : p.at("split")) SameJson(ToJson(PgWire::SplitStatements(entry.at(0).get<std::string>())), entry.at(1), std::format("split_statements {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
for (const Json& entry : p.at("stripComments")) Same(PgWire::StripComments(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("strip_comments {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
for (const Json& entry : p.at("fingerprint")) Same(PgWire::Fingerprint(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("fingerprint {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
for (const Json& entry : p.at("refused")) SameJson(Optional(PgWire::Refused(entry.at(0).get<std::string>())), entry.at(1), std::format("refused {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
for (const Json& entry : p.at("firstWords")) SameJson(ToJson(PgWire::FirstWords(entry.at(0).get<std::string>(), 4)), entry.at(1), std::format("first_words {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
for (const Json& entry : p.at("headline")) Same(PgWire::Headline(entry.at(0).get<std::string>()), entry.at(1).get<std::string>(), std::format("headline {}", Dyn::Repr(entry.at(0).get<std::string>())));
|
|
for (const Json& entry : p.at("cell")) Same(PgWire::Cell(OptionalString(entry.at(0))), entry.at(1).get<std::string>(), std::format("_cell {}", DumpCompactSorted(entry.at(0))));
|
|
for (const Json& entry : p.at("table")) Same(PgWire::Table(Strings(entry.at(0)), RowsOf(entry.at(1))), entry.at(2).get<std::string>(), std::format("table {}", DumpCompactSorted(entry.at(0))));
|
|
for (const Json& entry : p.at("errorText")) {
|
|
Same(PgWire::ErrorText(FieldsOf(entry.at(0)), OptionalString(entry.at(1)).value_or("")), entry.at(2).get<std::string>(), std::format("error_text {}", DumpCompactSorted(entry.at(0))));
|
|
}
|
|
for (const Json& entry : p.at("noticeText")) Same(PgWire::NoticeText(FieldsOf(entry.at(0))), entry.at(1).get<std::string>(), std::format("notice_text {}", DumpCompactSorted(entry.at(0))));
|
|
for (const Json& entry : p.at("runs")) {
|
|
if (!entry.contains("noServer")) continue;
|
|
std::string sql;
|
|
if (entry.at("sql").is_object()) {
|
|
const std::string unit = entry.at("sql").at("repeat").get<std::string>();
|
|
const auto length = entry.at("sql").at("length").get<std::size_t>();
|
|
while (sql.size() < length) sql += unit;
|
|
sql.resize(length);
|
|
} else {
|
|
sql = entry.at("sql").get<std::string>();
|
|
}
|
|
std::vector<std::string> lines;
|
|
const std::int32_t code = PgWire::RunSql("postgresql://u:pw@127.0.0.1:1/db", sql, [&](const std::string& line) { lines.push_back(line); });
|
|
SameJson(Json::array({code, ToJson(lines)}), Json::array({entry.at("code"), entry.at("out")}), std::format("run (no server) {}", Dyn::Repr(Dyn::Slice(sql, 0, 40))));
|
|
}
|
|
std::vector<std::string> hosted;
|
|
const std::int32_t code = PgWire::RunSql("postgresql://u@db.x.supabase.co/x", "select 1", [&](const std::string& line) { hosted.push_back(line); });
|
|
SameJson(Json::array({code, ToJson(hosted)}), p.at("hosted"), "run (a hosted database)");
|
|
for (const Json& entry : p.at("totp")) {
|
|
const Totp::Code got = Totp::At(entry.at(0).get<std::string>(), entry.at(1).get<double>());
|
|
SameJson(Json::array({got.code, got.left}), entry.at(2), std::format("totp {} at {}", entry.at(0).get<std::string>(), Dyn::ReprValue(entry.at(1))));
|
|
}
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
SetHome(fs::current_path());
|
|
const Json g = Dyn::LoadJsonFile(fs::current_path() / "tests" / "Goldens" / "fixture" / "pipeline.json");
|
|
Same(Schema::SchemaJson("block"), g.at("schema").at("block").get<std::string>(), "schema block");
|
|
Same(Schema::SchemaJson("whole"), g.at("schema").at("whole").get<std::string>(), "schema whole");
|
|
for (auto it = g.at("series").begin(); it != g.at("series").end(); ++it) CheckSeries(it.key(), it.value());
|
|
CheckPlan(g);
|
|
CheckPrompts(g, g.at("series"));
|
|
CheckDispositions(g);
|
|
CheckNormalize(g);
|
|
CheckStream(g);
|
|
CheckState(g);
|
|
CheckConfig(g);
|
|
CheckPgWire(g);
|
|
std::println("{} checks against the goldens", Checks);
|
|
if (Failures) std::println(std::cerr, "{} failed", Failures);
|
|
return Failures ? 1 : 0;
|
|
}
|