#include #include #include #include #include #include #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::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::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 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()); }