fold/tests/unit/test_config_guard.cpp
trvon 86b2f14a43
feat(analysis): harden project-wide scans
- persist TU-owned graphs and guarded configuration evidence
- add closed-world production reachability classification
- calibrate detector precision and reviewed self-scan policy
- terminate poisoned workers instead of longjmp recovery
2026-07-30 18:18:04 -04:00

300 lines
14 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <cstdint>
#include <limits>
#include <stdexcept>
#include <string>
#include <vector>
#include "fold/ir/config_guard.hpp"
using fold::ir::ConfigGuard;
using fold::ir::ConfigGuardExpr;
using fold::ir::configGuardFromJson;
using fold::ir::ConfigGuardLimits;
using fold::ir::ConfigGuardOp;
using fold::ir::ConfigGuardStatus;
using fold::ir::configGuardToJson;
using fold::ir::isConfigGuardSymbolId;
using fold::ir::normalizeConfigGuard;
using fold::ir::config_guard_json_keys::kBoolValue;
using fold::ir::config_guard_json_keys::kExpression;
using fold::ir::config_guard_json_keys::kIntValue;
using fold::ir::config_guard_json_keys::kOp;
using fold::ir::config_guard_json_keys::kOperands;
using fold::ir::config_guard_json_keys::kStatus;
using fold::ir::config_guard_json_keys::kSymbolId;
namespace {
std::string id(char digit) { return std::string(64, digit); }
ConfigGuardExpr eq(ConfigGuardExpr lhs, ConfigGuardExpr rhs) {
return ConfigGuardExpr::apply(ConfigGuardOp::Eq, {lhs, rhs});
}
ConfigGuardExpr ne(ConfigGuardExpr lhs, ConfigGuardExpr rhs) {
return ConfigGuardExpr::apply(ConfigGuardOp::Ne, {lhs, rhs});
}
ConfigGuardExpr integerEq(char symbol, std::int64_t value) {
return eq(ConfigGuardExpr::intSymbol(id(symbol)), ConfigGuardExpr::integer(value));
}
ConfigGuardExpr normalizedExpression(const ConfigGuard& guard) {
const auto normalized = normalizeConfigGuard(guard);
REQUIRE(normalized.status == ConfigGuardStatus::Projected);
REQUIRE(normalized.expression.has_value());
return *normalized.expression;
}
} // namespace
TEST_CASE("configuration guard accepts only opaque lowercase SHA-256 symbol IDs", "[ir][config]") {
REQUIRE(isConfigGuardSymbolId(id('0')));
REQUIRE(
isConfigGuardSymbolId("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"));
REQUIRE_FALSE(isConfigGuardSymbolId("feature_name"));
REQUIRE_FALSE(isConfigGuardSymbolId(std::string(63, 'a')));
REQUIRE_FALSE(isConfigGuardSymbolId(std::string(65, 'a')));
REQUIRE_FALSE(isConfigGuardSymbolId(std::string(64, 'A')));
REQUIRE_FALSE(isConfigGuardSymbolId(std::string(64, 'g')));
REQUIRE_THROWS_AS(ConfigGuardExpr::boolSymbol("debug"), std::invalid_argument);
REQUIRE_THROWS_AS(ConfigGuardExpr::intSymbol(std::string(64, 'F')), std::invalid_argument);
}
TEST_CASE("configuration guard status JSON is minimal and round-trips", "[ir][config]") {
const auto unconditional = ConfigGuard::unconditional().toJson();
REQUIRE(unconditional == nlohmann::json{{kStatus.data(), "unconditional"}});
REQUIRE(ConfigGuard::fromJson(unconditional) == ConfigGuard::unconditional());
const auto unknown = configGuardToJson(ConfigGuard::unknown());
REQUIRE(unknown == nlohmann::json{{kStatus.data(), "unknown"}});
REQUIRE(configGuardFromJson(unknown) == ConfigGuard::unknown());
const auto projected = ConfigGuard::projected(ConfigGuardExpr::boolSymbol(id('a')));
REQUIRE(ConfigGuard::fromJson(projected.toJson()) == projected);
}
TEST_CASE("configuration guard serializes bool and int64 literals without source text",
"[ir][config]") {
const auto guard = ConfigGuard::projected(
eq(ConfigGuardExpr::intSymbol(id('a')),
ConfigGuardExpr::integer(std::numeric_limits<std::int64_t>::min())));
const auto json = guard.toJson();
REQUIRE(json.at(kStatus) == "projected");
REQUIRE(json.at(kExpression).at(kOp) == "eq");
REQUIRE(json.at(kExpression).at(kOperands).at(0).at(kIntValue) ==
std::numeric_limits<std::int64_t>::min());
REQUIRE(json.at(kExpression).at(kOperands).at(1).at(kOp) == "int_symbol");
REQUIRE(json.dump().find("source") == std::string::npos);
REQUIRE(json.dump().find("feature") == std::string::npos);
const auto bool_guard = ConfigGuard::projected(ConfigGuardExpr::boolean(false));
REQUIRE(bool_guard.toJson().at(kExpression).at(kBoolValue) == false);
}
TEST_CASE("And and Or normalization flattens sorts and deduplicates", "[ir][config]") {
auto a = integerEq('a', 1);
auto b = integerEq('b', 2);
auto c = integerEq('c', 3);
const auto first = ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::And, {c, ConfigGuardExpr::apply(ConfigGuardOp::And, {b, a, b}), a}));
const auto second =
ConfigGuard::projected(ConfigGuardExpr::apply(ConfigGuardOp::And, {b, c, a}));
const auto normalized = normalizeConfigGuard(first);
REQUIRE(normalized == normalizeConfigGuard(second));
REQUIRE(normalized.toJson().dump() == second.toJson().dump());
REQUIRE(normalized.expression->op == ConfigGuardOp::And);
REQUIRE(normalized.expression->operands.size() == 3);
const auto or_guard = ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::Or, {a, ConfigGuardExpr::apply(ConfigGuardOp::Or, {c, b, c}), a}));
const auto or_expression = normalizedExpression(or_guard);
REQUIRE(or_expression.op == ConfigGuardOp::Or);
REQUIRE(or_expression.operands.size() == 3);
}
TEST_CASE("configuration guard folds boolean identities and annihilators", "[ir][config]") {
const auto symbol = ConfigGuardExpr::boolSymbol(id('a'));
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::And, {ConfigGuardExpr::boolean(true), symbol}))) ==
ConfigGuard::projected(symbol));
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::Or, {ConfigGuardExpr::boolean(false), symbol}))) ==
ConfigGuard::projected(symbol));
const auto false_and = normalizeConfigGuard(ConfigGuard::projected(
ConfigGuardExpr::apply(ConfigGuardOp::And, {symbol, ConfigGuardExpr::boolean(false)})));
REQUIRE(false_and == ConfigGuard::projected(ConfigGuardExpr::boolean(false)));
const auto true_or = normalizeConfigGuard(ConfigGuard::projected(
ConfigGuardExpr::apply(ConfigGuardOp::Or, {symbol, ConfigGuardExpr::boolean(true)})));
REQUIRE(true_or.status == ConfigGuardStatus::Unconditional);
REQUIRE_FALSE(true_or.expression.has_value());
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(
ConfigGuardExpr::apply(ConfigGuardOp::And, {}))) == ConfigGuard::unconditional());
REQUIRE(normalizeConfigGuard(
ConfigGuard::projected(ConfigGuardExpr::apply(ConfigGuardOp::Or, {}))) ==
ConfigGuard::projected(ConfigGuardExpr::boolean(false)));
}
TEST_CASE("configuration guard folds typed constant comparisons", "[ir][config]") {
struct ComparisonCase {
ConfigGuardOp op;
std::int64_t lhs;
std::int64_t rhs;
bool expected;
};
const std::vector<ComparisonCase> cases = {
{ConfigGuardOp::Eq, 2, 2, true}, {ConfigGuardOp::Ne, 2, 3, true},
{ConfigGuardOp::Lt, 2, 3, true}, {ConfigGuardOp::Le, 2, 2, true},
{ConfigGuardOp::Gt, 3, 2, true}, {ConfigGuardOp::Ge, 2, 3, false},
};
for (const auto& test : cases) {
const auto guard = ConfigGuard::projected(ConfigGuardExpr::apply(
test.op, {ConfigGuardExpr::integer(test.lhs), ConfigGuardExpr::integer(test.rhs)}));
const auto normalized = normalizeConfigGuard(guard);
if (test.expected) {
REQUIRE(normalized.status == ConfigGuardStatus::Unconditional);
} else {
REQUIRE(normalized == ConfigGuard::projected(ConfigGuardExpr::boolean(false)));
}
}
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(
eq(ConfigGuardExpr::boolean(true), ConfigGuardExpr::boolean(false)))) ==
ConfigGuard::projected(ConfigGuardExpr::boolean(false)));
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(
ne(ConfigGuardExpr::boolean(true), ConfigGuardExpr::boolean(false)))) ==
ConfigGuard::unconditional());
}
TEST_CASE("configuration guard validates comparison arity and operand types", "[ir][config]") {
const auto int_symbol = ConfigGuardExpr::intSymbol(id('a'));
const auto bool_symbol = ConfigGuardExpr::boolSymbol(id('b'));
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::Eq, {int_symbol}))) == ConfigGuard::unknown());
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::Eq, {int_symbol, bool_symbol}))) == ConfigGuard::unknown());
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::Lt, {bool_symbol, bool_symbol}))) == ConfigGuard::unknown());
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::integer(7))) ==
ConfigGuard::unknown());
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::Not, {bool_symbol, bool_symbol}))) == ConfigGuard::unknown());
}
TEST_CASE("configuration guard simplifies negation", "[ir][config]") {
const auto symbol = ConfigGuardExpr::boolSymbol(id('a'));
const auto not_symbol = ConfigGuardExpr::apply(ConfigGuardOp::Not, {symbol});
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(
ConfigGuardExpr::apply(ConfigGuardOp::Not, {ConfigGuardExpr::boolean(false)}))) ==
ConfigGuard::unconditional());
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(ConfigGuardExpr::apply(
ConfigGuardOp::Not, {not_symbol}))) == ConfigGuard::projected(symbol));
const auto comparison = integerEq('b', 9);
const auto negated = normalizeConfigGuard(
ConfigGuard::projected(ConfigGuardExpr::apply(ConfigGuardOp::Not, {comparison})));
REQUIRE(negated.status == ConfigGuardStatus::Projected);
REQUIRE(negated.expression->op == ConfigGuardOp::Ne);
REQUIRE(negated.expression->operands ==
normalizedExpression(ConfigGuard::projected(comparison)).operands);
}
TEST_CASE("configuration guard bounds cause Unknown before simplification", "[ir][config]") {
auto deep = ConfigGuardExpr::boolSymbol(id('a'));
for (int i = 0; i < 4; ++i)
deep = ConfigGuardExpr::apply(ConfigGuardOp::Not, {deep});
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(deep), ConfigGuardLimits{4, 20}) ==
ConfigGuard::unknown());
const auto wide = ConfigGuardExpr::apply(
ConfigGuardOp::And,
{ConfigGuardExpr::boolSymbol(id('a')), ConfigGuardExpr::boolSymbol(id('b')),
ConfigGuardExpr::boolSymbol(id('c')), ConfigGuardExpr::boolSymbol(id('d'))});
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(wide), ConfigGuardLimits{4, 4}) ==
ConfigGuard::unknown());
REQUIRE(normalizeConfigGuard(ConfigGuard::projected(wide), ConfigGuardLimits{2, 5}).status ==
ConfigGuardStatus::Projected);
}
TEST_CASE("configuration guard fromJson rejects malformed values without partial accept",
"[ir][config]") {
const auto valid = ConfigGuard::projected(integerEq('a', 1)).toJson();
auto malformed = valid;
malformed["source_expression"] = "DEBUG && LEVEL > 1";
REQUIRE_THROWS_AS(ConfigGuard::fromJson(malformed), std::invalid_argument);
malformed = valid;
malformed[kExpression][kOperands][0][kSymbolId] = std::string(64, 'A');
REQUIRE_THROWS_AS(ConfigGuard::fromJson(malformed), std::invalid_argument);
malformed = valid;
malformed[kExpression][kOperands][1][kIntValue] = 1.5;
REQUIRE_THROWS_AS(ConfigGuard::fromJson(malformed), std::invalid_argument);
malformed = valid;
malformed[kExpression][kOp] = "multiply";
REQUIRE_THROWS_AS(ConfigGuard::fromJson(malformed), std::invalid_argument);
malformed = valid;
malformed[kExpression][kOperands].erase(malformed[kExpression][kOperands].begin());
REQUIRE_THROWS_AS(ConfigGuard::fromJson(malformed), std::invalid_argument);
malformed = valid;
malformed[kExpression][kOperands][0][kOp] = "bool_symbol";
REQUIRE_THROWS_AS(ConfigGuard::fromJson(malformed), std::invalid_argument);
REQUIRE_THROWS_AS(ConfigGuard::fromJson(nlohmann::json::array()), std::invalid_argument);
REQUIRE_THROWS_AS(ConfigGuard::fromJson(nlohmann::json{{kStatus.data(), "projected"}}),
std::invalid_argument);
REQUIRE_THROWS_AS(ConfigGuard::fromJson(
nlohmann::json{{kStatus.data(), "unknown"}, {kExpression.data(), true}}),
std::invalid_argument);
REQUIRE_THROWS_AS(ConfigGuard::fromJson(nlohmann::json{{kStatus.data(), "sometimes"}}),
std::invalid_argument);
}
TEST_CASE("configuration guard fromJson validates all nodes before returning bounded Unknown",
"[ir][config]") {
const auto wide = ConfigGuard::projected(
ConfigGuardExpr::apply(ConfigGuardOp::And, {ConfigGuardExpr::boolSymbol(id('a')),
ConfigGuardExpr::boolSymbol(id('b')),
ConfigGuardExpr::boolSymbol(id('c'))}));
const auto json = wide.toJson(ConfigGuardLimits{8, 8});
REQUIRE(ConfigGuard::fromJson(json, ConfigGuardLimits{8, 3}) == ConfigGuard::unknown());
auto malformed = json;
malformed[kExpression][kOperands][2][kSymbolId] = "not-a-hash";
REQUIRE_THROWS_AS(ConfigGuard::fromJson(malformed, ConfigGuardLimits{8, 3}),
std::invalid_argument);
}
TEST_CASE("configuration guard serialization contains only canonical schema fields",
"[ir][config]") {
ConfigGuardExpr invalid;
invalid.op = ConfigGuardOp::BoolSymbol;
invalid.symbol_id = "ENABLE_DANGEROUS_MODE";
const auto json = ConfigGuard::projected(invalid).toJson();
REQUIRE(json == nlohmann::json{{kStatus.data(), "unknown"}});
REQUIRE(json.dump().find("ENABLE_DANGEROUS_MODE") == std::string::npos);
auto canonical = ConfigGuard::projected(
ConfigGuardExpr::apply(ConfigGuardOp::And, {ConfigGuardExpr::boolSymbol(id('b')),
ConfigGuardExpr::boolSymbol(id('a'))}));
const auto once = canonical.toJson();
const auto twice = ConfigGuard::fromJson(once).toJson();
REQUIRE(once == twice);
REQUIRE(once.dump() == twice.dump());
}