- 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
300 lines
14 KiB
C++
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());
|
|
}
|