fold/tests/unit/test_configuration_profile.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

198 lines
8.9 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <chrono>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <regex>
#include <string>
#include "fold/config/configuration_profile.hpp"
namespace {
using fold::config::AssumptionKind;
using fold::config::ConfigurationProfileErrorCode;
using fold::config::GetenvState;
using fold::config::parseConfigurationProfileText;
constexpr const char* kValidProfile = R"({
"version": 1,
"entrypoints": ["worker", "main"],
"assumptions": [
{"kind":"accessor", "selector":"Config::enabled", "value":false},
{"kind":"getenv", "selector":"FEATURE", "state":"unset"},
{"kind":"entry_arg", "entrypoint":"main", "index":0, "value":-7},
{"kind":"global", "selector":"g_ready", "value":true},
{"kind":"getenv", "selector":"LIMIT", "state":"set"}
]
})";
bool hasError(const fold::config::ConfigurationProfileParseResult& result,
ConfigurationProfileErrorCode code) {
for (const auto& error : result.errors) {
if (error.code == code)
return true;
}
return false;
}
struct TempFile {
std::filesystem::path path =
std::filesystem::temp_directory_path() /
("fold-configuration-profile-" +
std::to_string(std::chrono::steady_clock::now().time_since_epoch().count()) + ".json");
explicit TempFile(const std::string& contents) { std::ofstream(path) << contents; }
~TempFile() { std::filesystem::remove(path); }
TempFile(const TempFile&) = delete;
TempFile& operator=(const TempFile&) = delete;
};
} // namespace
TEST_CASE("configuration profile parses all v1 assumption kinds and typed values",
"[configuration-profile]") {
const auto result = parseConfigurationProfileText(kValidProfile);
REQUIRE(result);
REQUIRE(result.profile->version() == 1);
REQUIRE(result.profile->entrypoints() == std::vector<std::string>{"main", "worker"});
REQUIRE(result.profile->assumptions().size() == 5);
bool saw_bool = false;
bool saw_int = false;
bool saw_unset = false;
for (const auto& assumption : result.profile->assumptions()) {
REQUIRE(std::regex_match(assumption.symbolId().value(), std::regex("[0-9a-f]{64}")));
if (assumption.value && std::holds_alternative<bool>(*assumption.value))
saw_bool = true;
if (assumption.value && std::holds_alternative<std::int64_t>(*assumption.value))
saw_int = true;
if (assumption.kind == AssumptionKind::Getenv) {
REQUIRE_FALSE(assumption.value.has_value());
if (assumption.getenv_state == GetenvState::Unset)
saw_unset = true;
}
}
REQUIRE(saw_bool);
REQUIRE(saw_int);
REQUIRE(saw_unset);
}
TEST_CASE("configuration profile rejects root and assumption schema drift",
"[configuration-profile][strict]") {
const auto missing = parseConfigurationProfileText(R"({"entrypoints":[],"assumptions":[]})");
REQUIRE_FALSE(missing);
REQUIRE(hasError(missing, ConfigurationProfileErrorCode::MissingField));
const auto future =
parseConfigurationProfileText(R"({"version":2,"entrypoints":[],"assumptions":[]})");
REQUIRE_FALSE(future);
REQUIRE(hasError(future, ConfigurationProfileErrorCode::UnsupportedVersion));
const auto unknown = parseConfigurationProfileText(
R"({"version":1,"entrypoints":[],"assumptions":[],"extra":true})");
REQUIRE_FALSE(unknown);
REQUIRE(hasError(unknown, ConfigurationProfileErrorCode::UnknownField));
const auto nested_unknown = parseConfigurationProfileText(
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":"g","value":true,"extra":0}]})");
REQUIRE_FALSE(nested_unknown);
REQUIRE(hasError(nested_unknown, ConfigurationProfileErrorCode::UnknownField));
}
TEST_CASE("configuration profile performs no value or index coercion",
"[configuration-profile][strict]") {
const auto string_bool = parseConfigurationProfileText(
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":"g","value":"true"}]})");
REQUIRE_FALSE(string_bool);
REQUIRE(hasError(string_bool, ConfigurationProfileErrorCode::InvalidType));
const auto float_value = parseConfigurationProfileText(
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"accessor","selector":"f","value":1.0}]})");
REQUIRE_FALSE(float_value);
REQUIRE(hasError(float_value, ConfigurationProfileErrorCode::InvalidType));
const auto string_index = parseConfigurationProfileText(
R"({"version":1,"entrypoints":["main"],"assumptions":[{"kind":"entry_arg","entrypoint":"main","index":"0","value":1}]})");
REQUIRE_FALSE(string_index);
REQUIRE(hasError(string_index, ConfigurationProfileErrorCode::InvalidType));
}
TEST_CASE("configuration profile validates selectors, entry arguments, getenv, and duplicates",
"[configuration-profile][strict]") {
const auto empty_selector = parseConfigurationProfileText(
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":" ","value":true}]})");
REQUIRE_FALSE(empty_selector);
REQUIRE(hasError(empty_selector, ConfigurationProfileErrorCode::InvalidValue));
const auto unknown_entrypoint = parseConfigurationProfileText(
R"({"version":1,"entrypoints":["main"],"assumptions":[{"kind":"entry_arg","entrypoint":"worker","index":0,"value":1}]})");
REQUIRE_FALSE(unknown_entrypoint);
REQUIRE(hasError(unknown_entrypoint, ConfigurationProfileErrorCode::UnknownEntrypoint));
const auto negative_index = parseConfigurationProfileText(
R"({"version":1,"entrypoints":["main"],"assumptions":[{"kind":"entry_arg","entrypoint":"main","index":-1,"value":1}]})");
REQUIRE_FALSE(negative_index);
REQUIRE(hasError(negative_index, ConfigurationProfileErrorCode::InvalidValue));
const auto invalid_getenv = parseConfigurationProfileText(
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"getenv","selector":"X","state":"unset","value":1}]})");
REQUIRE_FALSE(invalid_getenv);
REQUIRE(hasError(invalid_getenv, ConfigurationProfileErrorCode::UnknownField));
const auto duplicate_key = parseConfigurationProfileText(
R"({"version":1,"version":1,"entrypoints":[],"assumptions":[]})");
REQUIRE_FALSE(duplicate_key);
REQUIRE(hasError(duplicate_key, ConfigurationProfileErrorCode::DuplicateField));
const auto duplicate_selector = parseConfigurationProfileText(
R"({"version":1,"entrypoints":[],"assumptions":[{"kind":"global","selector":"g","value":true},{"kind":"global","selector":"g","value":false}]})");
REQUIRE_FALSE(duplicate_selector);
REQUIRE(hasError(duplicate_selector, ConfigurationProfileErrorCode::DuplicateAssumption));
}
TEST_CASE("configuration profile canonical JSON and hash ignore input order and whitespace",
"[configuration-profile][canonical]") {
const auto first = parseConfigurationProfileText(R"({
"version":1,
"entrypoints":["worker","main"],
"assumptions":[
{"kind":"global","selector":"z","value":9},
{"kind":"entry_arg","entrypoint":"main","index":0,"value":true}
]
})");
const auto second = parseConfigurationProfileText(
R"({"assumptions":[{"value":true,"index":0,"entrypoint":"main","kind":"entry_arg"},{"selector":"z","value":9,"kind":"global"}],"entrypoints":["main","worker"],"version":1})");
REQUIRE(first);
REQUIRE(second);
REQUIRE(first.profile->canonicalJsonText() == second.profile->canonicalJsonText());
REQUIRE(first.profile->canonicalHash() == second.profile->canonicalHash());
REQUIRE(std::regex_match(first.profile->canonicalHash(), std::regex("[0-9a-f]{64}")));
REQUIRE(first.profile->assumptions().size() == second.profile->assumptions().size());
for (std::size_t index = 0; index < first.profile->assumptions().size(); ++index) {
REQUIRE(first.profile->assumptions()[index].symbolId() ==
second.profile->assumptions()[index].symbolId());
}
}
TEST_CASE("configuration profile parses files and reports bounded structured errors",
"[configuration-profile][file]") {
TempFile file(kValidProfile);
const auto parsed = fold::config::parseConfigurationProfileFile(file.path);
REQUIRE(parsed);
std::string invalid = R"({"version":1,"entrypoints":[],"assumptions":[])";
const auto syntax_error = parseConfigurationProfileText(invalid);
REQUIRE_FALSE(syntax_error);
REQUIRE(hasError(syntax_error, ConfigurationProfileErrorCode::JsonSyntax));
REQUIRE(syntax_error.errors.size() <= fold::config::kMaximumConfigurationProfileErrors);
REQUIRE_FALSE(syntax_error.errors.front().path.empty());
REQUIRE_FALSE(syntax_error.errors.front().message.empty());
const auto missing_file =
fold::config::parseConfigurationProfileFile(file.path.string() + ".missing");
REQUIRE_FALSE(missing_file);
REQUIRE(hasError(missing_file, ConfigurationProfileErrorCode::FileRead));
}