// Copyright (c) 2025 YAMS Contributors // SPDX-License-Identifier: GPL-3.0-or-later // // Unit tests for ConfigResolver component (PBI-090) // // Catch2 migration from GTest (yams-3s4 / yams-zns) #include #include #include #include #include #include #include #include #include #include #include #include using namespace yams::daemon; using Catch::Matchers::ContainsSubstring; namespace { struct ConfigResolverFixture { std::filesystem::path tempDir; ConfigResolverFixture() { tempDir = std::filesystem::temp_directory_path() / ("yams_config_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); std::filesystem::create_directories(tempDir); } ~ConfigResolverFixture() { std::error_code ec; std::filesystem::remove_all(tempDir, ec); } std::filesystem::path writeToml(const std::string& filename, const std::string& content) { auto path = tempDir / filename; std::ofstream out(path); out << content; return path; } }; class ProfileGuard { yams::daemon::TuneAdvisor::Profile prev_; public: explicit ProfileGuard(yams::daemon::TuneAdvisor::Profile profile) : prev_(yams::daemon::TuneAdvisor::tuningProfile()) { yams::daemon::TuneAdvisor::setTuningProfile(profile); } ~ProfileGuard() { yams::daemon::TuneAdvisor::setTuningProfile(prev_); } ProfileGuard(const ProfileGuard&) = delete; ProfileGuard& operator=(const ProfileGuard&) = delete; }; struct EnvGuard { std::string name; std::optional originalValue; explicit EnvGuard(const std::string& envName, const std::string& newValue) : name(envName) { if (const char* orig = std::getenv(name.c_str())) { originalValue = orig; } #ifdef _WIN32 _putenv_s(name.c_str(), newValue.c_str()); #else setenv(name.c_str(), newValue.c_str(), 1); #endif } ~EnvGuard() { if (originalValue) { #ifdef _WIN32 _putenv_s(name.c_str(), originalValue->c_str()); #else setenv(name.c_str(), originalValue->c_str(), 1); #endif } else { #ifdef _WIN32 _putenv_s(name.c_str(), ""); #else unsetenv(name.c_str()); #endif } } EnvGuard(const EnvGuard&) = delete; EnvGuard& operator=(const EnvGuard&) = delete; }; } // namespace TEST_CASE("TuneAdvisor bounded override readers share env and override behavior", "[daemon][components][tuning][catch2]") { TuneAdvisor::setWorkerPollMs(0); TuneAdvisor::setPoolScaleStep(0); TuneAdvisor::setConnectionSlotsScaleStep(0); { EnvGuard env("YAMS_WORKER_POLL_MS", "75"); CHECK(TuneAdvisor::workerPollMs() == 75u); TuneAdvisor::setWorkerPollMs(90u); CHECK(TuneAdvisor::workerPollMs() == 90u); TuneAdvisor::setWorkerPollMs(0); CHECK(TuneAdvisor::workerPollMs() == 75u); } { EnvGuard env("YAMS_WORKER_POLL_MS", "49"); CHECK(TuneAdvisor::workerPollMs() == 150u); } { EnvGuard env("YAMS_POOL_SCALE_STEP", "16"); CHECK(TuneAdvisor::poolScaleStep() == 16); } { EnvGuard env("YAMS_POOL_SCALE_STEP", "17"); CHECK(TuneAdvisor::poolScaleStep() == 1); } { EnvGuard env("YAMS_CONN_SLOTS_STEP", "128"); CHECK(TuneAdvisor::connectionSlotsScaleStep() == 128u); } { EnvGuard env("YAMS_CONN_SLOTS_STEP", "129"); CHECK(TuneAdvisor::connectionSlotsScaleStep() == 16u); } TuneAdvisor::setWorkerPollMs(0); TuneAdvisor::setPoolScaleStep(0); TuneAdvisor::setConnectionSlotsScaleStep(0); } TEST_CASE("ConfigResolver::resolveEmbeddingChunkingPolicy defaults are embedding-safe", "[daemon][components][config][chunking][catch2]") { // Ensure config file discovery doesn't accidentally pick up a user config during local dev. EnvGuard cfg("YAMS_CONFIG_PATH", ""); auto policy = ConfigResolver::resolveEmbeddingChunkingPolicy(); CHECK(policy.strategy == yams::vector::ChunkingStrategy::PARAGRAPH_BASED); CHECK_FALSE(policy.config.preserve_sentences); CHECK_FALSE(policy.config.use_token_count); } TEST_CASE("ConfigResolver::resolveEmbeddingChunkingPolicy supports all embed strategies", "[daemon][components][config][chunking][catch2]") { struct Case { const char* value; yams::vector::ChunkingStrategy expected; }; const std::vector cases = { {"fixed", yams::vector::ChunkingStrategy::FIXED_SIZE}, {"sentence", yams::vector::ChunkingStrategy::SENTENCE_BASED}, {"paragraph", yams::vector::ChunkingStrategy::PARAGRAPH_BASED}, {"recursive", yams::vector::ChunkingStrategy::RECURSIVE}, {"sliding_window", yams::vector::ChunkingStrategy::SLIDING_WINDOW}, {"markdown", yams::vector::ChunkingStrategy::MARKDOWN_AWARE}, }; const std::string sample = "# Title\n\n" "Intro paragraph with enough text to chunk reasonably. " "Second sentence here. Third sentence here.\n\n" "## Section\n" "- bullet one\n- bullet two\n\n" "Final paragraph."; for (const auto& tc : cases) { DYNAMIC_SECTION("strategy=" << tc.value) { EnvGuard g("YAMS_EMBED_CHUNK_STRATEGY", tc.value); auto policy = ConfigResolver::resolveEmbeddingChunkingPolicy(); CHECK(policy.strategy == tc.expected); auto chunker = yams::vector::createChunker(policy.strategy, policy.config, nullptr); REQUIRE(chunker); auto chunks = chunker->chunkDocument(sample, "hash"); // Some strategies may return empty and rely on caller fallback, but for this // representative input we expect at least one chunk. REQUIRE_FALSE(chunks.empty()); for (const auto& c : chunks) { CHECK(c.strategy_used == tc.expected); CHECK_FALSE(c.content.empty()); } } } } TEST_CASE("ConfigResolver::resolveEmbeddingChunkingPolicy reads from config file", "[daemon][components][config][chunking][catch2]") { ConfigResolverFixture fx; auto configPath = fx.writeToml("config.toml", R"TOML( [embeddings.chunking] strategy = "fixed" target = 256 min = 64 max = 512 overlap = 0 use_tokens = 0 preserve_sentences = 0 )TOML"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveEmbeddingChunkingPolicy(); CHECK(policy.strategy == yams::vector::ChunkingStrategy::FIXED_SIZE); CHECK(policy.config.target_chunk_size == 256); CHECK(policy.config.min_chunk_size == 64); CHECK(policy.config.max_chunk_size == 512); CHECK(policy.config.overlap_size == 0); CHECK(policy.config.overlap_percentage == 0.0); CHECK_FALSE(policy.config.use_token_count); CHECK_FALSE(policy.config.preserve_sentences); } TEST_CASE("ConfigResolver::envTruthy correctly parses truthy values", "[daemon][components][config][catch2]") { SECTION("truthy values") { CHECK(ConfigResolver::envTruthy("1")); CHECK(ConfigResolver::envTruthy("true")); CHECK(ConfigResolver::envTruthy("TRUE")); CHECK(ConfigResolver::envTruthy("True")); CHECK(ConfigResolver::envTruthy("yes")); CHECK(ConfigResolver::envTruthy("YES")); CHECK(ConfigResolver::envTruthy("Yes")); CHECK(ConfigResolver::envTruthy("on")); CHECK(ConfigResolver::envTruthy("ON")); CHECK(ConfigResolver::envTruthy("On")); } SECTION("falsy values") { CHECK_FALSE(ConfigResolver::envTruthy("0")); CHECK_FALSE(ConfigResolver::envTruthy("false")); CHECK_FALSE(ConfigResolver::envTruthy("FALSE")); CHECK_FALSE(ConfigResolver::envTruthy("no")); CHECK_FALSE(ConfigResolver::envTruthy("NO")); CHECK_FALSE(ConfigResolver::envTruthy("off")); CHECK_FALSE(ConfigResolver::envTruthy("OFF")); CHECK_FALSE(ConfigResolver::envTruthy("")); CHECK_FALSE(ConfigResolver::envTruthy(nullptr)); } SECTION("unrecognized values are truthy (not in falsey list)") { // The implementation treats anything NOT in {0, false, off, no} as truthy CHECK(ConfigResolver::envTruthy("garbage")); CHECK(ConfigResolver::envTruthy("maybe")); CHECK(ConfigResolver::envTruthy("123")); CHECK(ConfigResolver::envTruthy("yep")); CHECK(ConfigResolver::envTruthy("nope")); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver parseSimpleTomlFlat parses TOML files", "[daemon][components][config][catch2]") { SECTION("basic TOML parsing with sections") { auto configPath = writeToml("test.toml", R"( [daemon] socket_path = "/tmp/test.sock" log_level = "debug" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["daemon.socket_path"] == "/tmp/test.sock"); CHECK(config["daemon.log_level"] == "debug"); } SECTION("missing file returns empty map") { auto config = ConfigResolver::parseSimpleTomlFlat(tempDir / "nonexistent.toml"); CHECK(config.empty()); } SECTION("empty file returns empty map") { auto configPath = writeToml("empty.toml", ""); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config.empty()); } SECTION("comments are ignored") { auto configPath = writeToml("comments.toml", R"( # This is a comment [section] # Another comment key = "value" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["section.key"] == "value"); CHECK(config.size() == 1); } SECTION("multiple sections parsed correctly") { auto configPath = writeToml("multi.toml", R"( [section1] key1 = "value1" [section2] key2 = "value2" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["section1.key1"] == "value1"); CHECK(config["section2.key2"] == "value2"); } SECTION("daemon resolver delegates to shared parser semantics") { auto configPath = writeToml("shared_semantics.toml", R"( top_level = "root" # inline comment [search.path_tree] enable = true mode = "preferred" quoted_hash = "# not a comment" [daemon] socket_path = "/tmp/yams.sock" )"); auto resolverConfig = ConfigResolver::parseSimpleTomlFlat(configPath); auto sharedConfig = yams::config::parse_simple_toml(configPath); CHECK(resolverConfig == sharedConfig); CHECK(resolverConfig["top_level"] == "root"); CHECK(resolverConfig["search.path_tree.enable"] == "true"); CHECK(resolverConfig["search.path_tree.mode"] == "preferred"); CHECK(resolverConfig["search.path_tree.quoted_hash"] == "# not a comment"); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver resolveDefaultConfigPath searches standard paths", "[daemon][components][config][catch2]") { SECTION("YAMS_CONFIG_PATH env var takes precedence") { auto configPath = writeToml("custom.toml", "[daemon]\nlog_level = \"trace\"\n"); EnvGuard guard("YAMS_CONFIG_PATH", configPath.string()); auto resolved = ConfigResolver::resolveDefaultConfigPath(); CHECK(resolved == configPath); } SECTION("returns empty path if no config found") { // Clear env var to test fallback EnvGuard guard("YAMS_CONFIG_PATH", ""); // Note: this test may find system config, so we just check it returns a valid result auto resolved = ConfigResolver::resolveDefaultConfigPath(); // Either found or empty - both are valid outcomes CHECK((resolved.empty() || std::filesystem::exists(resolved) || !resolved.empty())); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver resolveEmbeddingBackend defaults to simeon for new installs", "[daemon][components][config][catch2]") { EnvGuard backendGuard("YAMS_EMBED_BACKEND", ""); auto configPath = writeToml("embedding_backend_default.toml", R"( [core] data_dir = "~/.yams/data" )"); EnvGuard configGuard("YAMS_CONFIG_PATH", configPath.string()); CHECK(ConfigResolver::resolveEmbeddingBackend() == "simeon"); CHECK(ConfigResolver::resolveEmbeddingBackend("auto") == "auto"); } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver resolveEmbeddingBackend reads and normalizes configured backend", "[daemon][components][config][catch2]") { EnvGuard backendGuard("YAMS_EMBED_BACKEND", ""); SECTION("reads daemon from config") { auto configPath = writeToml("embedding_backend_daemon.toml", R"( [embeddings] backend = "daemon" )"); EnvGuard configGuard("YAMS_CONFIG_PATH", configPath.string()); CHECK(ConfigResolver::resolveEmbeddingBackend() == "daemon"); } SECTION("normalizes uppercase simeon from config") { auto configPath = writeToml("embedding_backend_simeon_upper.toml", R"( [embeddings] backend = "SIMEON" )"); EnvGuard configGuard("YAMS_CONFIG_PATH", configPath.string()); CHECK(ConfigResolver::resolveEmbeddingBackend() == "simeon"); } SECTION("maps legacy onnx backend names to onnxruntime") { auto configPath = writeToml("embedding_backend_legacy_onnx.toml", R"( [embeddings] backend = "local_onnx" )"); EnvGuard configGuard("YAMS_CONFIG_PATH", configPath.string()); CHECK(ConfigResolver::resolveEmbeddingBackend() == "onnxruntime"); } SECTION("normalizes explicit onnxruntime from config") { auto configPath = writeToml("embedding_backend_onnxruntime.toml", R"( [embeddings] backend = "ONNX-RUNTIME" )"); EnvGuard configGuard("YAMS_CONFIG_PATH", configPath.string()); CHECK(ConfigResolver::resolveEmbeddingBackend() == "onnxruntime"); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver resolveEmbeddingBackend maps ONNX env aliases to onnxruntime", "[daemon][components][config][catch2]") { SECTION("maps onnx env alias") { EnvGuard backendGuard("YAMS_EMBED_BACKEND", "onnx"); CHECK(ConfigResolver::resolveEmbeddingBackend() == "onnxruntime"); } SECTION("maps local_onnx env alias") { EnvGuard backendGuard("YAMS_EMBED_BACKEND", "local_onnx"); CHECK(ConfigResolver::resolveEmbeddingBackend() == "onnxruntime"); } SECTION("maps ort env alias") { EnvGuard backendGuard("YAMS_EMBED_BACKEND", "ort"); CHECK(ConfigResolver::resolveEmbeddingBackend() == "onnxruntime"); } SECTION("keeps daemon env selection distinct") { EnvGuard backendGuard("YAMS_EMBED_BACKEND", "daemon"); CHECK(ConfigResolver::resolveEmbeddingBackend() == "daemon"); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveTopologyTunerPolicy defaults are empty when missing", "[daemon][components][config][topology_tuner][catch2]") { auto configPath = writeToml("config.toml", R"TOML( [daemon] socket_path = "/tmp/test.sock" )TOML"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveTopologyTunerPolicy(); CHECK_FALSE(policy.enabled.has_value()); CHECK_FALSE(policy.cooldownMinutes.has_value()); CHECK_FALSE(policy.docCountDelta.has_value()); CHECK_FALSE(policy.rewardAlphaSingleton.has_value()); CHECK_FALSE(policy.rewardBetaGiantCluster.has_value()); CHECK_FALSE(policy.rewardGammaGiniDeviation.has_value()); CHECK_FALSE(policy.rewardDeltaIntraEdge.has_value()); } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveTopologyRoutingPolicy reads populated TOML block", "[daemon][components][config][topology_routing][catch2]") { auto configPath = writeToml("config.toml", R"TOML( [search.topology] enable_weak_query_routing = true max_clusters = 3 max_docs = 42 medoid_boost = 0.2 rrf_k = 33 )TOML"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveTopologyRoutingPolicy(); REQUIRE(policy.enableWeakQueryRouting.has_value()); CHECK(*policy.enableWeakQueryRouting == true); REQUIRE(policy.maxClusters.has_value()); CHECK(*policy.maxClusters == 3U); REQUIRE(policy.maxDocs.has_value()); CHECK(*policy.maxDocs == 42U); REQUIRE(policy.medoidBoost.has_value()); CHECK(*policy.medoidBoost > 0.19f); CHECK(*policy.medoidBoost < 0.21f); REQUIRE(policy.rrfK.has_value()); CHECK(*policy.rrfK == 33.0f); } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveTopologyRoutingPolicy honors legacy env overlays", "[daemon][components][config][topology_routing][catch2]") { EnvGuard cfg("YAMS_CONFIG_PATH", ""); EnvGuard enable("YAMS_SEARCH_ENABLE_TOPOLOGY_WEAK_ROUTING", "1"); EnvGuard clusters("YAMS_SEARCH_TOPOLOGY_MAX_CLUSTERS", "5"); EnvGuard docs("YAMS_SEARCH_TOPOLOGY_MAX_DOCS", "17"); EnvGuard medoid("YAMS_SEARCH_TOPOLOGY_MEDOID_BOOST", "0.15"); auto policy = ConfigResolver::resolveTopologyRoutingPolicy(); REQUIRE(policy.enableWeakQueryRouting.has_value()); CHECK(*policy.enableWeakQueryRouting == true); REQUIRE(policy.maxClusters.has_value()); CHECK(*policy.maxClusters == 5U); REQUIRE(policy.maxDocs.has_value()); CHECK(*policy.maxDocs == 17U); REQUIRE(policy.medoidBoost.has_value()); CHECK(*policy.medoidBoost > 0.14f); CHECK(*policy.medoidBoost < 0.16f); } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveEmbeddingSelectionPolicy reads semantic graph ingest flag", "[daemon][components][config][embeddings][catch2]") { auto configPath = writeToml("config.toml", R"TOML( [embeddings.selection] update_semantic_graph_during_ingest = false )TOML"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveEmbeddingSelectionPolicy(); CHECK(policy.updateSemanticGraphDuringIngest == false); } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveTopologyTunerPolicy reads populated TOML block", "[daemon][components][config][topology_tuner][catch2]") { auto configPath = writeToml("config.toml", R"TOML( [topology.tuner] enabled = true cooldown_minutes = 30 doc_count_delta = 250 [topology.tuner.reward] alpha_singleton = 0.5 beta_giant_cluster = 0.3 gamma_gini_deviation = 0.15 delta_intra_edge = 0.05 )TOML"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveTopologyTunerPolicy(); REQUIRE(policy.enabled.has_value()); CHECK(*policy.enabled == true); REQUIRE(policy.cooldownMinutes.has_value()); CHECK(*policy.cooldownMinutes == 30u); REQUIRE(policy.docCountDelta.has_value()); CHECK(*policy.docCountDelta == 250u); REQUIRE(policy.rewardAlphaSingleton.has_value()); CHECK(*policy.rewardAlphaSingleton == 0.5); REQUIRE(policy.rewardBetaGiantCluster.has_value()); CHECK(*policy.rewardBetaGiantCluster == 0.3); REQUIRE(policy.rewardGammaGiniDeviation.has_value()); CHECK(*policy.rewardGammaGiniDeviation == 0.15); REQUIRE(policy.rewardDeltaIntraEdge.has_value()); CHECK(*policy.rewardDeltaIntraEdge == 0.05); } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveTopologyTunerPolicy honors disabled flag explicitly", "[daemon][components][config][topology_tuner][catch2]") { auto configPath = writeToml("config.toml", R"TOML( [topology.tuner] enabled = false )TOML"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveTopologyTunerPolicy(); REQUIRE(policy.enabled.has_value()); CHECK(*policy.enabled == false); } TEST_CASE("ConfigResolver vector sentinel operations", "[daemon][components][config][catch2][sentinel]") { auto tempDir = std::filesystem::temp_directory_path() / ("yams_sentinel_test_" + std::to_string(std::chrono::steady_clock::now().time_since_epoch().count())); std::filesystem::create_directories(tempDir); SECTION("readVectorSentinelDim returns nullopt for missing file") { auto dim = ConfigResolver::readVectorSentinelDim(tempDir); CHECK_FALSE(dim.has_value()); } SECTION("writeVectorSentinel then readVectorSentinelDim round-trips") { ConfigResolver::writeVectorSentinel(tempDir, 384, "test_table", 1); auto dim = ConfigResolver::readVectorSentinelDim(tempDir); REQUIRE(dim.has_value()); CHECK(dim.value() == 384); } SECTION("different dimensions are preserved") { for (size_t testDim : {128, 256, 384, 768, 1024}) { ConfigResolver::writeVectorSentinel(tempDir, testDim, "test", 1); auto dim = ConfigResolver::readVectorSentinelDim(tempDir); REQUIRE(dim.has_value()); CHECK(dim.value() == testDim); } } std::error_code ec; std::filesystem::remove_all(tempDir, ec); } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver parses [tuning] section", "[daemon][components][config][catch2]") { SECTION("efficient profile parses correctly") { auto configPath = writeToml("efficient.toml", R"( [tuning] profile = "efficient" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["tuning.profile"] == "efficient"); } SECTION("balanced profile parses correctly") { auto configPath = writeToml("balanced.toml", R"( [tuning] profile = "balanced" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["tuning.profile"] == "balanced"); } SECTION("aggressive profile parses correctly") { auto configPath = writeToml("aggressive.toml", R"( [tuning] profile = "aggressive" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["tuning.profile"] == "aggressive"); } SECTION("conservative alias for efficient") { auto configPath = writeToml("conservative.toml", R"( [tuning] profile = "conservative" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["tuning.profile"] == "conservative"); } SECTION("case-insensitive profile names") { auto configPath = writeToml("case_test.toml", R"( [tuning] profile = "EFFICIENT" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["tuning.profile"] == "EFFICIENT"); } SECTION("missing tuning section returns empty") { auto configPath = writeToml("no_tuning.toml", R"( [daemon] socket_path = "/tmp/test.sock" )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config.find("tuning.profile") == config.end()); } SECTION("multiple tuning options parsed together") { auto configPath = writeToml("multi_tuning.toml", R"( [tuning] profile = "aggressive" pool_cooldown_ms = 250 worker_poll_ms = 100 )"); auto config = ConfigResolver::parseSimpleTomlFlat(configPath); CHECK(config["tuning.profile"] == "aggressive"); CHECK(config["tuning.pool_cooldown_ms"] == "250"); CHECK(config["tuning.worker_poll_ms"] == "100"); } } TEST_CASE("Tuning profile from config affects TuneAdvisor methods", "[daemon][components][config][catch2]") { SECTION("efficient profile scales post-ingest concurrency down") { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Efficient); EnvGuard maxThreadsGuard("YAMS_MAX_THREADS", "0"); EnvGuard postIngestGuard("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); yams::daemon::TuneAdvisor::setHardwareConcurrencyForTests(8); // Efficient profile scale is 0.0, old totalBudget = 2 // With active-stage floor: total = max(2, 6 active stages) = 6 // Each stage gets 1 slot; postIngestBatchSize = max(1, floor(8.0 * 0.0)) = 1 CHECK(TuneAdvisor::postExtractionConcurrent() == 1u); CHECK(TuneAdvisor::postKgConcurrent() == 1u); CHECK(TuneAdvisor::postSymbolConcurrent() == 1u); CHECK(TuneAdvisor::postEntityConcurrent() == 1u); CHECK(TuneAdvisor::postTitleConcurrent() == 1u); CHECK(TuneAdvisor::postEmbedConcurrent() == 1u); CHECK(TuneAdvisor::postIngestBatchSize() == 1u); } SECTION("balanced profile uses medium values") { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Balanced); EnvGuard maxThreadsGuard("YAMS_MAX_THREADS", "0"); EnvGuard postIngestGuard("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); yams::daemon::TuneAdvisor::setHardwareConcurrencyForTests(8); // Balanced profile scale is 0.5, old totalBudget = 2 // With active-stage floor: total = max(2, 6 active stages) = 6 // Each stage gets 1 slot; postIngestBatchSize = max(1, floor(8.0 * 0.5)) = 4 CHECK(TuneAdvisor::postExtractionConcurrent() == 1u); CHECK(TuneAdvisor::postKgConcurrent() == 1u); CHECK(TuneAdvisor::postSymbolConcurrent() == 1u); CHECK(TuneAdvisor::postEntityConcurrent() == 1u); CHECK(TuneAdvisor::postTitleConcurrent() == 1u); CHECK(TuneAdvisor::postEmbedConcurrent() == 1u); CHECK(TuneAdvisor::postIngestBatchSize() == 4u); } SECTION("aggressive profile uses maximum values") { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Aggressive); EnvGuard maxThreadsGuard("YAMS_MAX_THREADS", "0"); EnvGuard postIngestGuard("YAMS_POST_INGEST_TOTAL_CONCURRENT", "0"); yams::daemon::TuneAdvisor::setHardwareConcurrencyForTests(8); // Aggressive profile scale is 1.0, old totalBudget = 3 // With active-stage floor: total = max(3, 6 active stages) = 6 // Each stage gets 1 slot; postIngestBatchSize = max(1, floor(8.0 * 1.0)) = 8 CHECK(TuneAdvisor::postExtractionConcurrent() == 1u); CHECK(TuneAdvisor::postKgConcurrent() == 1u); CHECK(TuneAdvisor::postSymbolConcurrent() == 1u); CHECK(TuneAdvisor::postEntityConcurrent() == 1u); CHECK(TuneAdvisor::postTitleConcurrent() == 1u); CHECK(TuneAdvisor::postEmbedConcurrent() == 1u); CHECK(TuneAdvisor::postIngestBatchSize() == 8u); } SECTION("profile affects cpuBudgetPercent") { { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Efficient); CHECK(TuneAdvisor::cpuBudgetPercent() == 40u); } { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Balanced); CHECK(TuneAdvisor::cpuBudgetPercent() == 50u); } { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Aggressive); // 40.0 + 1.0 * 20.0 = 60 CHECK(TuneAdvisor::cpuBudgetPercent() == 60u); } } SECTION("profile affects poolCooldownMs") { { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Efficient); CHECK(TuneAdvisor::poolCooldownMs() == 750u); } { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Balanced); CHECK(TuneAdvisor::poolCooldownMs() == 500u); } { ProfileGuard guard(yams::daemon::TuneAdvisor::Profile::Aggressive); CHECK(TuneAdvisor::poolCooldownMs() == 250u); } } } TEST_CASE("YAMS_TUNING_PROFILE env var overrides config", "[daemon][components][config][catch2]") { auto clearProfileOverride = []() { yams::daemon::TuneAdvisor::tuningProfileOverride_.store(0, std::memory_order_relaxed); }; SECTION("efficient profile from env var") { clearProfileOverride(); EnvGuard envGuard("YAMS_TUNING_PROFILE", "efficient"); auto profile = yams::daemon::TuneAdvisor::tuningProfile(); CHECK(profile == yams::daemon::TuneAdvisor::Profile::Efficient); } SECTION("aggressive profile from env var") { clearProfileOverride(); EnvGuard envGuard("YAMS_TUNING_PROFILE", "aggressive"); auto profile = yams::daemon::TuneAdvisor::tuningProfile(); CHECK(profile == yams::daemon::TuneAdvisor::Profile::Aggressive); } SECTION("conservative alias maps to efficient") { clearProfileOverride(); EnvGuard envGuard("YAMS_TUNING_PROFILE", "conservative"); auto profile = yams::daemon::TuneAdvisor::Profile::Efficient; CHECK(yams::daemon::TuneAdvisor::tuningProfile() == profile); } SECTION("invalid env var falls back to balanced") { clearProfileOverride(); EnvGuard envGuard("YAMS_TUNING_PROFILE", "invalid_profile"); auto profile = yams::daemon::TuneAdvisor::tuningProfile(); CHECK(profile == yams::daemon::TuneAdvisor::Profile::Balanced); } SECTION("empty env var falls back to balanced") { clearProfileOverride(); EnvGuard envGuard("YAMS_TUNING_PROFILE", ""); auto profile = yams::daemon::TuneAdvisor::tuningProfile(); CHECK(profile == yams::daemon::TuneAdvisor::Profile::Balanced); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolvePostIngestCaps reads [tuning.post_ingest]", "[daemon][components][config][post_ingest][catch2]") { SECTION("missing section returns all nullopt") { auto configPath = writeToml("no_post_ingest.toml", R"( [tuning] profile = "balanced" )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto caps = ConfigResolver::resolvePostIngestCaps(); CHECK_FALSE(caps.totalConcurrent.has_value()); CHECK_FALSE(caps.embedConcurrent.has_value()); CHECK_FALSE(caps.extractionConcurrent.has_value()); CHECK_FALSE(caps.kgConcurrent.has_value()); CHECK_FALSE(caps.symbolConcurrent.has_value()); CHECK_FALSE(caps.entityConcurrent.has_value()); CHECK_FALSE(caps.titleConcurrent.has_value()); } SECTION("all keys populate the struct") { auto configPath = writeToml("post_ingest.toml", R"( [tuning.post_ingest] total_concurrent = 12 embed_concurrent = 4 extraction_concurrent = 5 kg_concurrent = 6 symbol_concurrent = 3 entity_concurrent = 2 title_concurrent = 2 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto caps = ConfigResolver::resolvePostIngestCaps(); REQUIRE(caps.totalConcurrent.has_value()); CHECK(*caps.totalConcurrent == 12u); REQUIRE(caps.embedConcurrent.has_value()); CHECK(*caps.embedConcurrent == 4u); REQUIRE(caps.extractionConcurrent.has_value()); CHECK(*caps.extractionConcurrent == 5u); REQUIRE(caps.kgConcurrent.has_value()); CHECK(*caps.kgConcurrent == 6u); REQUIRE(caps.symbolConcurrent.has_value()); CHECK(*caps.symbolConcurrent == 3u); REQUIRE(caps.entityConcurrent.has_value()); CHECK(*caps.entityConcurrent == 2u); REQUIRE(caps.titleConcurrent.has_value()); CHECK(*caps.titleConcurrent == 2u); } SECTION("out-of-range values are dropped (nullopt)") { auto configPath = writeToml("oor.toml", R"( [tuning.post_ingest] total_concurrent = 0 embed_concurrent = 99 extraction_concurrent = 500 entity_concurrent = 17 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto caps = ConfigResolver::resolvePostIngestCaps(); CHECK_FALSE(caps.totalConcurrent.has_value()); CHECK_FALSE(caps.embedConcurrent.has_value()); CHECK_FALSE(caps.extractionConcurrent.has_value()); CHECK_FALSE(caps.entityConcurrent.has_value()); } SECTION("partial population — only set keys populate") { auto configPath = writeToml("partial.toml", R"( [tuning.post_ingest] embed_concurrent = 3 kg_concurrent = 5 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto caps = ConfigResolver::resolvePostIngestCaps(); CHECK_FALSE(caps.totalConcurrent.has_value()); REQUIRE(caps.embedConcurrent.has_value()); CHECK(*caps.embedConcurrent == 3u); CHECK_FALSE(caps.extractionConcurrent.has_value()); REQUIRE(caps.kgConcurrent.has_value()); CHECK(*caps.kgConcurrent == 5u); CHECK_FALSE(caps.symbolConcurrent.has_value()); CHECK_FALSE(caps.entityConcurrent.has_value()); CHECK_FALSE(caps.titleConcurrent.has_value()); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveSimeonEncoderPolicy reads [embeddings.simeon]", "[daemon][components][config][simeon][catch2]") { SECTION("missing section returns all nullopt") { auto configPath = writeToml("no_simeon.toml", R"( [daemon] log_level = "info" )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); for (auto* env : {"YAMS_SIMEON_NGRAM_MODE", "YAMS_SIMEON_NGRAM_MIN", "YAMS_SIMEON_NGRAM_MAX", "YAMS_SIMEON_SKETCH_DIM", "YAMS_SIMEON_OUTPUT_DIM", "YAMS_SIMEON_PROJECTION", "YAMS_SIMEON_PQ_BYTES"}) { unsetenv(env); } auto policy = ConfigResolver::resolveSimeonEncoderPolicy(); CHECK_FALSE(policy.ngramMode.has_value()); CHECK_FALSE(policy.ngramMin.has_value()); CHECK_FALSE(policy.ngramMax.has_value()); CHECK_FALSE(policy.sketchDim.has_value()); CHECK_FALSE(policy.outputDim.has_value()); CHECK_FALSE(policy.projection.has_value()); CHECK_FALSE(policy.l2Normalize.has_value()); CHECK_FALSE(policy.pqBytes.has_value()); } SECTION("populated TOML section fills every field") { auto configPath = writeToml("simeon_encoder.toml", R"( [embeddings.simeon] ngram_mode = "char_and_word" ngram_min = 3 ngram_max = 5 sketch_dim = 4096 output_dim = 384 projection = "fwht" l2_normalize = true pq_bytes = 64 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); for (auto* env : {"YAMS_SIMEON_NGRAM_MODE", "YAMS_SIMEON_NGRAM_MIN", "YAMS_SIMEON_NGRAM_MAX", "YAMS_SIMEON_SKETCH_DIM", "YAMS_SIMEON_OUTPUT_DIM", "YAMS_SIMEON_PROJECTION", "YAMS_SIMEON_PQ_BYTES"}) { unsetenv(env); } auto policy = ConfigResolver::resolveSimeonEncoderPolicy(); REQUIRE(policy.ngramMode.has_value()); CHECK(*policy.ngramMode == "char_and_word"); REQUIRE(policy.ngramMin.has_value()); CHECK(*policy.ngramMin == 3u); REQUIRE(policy.ngramMax.has_value()); CHECK(*policy.ngramMax == 5u); REQUIRE(policy.sketchDim.has_value()); CHECK(*policy.sketchDim == 4096u); REQUIRE(policy.outputDim.has_value()); CHECK(*policy.outputDim == 384u); REQUIRE(policy.projection.has_value()); CHECK(*policy.projection == "fwht"); REQUIRE(policy.l2Normalize.has_value()); CHECK(*policy.l2Normalize); REQUIRE(policy.pqBytes.has_value()); CHECK(*policy.pqBytes == 64u); } SECTION("env vars override TOML values") { auto configPath = writeToml("override_encoder.toml", R"( [embeddings.simeon] projection = "achlioptas_sparse" sketch_dim = 2048 output_dim = 256 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); EnvGuard proj("YAMS_SIMEON_PROJECTION", "fwht"); EnvGuard sketch("YAMS_SIMEON_SKETCH_DIM", "8192"); EnvGuard out("YAMS_SIMEON_OUTPUT_DIM", "1024"); auto policy = ConfigResolver::resolveSimeonEncoderPolicy(); REQUIRE(policy.projection.has_value()); CHECK(*policy.projection == "fwht"); REQUIRE(policy.sketchDim.has_value()); CHECK(*policy.sketchDim == 8192u); REQUIRE(policy.outputDim.has_value()); CHECK(*policy.outputDim == 1024u); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveSimeonBm25Policy reads [embeddings.simeon.bm25]", "[daemon][components][config][simeon][catch2]") { SECTION("missing section returns all nullopt") { auto configPath = writeToml("no_bm25.toml", R"( [daemon] log_level = "info" )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); for (auto* env : {"YAMS_SIMEON_BM25_ENABLED", "YAMS_SIMEON_BM25_VARIANT", "YAMS_SIMEON_BM25_SUBWORD_GAMMA", "YAMS_SIMEON_BM25_MAX_CORPUS_DOCS"}) { unsetenv(env); } auto policy = ConfigResolver::resolveSimeonBm25Policy(); CHECK_FALSE(policy.enabled.has_value()); CHECK_FALSE(policy.variant.has_value()); CHECK_FALSE(policy.subwordGamma.has_value()); CHECK_FALSE(policy.maxCorpusDocs.has_value()); } SECTION("populated TOML section fills every field") { auto configPath = writeToml("bm25.toml", R"( [embeddings.simeon.bm25] enabled = true variant = "sab_smooth" subword_gamma = 5.0 max_corpus_docs = 200000 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); for (auto* env : {"YAMS_SIMEON_BM25_ENABLED", "YAMS_SIMEON_BM25_VARIANT", "YAMS_SIMEON_BM25_SUBWORD_GAMMA", "YAMS_SIMEON_BM25_MAX_CORPUS_DOCS"}) { unsetenv(env); } auto policy = ConfigResolver::resolveSimeonBm25Policy(); REQUIRE(policy.enabled.has_value()); CHECK(*policy.enabled); REQUIRE(policy.variant.has_value()); CHECK(*policy.variant == "sab_smooth"); REQUIRE(policy.subwordGamma.has_value()); CHECK(*policy.subwordGamma == 5.0f); REQUIRE(policy.maxCorpusDocs.has_value()); CHECK(*policy.maxCorpusDocs == 200000u); } SECTION("env vars override TOML values") { auto configPath = writeToml("override_bm25.toml", R"( [embeddings.simeon.bm25] variant = "atire" subword_gamma = 3.0 max_corpus_docs = 50000 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); EnvGuard variantEnv("YAMS_SIMEON_BM25_VARIANT", "sab_smooth"); EnvGuard gammaEnv("YAMS_SIMEON_BM25_SUBWORD_GAMMA", "7.5"); EnvGuard capEnv("YAMS_SIMEON_BM25_MAX_CORPUS_DOCS", "123456"); auto policy = ConfigResolver::resolveSimeonBm25Policy(); REQUIRE(policy.variant.has_value()); CHECK(*policy.variant == "sab_smooth"); REQUIRE(policy.subwordGamma.has_value()); CHECK(*policy.subwordGamma == 7.5f); REQUIRE(policy.maxCorpusDocs.has_value()); CHECK(*policy.maxCorpusDocs == 123456u); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveRerankerBackendPolicy reads [search]", "[daemon][components][config][reranker][catch2]") { SECTION("returns unset when not configured") { auto configPath = writeToml("reranker_default.toml", R"( [search] default_limit = 10 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveRerankerBackendPolicy(); CHECK_FALSE(policy.backend.has_value()); } SECTION("reads explicit reranker backend") { auto configPath = writeToml("reranker_backend.toml", R"( [search] reranker_backend = "colbert" )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveRerankerBackendPolicy(); REQUIRE(policy.backend.has_value()); CHECK(*policy.backend == "colbert"); } SECTION("normalizes reranker backend to lowercase") { auto configPath = writeToml("reranker_backend_upper.toml", R"( [search] reranker_backend = "ONNX" )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); auto policy = ConfigResolver::resolveRerankerBackendPolicy(); REQUIRE(policy.backend.has_value()); CHECK(*policy.backend == "onnx"); } } TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver::resolveInstrumentationPolicy applies memory profile", "[daemon][components][config][instrumentation][catch2]") { SECTION("auto profile activates when MallocStackLoggingNoCompact is present") { auto configPath = writeToml("instrumentation_auto.toml", R"( [daemon] log_level = "info" )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); EnvGuard msl("MallocStackLoggingNoCompact", "1"); EnvGuard mslCompact("MallocStackLogging", "0"); auto policy = ConfigResolver::resolveInstrumentationPolicy(); CHECK(policy.profile == "auto"); CHECK(policy.memoryProfileActive); CHECK(policy.suppressAutoRepair); CHECK(policy.suppressSimeonLexicalBuild); CHECK(policy.suppressVectorIndexBuild); } SECTION("normal profile disables auto MSL suppression") { auto configPath = writeToml("instrumentation_normal.toml", R"( [daemon.instrumentation] profile = "normal" )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); EnvGuard msl("MallocStackLoggingNoCompact", "1"); auto policy = ConfigResolver::resolveInstrumentationPolicy(); CHECK(policy.profile == "normal"); CHECK_FALSE(policy.memoryProfileActive); CHECK_FALSE(policy.suppressAutoRepair); CHECK_FALSE(policy.suppressSimeonLexicalBuild); CHECK_FALSE(policy.suppressVectorIndexBuild); } SECTION("memory profile supports explicit overrides and stack-log threshold") { auto configPath = writeToml("instrumentation_memory.toml", R"( [daemon.instrumentation] profile = "memory" suppress_auto_repair = false suppress_simeon_lexical_build = true suppress_vector_index_build = true msl_stack_log_warn_mb = 1536 )"); EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string()); EnvGuard msl("MallocStackLogging", "0"); EnvGuard mslCompact("MallocStackLoggingNoCompact", "0"); auto policy = ConfigResolver::resolveInstrumentationPolicy(); CHECK(policy.profile == "memory"); CHECK(policy.memoryProfileActive); CHECK_FALSE(policy.suppressAutoRepair); CHECK(policy.suppressSimeonLexicalBuild); CHECK(policy.suppressVectorIndexBuild); CHECK(policy.mslStackLogWarnBytes == 1536ULL * 1024ULL * 1024ULL); } }