Crafter.Review/tests/Config/main.cpp
Jorijn van der Graaf d5e54ffeb3
feat(config): modes - a mode is an artifact kind, the lenses it runs and its effort
The config defines the modes (modes.<name>): the kind of artifact the mode
reviews - reply, series or commits, the three shipped partitions; a kind
is a mode of its own name - the lenses it runs, in its kind's gate order,
and the effort it runs at, else the kind's default (max for a reply or a
series, xhigh for a commit range). `run --mode` names any of them, the
dump records mode and kind, and everything that decided by the mode's
name - the partition and hash, the prompts, the schemas, the disposition
and reuse-key rules, what the CLI requires - decides by the kind.

Every lens stays off until a mode lists it; lenses.<name>.run is retired
with a warning that says so, and a lens of another kind, a name that is
no lens, a lens listed twice and an unknown effort are config problems.
The defaults run no lens; the template ships the three kinds as modes
with every lens of their kind at the kind's effort, to trim (the owner's
ruling of 2026-11-01), and init says so as it writes it - a kept config
is the repository's own, and only one that lists no lens gets a step.

The readers of the retired key follow: the CI smoke config, the shipped
skill, the procedure (its description of the config as well as its
mechanics), the manifest template, the run's messages and the comments
that named the three modes. The run lock, the run numbers and the dump
names are the kind's, so one full run per hash holds across the modes
of a kind and their rounds count on together; the dump says which mode
ran. The database broker starts for a lens the mode runs, as every other
reader of the lenses off reads them. A named mode runs through the CLI
in the kernel-modes test.
2026-11-02 00:29:00 +01:00

247 lines
24 KiB
C++

// 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();
auto noLensOn = [](const std::vector<std::string>& problems) { return problems.size() == 1 && problems[0].starts_with("modes: no mode lists a lens"); };
Check("no file: the defaults, _path None, and the one problem of a repository that turns no lens on (2026-10-31)", noLensOn(loaded.problems) && loaded.config.at("aiTrailers") == "forbid"
&& loaded.config.at("functional").at("appUrl") == "http://localhost:8080" && loaded.config.at("_path").is_null(), Show(loaded.problems));
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); };
// A config under test lists the bug-prover in the commits mode unless it
// defines modes itself: every lens is off until a mode lists it, and a
// config with none listed is a problem of its own (checked below).
auto bad = [&](Json value) {
if (!value.contains("modes")) value["modes"] = {{"commits", {{"lenses", {"repo-bug-prover"}}}}};
put(value);
return load().problems;
};
put({{"base", "origin/dev"}, {"aiTrailers", "allow"}, {"allowDirty", {"supabase/config.toml"}}, {"modes", {{"commits", {{"lenses", {"repo-bug-prover"}}}}}},
{"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"}},
{"modes", {{"commits", {{"lenses", {"repo-functional"}}}}}}});
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));
// A path found on the host is not there inside the Flatpak.
const std::vector<std::string> missingCommand = bad({{"functional", {{"mcp", {{"command", "/nonexistent/bin/npx"}}}}}});
Check("functional.mcp.command: an absolute path that is no program here is a problem; a name, or a program that is there, is not",
missingCommand.size() == 1 && missingCommand[0].starts_with("functional.mcp.command '/nonexistent/bin/npx' is no program here")
&& bad({{"functional", {{"mcp", {{"command", "/bin/sh"}}}}}}).empty() && bad({{"functional", {{"mcp", {{"command", "npx"}}}}}}).empty(),
Show(missingCommand));
// A lens's own section (lenses.<name>.section, 2026-11-01, Jorijn: a path
// per lens rather than a directory listed): a file of the repository's,
// appended to the shipped charter. Only the section problems and the
// unknown keys count here: a config under test may turn no lens on.
auto sectionProblems = [&](const Json& lenses) {
std::vector<std::string> out;
for (const std::string& problem : bad({{"lenses", lenses}})) {
if (problem.contains("section") || problem.contains("unknown key lenses.")) out.push_back(problem);
}
return out;
};
const std::string outside = "lenses.repo-facts.section must be null or a file inside the repository (a relative path)";
Check("lenses.<name>.section: not a string, a blank one, an absolute path and one climbing out with .. are problems (slots.dir's rule) and resolve to nothing",
Any(sectionProblems({{"repo-facts", {{"section", 5}}}}), outside) && Any(sectionProblems({{"repo-facts", {{"section", " "}}}}), outside)
&& Any(sectionProblems({{"repo-facts", {{"section", "/elsewhere/facts.md"}}}}), outside) && Any(sectionProblems({{"repo-facts", {{"section", "../facts.md"}}}}), outside)
&& Config::LensSectionsOf({{"lenses", {{"repo-facts", {{"section", "/elsewhere/facts.md"}}}}}}, td).empty()
&& Config::LensSectionsOf({{"lenses", {{"repo-facts", {{"section", "a/../../facts.md"}}}}}}, td).empty());
fs::create_directories(td / "sections" / "adir");
std::ofstream(td / "sections" / "facts.md", std::ios::binary) << "## Ours\n";
std::ofstream(td / "sections" / "README.md", std::ios::binary) << "beside the sections, no concern of the config's\n";
std::ofstream(td / "sections" / "bad.md", std::ios::binary) << std::string("\xff\n", 2);
fs::create_symlink("missing-target.md", td / "sections" / "dangling.md");
const std::vector<std::string> named = sectionProblems({{"repo-facts", {{"section", "sections/facts.md"}}}, {"repo-bug-prover", {{"section", "sections/bad.md"}}},
{"repo-security", {{"section", "sections/dangling.md"}}}, {"repo-functional", {{"section", "sections/adir"}}},
{"facts", {{"section", "sections/nope.md"}}}});
Check("lenses.<name>.section: a file that is there is fine, whatever lies beside it; one not UTF-8, a dangling link, a directory or a path to nothing is a problem naming the lens and the path",
named.size() == 4 && Any(named, "lenses.repo-bug-prover.section: sections/bad.md is not UTF-8") && Any(named, "lenses.repo-security.section: sections/dangling.md cannot be read")
&& Any(named, "lenses.repo-functional.section: sections/adir cannot be read") && Any(named, "lenses.facts.section: sections/nope.md does not exist"),
Show(named));
const std::vector<std::string> unknown = sectionProblems({{"repo-fact", {{"section", "sections/facts.md"}}}, {"_common", {{"section", "sections/facts.md"}}},
{"repo-facts", {{"sectoin", "sections/facts.md"}}}});
Check("lenses.<name>.section: a name that is no lens - the common text's too - and a misspelt key are unknown keys",
Any(unknown, "unknown key lenses.repo-fact") && Any(unknown, "unknown key lenses._common") && Any(unknown, "unknown key lenses.repo-facts.sectoin"), Show(unknown));
// A file that cannot be read - where permissions bind, as they do not for
// root.
std::ofstream(td / "sections" / "locked.md", std::ios::binary) << "## Ours\n";
fs::permissions(td / "sections" / "locked.md", fs::perms::none);
const bool unreadable = !Platform::ReadFile(td / "sections" / "locked.md");
const std::vector<std::string> locked = sectionProblems({{"repo-facts", {{"section", "sections/locked.md"}}}});
fs::permissions(td / "sections" / "locked.md", fs::perms::owner_all);
Check("lenses.<name>.section: a file that cannot be read is a problem naming it", !unreadable || Any(locked, "lenses.repo-facts.section: sections/locked.md cannot be read"), Show(locked));
const std::map<std::string, fs::path> resolved = Config::LensSectionsOf({{"lenses", {{"repo-facts", {{"section", "sections/facts.md"}}}}}}, td);
Check("lenses.<name>.section: a file that is there has no problem and resolves against the repository, keyed by the lens's file; null is none",
sectionProblems({{"repo-facts", {{"section", "sections/facts.md"}}}}).empty() && resolved.size() == 1 && resolved.at("repo-facts.md") == td / "sections" / "facts.md"
&& Config::LensSectionsOf({{"lenses", {{"repo-facts", {{"section", nullptr}}}}}}, td).empty());
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();
std::vector<std::string> everyLens;
for (const Plan::Lens& lens : Plan::Lenses) everyLens.emplace_back(lens.file);
bool defaultsMatch = true;
std::set<std::string> names;
for (const Plan::Lens& lens : Plan::Lenses) {
names.insert(Plan::LensName(lens.file));
const Json entry = Config::Defaults().at("lenses").at(Plan::LensName(lens.file));
defaultsMatch = defaultsMatch && entry.at("environment") == lens.env && !entry.contains("run");
}
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());
Json environments = {{"repo-better-solution.md", "none"}, {fn, "browser"}, {"repo-bug-prover.md", "none"}, {"repo-security.md", "none"}, {Plan::FactsLens, "none"}};
// The kernel modes' lenses (2026-10-31), every one without an environment.
for (const std::string_view lf : {"better-solution.md", "code-quality.md", "minimalism.md", "bug-prover.md", "facts.md", "consistency.md", "process-docs.md", "etiquette.md", "conciseness.md", "maintainer-persona.md"}) {
environments[std::string(lf)] = "none";
}
Check("modes: the defaults are the three kinds as modes of their own name with no lens and no effort of their own; every lens is off until a mode lists it (2026-10-31, opt-in only) and the one problem names every lens; lenses hold the environment alone - none/browser/none/none/none",
noLensOn(loaded.problems) && loaded.problems[0].contains("repo-facts") && loaded.problems[0].contains("maintainer-persona") && Config::LensesOff(loaded.config) == everyLens
&& Dyn::Equal(Config::Environments(loaded.config), environments) && defaultNames == names && defaultsMatch && defaults.at("modes").size() == 3
&& Dyn::Equal(defaults.at("modes").at("reply"), {{"kind", "reply"}, {"lenses", Json::array()}, {"effort", nullptr}})
&& Config::ModeNames(loaded.config) == std::vector<std::string>{"reply", "series", "commits"} && Config::ModeOf(loaded.config, "commits")->lensFiles.empty()
&& Config::ModeOf(loaded.config, "commits")->kind == "commits" && !Config::ModeOf(loaded.config, "nope"),
Show(loaded.problems));
put({{"modes", {{"commits", {{"lenses", {"repo-bug-prover"}}}}}}, {"lenses", {{"repo-bug-prover", {{"environment", "browser"}}}}}});
loaded = load();
std::vector<std::string> allButProver;
for (const Plan::Lens& lens : Plan::Lenses) {
if (lens.file != "repo-bug-prover.md") allButProver.emplace_back(lens.file);
}
Check("modes: a kind's mode lists its lenses; the lenses no mode lists, and the kind's lenses the mode leaves out, come in table order; a static lens may take the browser", loaded.problems.empty()
&& Config::LensesOff(loaded.config) == allButProver
&& Config::LensesOff(loaded.config, "commits") == std::vector<std::string>{"repo-better-solution.md", "repo-functional.md", "repo-security.md", "repo-facts.md"}
&& Config::ModeOf(loaded.config, "commits")->lensFiles == std::vector<std::string>{"repo-bug-prover.md"} && !Config::ModeOf(loaded.config, "commits")->effort
&& 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({{"modes", {{"quick", {{"lenses", {"repo-bug-prover"}}}}}}});
Check("modes: a mode of the repository's own name needs its kind (its lenses cannot be checked without one, so nothing is listed)", problems.size() == 2
&& Any(problems, "modes.quick.kind must name the artifact kind") && Any(problems, "no mode lists a lens"), Show(problems));
put({{"modes", {{"quick", {{"kind", "commits"}, {"lenses", {"repo-facts", "repo-bug-prover"}}, {"effort", "high"}}}}}});
loaded = load();
const std::optional<Config::Mode> quick = Config::ModeOf(loaded.config, "quick");
Check("modes: a named mode is a kind, the lenses it lists in the table's order and its effort; the kinds' own modes stay beside it with no lens", loaded.problems.empty() && quick
&& quick->kind == "commits" && quick->lensFiles == std::vector<std::string>{"repo-bug-prover.md", "repo-facts.md"} && quick->effort == "high"
&& Config::ModeNames(loaded.config) == std::vector<std::string>{"reply", "series", "commits", "quick"}
&& Config::LensesOff(loaded.config, "quick") == std::vector<std::string>{"repo-better-solution.md", "repo-functional.md", "repo-security.md"}
&& Config::ModeOf(loaded.config, "commits")->lensFiles.empty() && Config::LensesOff(loaded.config, "nope").empty(), Show(loaded.problems));
problems = bad({{"modes", {{"commits", {{"kind", "series"}, {"lenses", {"repo-bug-prover", "facts", "nope", "repo-bug-prover"}}, {"effort", "ultra"}}}}}});
Check("modes: a kind's own mode is that kind; a lens of another kind, a name that is no lens and a lens listed twice are each a problem; the effort is one of the five", problems.size() == 5
&& Any(problems, "modes.commits.kind must be commits") && Any(problems, "facts is no lens of a commits") && Any(problems, "nope is no lens of a commits")
&& Any(problems, "repo-bug-prover is listed twice") && Any(problems, "modes.commits.effort must be one of low, medium, high, xhigh, max"), Show(problems));
Check("modes: not an object, a mode that is no object, lenses that are no list", Any(bad({{"modes", 5}}), "modes must be an object")
&& Any(bad({{"modes", {{"commits", 5}}}}), "modes.commits must be an object") && Any(bad({{"modes", {{"commits", {{"lenses", "repo-bug-prover"}}}}}}), "modes.commits.lenses must be a list of lens names"));
put({{"lenses", {{"repo-bug-prover", {{"run", true}}}}}});
loaded = load();
Check("lenses.<name>.run is retired: a warning that says a mode lists what runs, and nothing runs until one does", loaded.config.at("_warnings").size() == 1
&& Dyn::Str(loaded.config.at("_warnings").at(0)).contains("lenses.repo-bug-prover.run is no longer read") && noLensOn(loaded.problems), Show(loaded.problems));
problems = bad({{"lenses", {{"nope", {{"environment", "none"}}}, {"repo-better-solution", {{"environment", "http"}}}}}});
Check("lenses: an unknown lens is an unknown key; environment must be one of none, browser, supabase, browser+supabase", problems.size() == 2 && Any(problems, "lenses.nope")
&& Any(problems, "one of none, browser, supabase, browser+supabase"), Show(problems));
problems = bad({{"modes", {{"commits", {{"lenses", {"repo-functional"}}}}}}, {"lenses", {{"repo-functional", {{"environment", "none"}}}}}});
Check("the functional lens listed by a mode with no application under test is a problem; unlisted with none is fine; an unknown environment is reported once",
problems.size() == 1 && problems[0].contains("empirical")
&& bad({{"modes", {{"commits", {{"lenses", {"repo-bug-prover"}}}}}}, {"lenses", {{"repo-functional", {{"environment", "none"}}}}}}).empty()
&& bad({{"modes", {{"commits", {{"lenses", {"repo-functional"}}}}}}, {"lenses", {{"repo-functional", {{"environment", "http"}}}}}}).size() == 1, Show(problems));
problems = bad({{"modes", {{"commits", {{"lenses", {"repo-functional"}}}}}}, {"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({{"modes", {{"commits", {{"lenses", {"repo-functional", "repo-bug-prover", "repo-better-solution"}}}}}},
{"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 listed lens drives the application (plan.ENVIRONMENTS item 4)",
Any(bad({{"functional", {{"appUrl", "ftp://x"}}}, {"modes", {{"commits", {{"lenses", {"repo-functional"}}}}}}}), "appUrl")
&& !Any(bad({{"functional", {{"appUrl", "ftp://x"}}}}), "appUrl")
&& bad({{"functional", {{"appUrl", "ftp://x"}}}, {"modes", {{"commits", {{"lenses", {"repo-bug-prover"}}}}}}, {"lenses", {{"repo-bug-prover", {{"environment", "supabase"}}}}}}).empty()
&& Any(bad({{"functional", {{"appUrl", "ftp://x"}}}, {"modes", {{"commits", {{"lenses", {"repo-bug-prover"}}}}}}, {"lenses", {{"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 (off, browser)", problems.size() == 1
&& problems[0].contains("must be an object") && Config::LensesOff(load().config) == allButProver && Config::Environments(load().config).at(fn) == "browser", Show(problems));
Json noLens = Json::object();
for (const std::string_view kind : Plan::Kinds) noLens[std::string(kind)] = {{"lenses", Json::array()}};
problems = bad({{"modes", noLens}});
Check("no mode listing a lens is the problem that names them", problems.size() == 1 && problems[0].contains("no mode lists a lens") && problems[0].contains("repo-facts"), Show(problems));
const Json ex = Config::Example();
Check("the example is what process/templates/config.json holds: the three kinds as modes with every lens of their kind at the kind's effort, no problem", Config::Validate(ex).empty()
&& Dyn::Equal(Dyn::LoadJsonFile(fs::current_path() / "process" / "templates" / "config.json"), ex)
&& ex.at("modes").at("commits").at("lenses").size() == 5 && ex.at("modes").at("series").at("lenses").size() == 10 && ex.at("modes").at("reply").at("lenses").size() == 5
&& ex.at("modes").at("commits").at("effort") == "xhigh" && ex.at("modes").at("series").at("effort") == "max" && ex.at("modes").at("reply").at("effort") == "max"
&& std::ranges::none_of(ex.at("lenses"), [](const Json& entry) { return entry.contains("run"); }), Show(Config::Validate(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;
}