* refactor(tuning): prune dormant runtime knobs Remove unused tuning controls and adaptive search budget state. Extract testable ONNX slot reconciliation and search telemetry hydration seams, and correct the Apple Silicon sysctl value width. * refactor(vector): retire dormant backends Remove unused FAISS, model-infrastructure, VectorIndexManager, and TurboQuant paths while keeping Simeon PQ and float-vector storage. Migrate unused V2.2 quantized schemas transactionally to V2.3. Databases containing non-default quantized formats or packed-code blobs fail closed with recovery guidance and no mutation. * refactor(plugins): remove legacy SDK tooling Remove the Ghidra and SDK submodules, their bespoke tests, and stale scan exclusions. Retain generic PluginProcess coverage against the repository mock fixture. * build(vector): remove stale FAISS overrides * refactor(ipc): remove dormant FSM snapshots * refactor(ingest): unify bounded KG enqueue * fix(plugins): harden allocation contracts Keep ABI outputs null or fully initialized when allocations fail, and represent empty object payloads without relying on malloc(0). * fix(storage): define empty object reads Return a null buffer with zero length for successful empty-object reads instead of treating malloc(0) as an allocation contract. * refactor(storage): trim through string views * test: prune dormant harness helpers Remove unused readiness and fixture helpers, and keep benchmark client-count doubling in a wide integer domain. * chore(cpp): initialize analyzer-facing locals * build(simeon): advance analyzer cleanup * refactor(config): drop dead daemon client keys * refactor(ipc): inject daemon client transport Socket and in-process execution now share DaemonClient request dispatch, so CLI fallbacks do not need alternate service paths. * refactor(retrieval): route CLI through daemon Search and grep now use the same daemon handlers over socket or in-process transport. Live grep remains an explicit freshness inspection. * refactor(graph): move CLI scope into daemon Graph CLI requests now share daemon handlers across socket and in-process transports. CWD filtering occurs before pagination so scoped totals and pages stay correct. * build: isolate test-only runtime controls * refactor(metadata): decouple lock telemetry * fix(services): return on request timeout * fix(retrieval): enforce workspace scope * fix(graph): scope nodes before pagination * fix(grep): drop binary result lines * fix(daemon): bound checkpoint wait * fix(repair): use KG health checks * fix(storage): always verify compressed CRC * perf(vector): skip unchanged PQ snapshots * build(zyp): exclude bench from default build * build: protect Meson files from formatter * refactor: remove unreachable production APIs * refactor(daemon): remove dormant runtime state * fix(vector): harden daemon provider calls Timed-out coroutines must own their client and requests. Provider discovery must also fail instead of returning synthetic embeddings in production. * refactor(storage): remove unusable adapters * fix(cli): unify in-process add requests * fix(tsan): preserve async worker progress * test(cli): isolate daemon-dependent suites * fix(repair): expose queue backoff progress
1345 lines
50 KiB
C++
1345 lines
50 KiB
C++
// 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 <catch2/catch_approx.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
#include <catch2/matchers/catch_matchers_string.hpp>
|
|
|
|
#include <yams/compat/unistd.h>
|
|
#include <yams/config/config_helpers.h>
|
|
#include <yams/config/config_migration.h>
|
|
#include <yams/daemon/components/ConfigResolver.h>
|
|
#include <yams/daemon/components/TuneAdvisor.h>
|
|
#include <yams/daemon/daemon.h>
|
|
|
|
#include <chrono>
|
|
#include <cstdlib>
|
|
#include <filesystem>
|
|
#include <fstream>
|
|
#include <string>
|
|
|
|
#include <yams/vector/document_chunker.h>
|
|
|
|
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<std::string> 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 applies one typed tuning snapshot for startup and reload",
|
|
"[daemon][components][config][tuning][catch2]") {
|
|
ConfigResolver::ConfigSections sections;
|
|
sections["tuning"] = {{"target_cpu_percent", "175"},
|
|
{"post_ingest_capacity", "4096"},
|
|
{"post_ingest_threads_min", "3"},
|
|
{"control_interval_ms", "250"}};
|
|
sections["tuning.ingest"] = {{"store_batch_size", "23"}};
|
|
sections["tuning.post_ingest"] = {{"coalesce_ms", "3"}};
|
|
|
|
TuningConfig base;
|
|
base.postIngestThreadsMax = 12;
|
|
base.topologyAlgorithm = "multiscale";
|
|
|
|
const auto resolved = ConfigResolver::applyRuntimeTuning(sections, base);
|
|
CHECK(resolved.targetCpuPercent == 175);
|
|
CHECK(resolved.postIngestCapacity == 4096);
|
|
CHECK(resolved.postIngestThreadsMin == 3);
|
|
CHECK(resolved.ingestStoreBatchSize == 23);
|
|
CHECK(resolved.postIngestCoalesceMs == 3);
|
|
CHECK(resolved.controlIntervalMs == 250);
|
|
CHECK(resolved.postIngestThreadsMax == 12);
|
|
CHECK(resolved.topologyAlgorithm == "multiscale");
|
|
|
|
sections["tuning"] = {{"target_cpu_percent", "-1"},
|
|
{"post_ingest_capacity", "-2"},
|
|
{"control_interval_ms", "4294967296"}};
|
|
sections["tuning.ingest"] = {{"store_batch_size", "257"}};
|
|
sections["tuning.post_ingest"] = {{"coalesce_ms", "21"}};
|
|
base.targetCpuPercent = 80;
|
|
base.postIngestCapacity = 2048;
|
|
base.ingestStoreBatchSize = 32;
|
|
base.controlIntervalMs = 500;
|
|
base.postIngestCoalesceMs = 4;
|
|
const auto rejected = ConfigResolver::applyRuntimeTuning(sections, base);
|
|
CHECK(rejected.targetCpuPercent == 80);
|
|
CHECK(rejected.postIngestCapacity == 2048);
|
|
CHECK(rejected.ingestStoreBatchSize == 32);
|
|
CHECK(rejected.controlIntervalMs == 500);
|
|
CHECK(rejected.postIngestCoalesceMs == 4);
|
|
}
|
|
|
|
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<Case> 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::resolveTopologyEnginePolicy reads routing representatives",
|
|
"[daemon][components][config][topology][catch2]") {
|
|
auto configPath = writeToml("config.toml", R"TOML(
|
|
[topology]
|
|
engine = "connected"
|
|
routing_representatives = 4
|
|
boundary_spill = true
|
|
boundary_spill_limit = 1
|
|
boundary_spill_distance_ratio = +1.2
|
|
boundary_spill_residual_penalty = 1.5e0
|
|
|
|
[topology.features]
|
|
entity_fusion = true
|
|
entity_signature_k = 12
|
|
entity_fusion_alpha = 0.2
|
|
entity_min_confidence = 0.5
|
|
matryoshka_coarse_view = true
|
|
matryoshka_target_dim = 256
|
|
minhash_sketch = true
|
|
minhash_sketch_dim = 24
|
|
minhash_alpha = 0.15
|
|
)TOML");
|
|
|
|
EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string());
|
|
auto policy = ConfigResolver::resolveTopologyEnginePolicy();
|
|
|
|
REQUIRE(policy.engine.has_value());
|
|
CHECK(*policy.engine == "connected");
|
|
REQUIRE(policy.routingRepresentativeCount.has_value());
|
|
CHECK(*policy.routingRepresentativeCount == 4);
|
|
REQUIRE(policy.boundarySpillEnabled.has_value());
|
|
CHECK(*policy.boundarySpillEnabled);
|
|
REQUIRE(policy.boundarySpillLimit.has_value());
|
|
CHECK(*policy.boundarySpillLimit == 1);
|
|
REQUIRE(policy.boundarySpillDistanceRatio.has_value());
|
|
CHECK(*policy.boundarySpillDistanceRatio == Catch::Approx(1.2));
|
|
REQUIRE(policy.boundarySpillResidualPenalty.has_value());
|
|
CHECK(*policy.boundarySpillResidualPenalty == Catch::Approx(1.5));
|
|
CHECK(*policy.featureEntityFusion);
|
|
CHECK(*policy.featureEntitySignatureK == 12);
|
|
CHECK(*policy.featureEntityFusionAlpha == Catch::Approx(0.2F));
|
|
CHECK(*policy.featureEntityMinConfidence == Catch::Approx(0.5F));
|
|
CHECK(*policy.featureMatryoshkaCoarseView);
|
|
CHECK(*policy.featureMatryoshkaTargetDim == 256);
|
|
CHECK(*policy.featureMinHashSketch);
|
|
CHECK(*policy.featureMinHashSketchDim == 24);
|
|
CHECK(*policy.featureMinHashAlpha == Catch::Approx(0.15F));
|
|
}
|
|
|
|
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
|
|
vector_policy = shadow
|
|
min_clusters = 1
|
|
max_clusters = 3
|
|
max_seed_documents = 24
|
|
representative_limit = 2
|
|
ann_candidate_limit = 16
|
|
adaptive_probe_score_gap = 0.07
|
|
narrow_min_boundary_margin = 0.03
|
|
max_docs = 42
|
|
expansion_output_limit = 96
|
|
graph_weighted_seed_ranking = true
|
|
medoid_boost = 0.2
|
|
route_calibration_fingerprint = "atlas-123"
|
|
route_calibration_queries = 100
|
|
route_calibration_protected_candidates = 250
|
|
route_calibration_missed_protected_candidates = 2
|
|
route_min_calibration_queries = 75
|
|
route_max_misses_per_thousand = 10
|
|
route_calibration_min_boundary_margin = 0.2
|
|
route_calibration_min_seed_hits = 2
|
|
route_work_max_rows_visited = 64
|
|
route_work_max_exact_distance_evaluations = 32
|
|
route_work_max_ann_candidates = 48
|
|
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.vectorPolicy.has_value());
|
|
CHECK(*policy.vectorPolicy == "shadow");
|
|
REQUIRE(policy.maxClusters.has_value());
|
|
CHECK(*policy.maxClusters == 3U);
|
|
REQUIRE(policy.minClusters.has_value());
|
|
CHECK(*policy.minClusters == 1U);
|
|
REQUIRE(policy.maxSeedDocuments.has_value());
|
|
CHECK(*policy.maxSeedDocuments == 24U);
|
|
REQUIRE(policy.representativeLimit.has_value());
|
|
CHECK(*policy.representativeLimit == 2U);
|
|
REQUIRE(policy.annCandidateLimit.has_value());
|
|
CHECK(*policy.annCandidateLimit == 16U);
|
|
REQUIRE(policy.adaptiveProbeScoreGap.has_value());
|
|
CHECK(*policy.adaptiveProbeScoreGap == Catch::Approx(0.07F));
|
|
REQUIRE(policy.narrowMinBoundaryMargin.has_value());
|
|
CHECK(*policy.narrowMinBoundaryMargin == Catch::Approx(0.03F));
|
|
REQUIRE(policy.maxDocs.has_value());
|
|
CHECK(*policy.maxDocs == 42U);
|
|
REQUIRE(policy.expansionOutputLimit.has_value());
|
|
CHECK(*policy.expansionOutputLimit == 96U);
|
|
REQUIRE(policy.graphWeightedSeedRanking.has_value());
|
|
CHECK(*policy.graphWeightedSeedRanking);
|
|
REQUIRE(policy.medoidBoost.has_value());
|
|
CHECK(*policy.medoidBoost > 0.19f);
|
|
CHECK(*policy.medoidBoost < 0.21f);
|
|
CHECK(*policy.routeCalibrationFingerprint == "atlas-123");
|
|
CHECK(*policy.routeCalibrationQueries == 100U);
|
|
CHECK(*policy.routeCalibrationProtectedCandidates == 250U);
|
|
CHECK(*policy.routeCalibrationMissedProtectedCandidates == 2U);
|
|
CHECK(*policy.routeMinCalibrationQueries == 75U);
|
|
CHECK(*policy.routeMaxMissesPerThousand == 10U);
|
|
CHECK(*policy.routeCalibrationMinBoundaryMargin == Catch::Approx(0.2F));
|
|
CHECK(*policy.routeCalibrationMinSeedHits == 2U);
|
|
CHECK(*policy.routeWorkMaxRowsVisited == 64U);
|
|
CHECK(*policy.routeWorkMaxExactDistanceEvaluations == 32U);
|
|
CHECK(*policy.routeWorkMaxAnnCandidates == 48U);
|
|
REQUIRE(policy.rrfK.has_value());
|
|
CHECK(*policy.rrfK == 33.0f);
|
|
}
|
|
|
|
TEST_CASE("Generated config keeps topology-assisted hybrid search as the product default",
|
|
"[config][search][topology][catch2]") {
|
|
const auto defaults = yams::config::ConfigMigrator::getLatestConfigDefaults();
|
|
const auto topologyIt = defaults.find("search.topology");
|
|
REQUIRE(topologyIt != defaults.end());
|
|
|
|
const auto& topology = topologyIt->second;
|
|
CHECK(topology.at("mode") == "hybrid_assist");
|
|
CHECK(topology.at("vector_policy") == "shadow");
|
|
CHECK(topology.at("graph_weighted_seed_ranking") == "false");
|
|
CHECK_FALSE(topology.contains("enable_weak_query_routing"));
|
|
CHECK_FALSE(topology.contains("routing_variant"));
|
|
}
|
|
|
|
TEST_CASE_METHOD(ConfigResolverFixture, "ConfigResolver reads topology evidence weight",
|
|
"[daemon][components][config][topology_routing][catch2]") {
|
|
auto configPath = writeToml("config.toml", R"TOML(
|
|
[search.topology]
|
|
evidence_weight = 0.03
|
|
)TOML");
|
|
|
|
EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string());
|
|
auto topology = ConfigResolver::resolveTopologyRoutingPolicy();
|
|
REQUIRE(topology.evidenceWeight.has_value());
|
|
CHECK(*topology.evidenceWeight == Catch::Approx(0.03F));
|
|
}
|
|
|
|
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; batching remains a cap and does not reserve work.
|
|
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() == 32u);
|
|
}
|
|
|
|
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; batching remains a cap and does not reserve work.
|
|
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() == 32u);
|
|
}
|
|
|
|
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; batching remains a cap and does not reserve work.
|
|
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() == 32u);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
SECTION("fragment geometry encoder profile is read from TOML") {
|
|
auto configPath = writeToml("bm25_fragment_encoder.toml", R"(
|
|
[embeddings.simeon.bm25.fragment_geometry]
|
|
encoder_profile = "fixed_hash_384"
|
|
)");
|
|
EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string());
|
|
|
|
auto policy = ConfigResolver::resolveSimeonBm25Policy();
|
|
REQUIRE(policy.fragmentGeometryEncoderProfile.has_value());
|
|
CHECK(*policy.fragmentGeometryEncoderProfile == "fixed_hash_384");
|
|
}
|
|
|
|
SECTION("embedding encoder profile is read from TOML") {
|
|
auto configPath = writeToml("simeon_encoder_profile.toml", R"(
|
|
[embeddings.simeon]
|
|
encoder_profile = "fixed_hash_384"
|
|
)");
|
|
EnvGuard cfg("YAMS_CONFIG_PATH", configPath.string());
|
|
|
|
auto policy = ConfigResolver::resolveSimeonEncoderPolicy();
|
|
REQUIRE(policy.encoderProfile.has_value());
|
|
CHECK(*policy.encoderProfile == "fixed_hash_384");
|
|
}
|
|
}
|
|
|
|
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(DaemonConfig{});
|
|
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(DaemonConfig{});
|
|
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(DaemonConfig{});
|
|
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(DaemonConfig{});
|
|
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(DaemonConfig{});
|
|
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(DaemonConfig{});
|
|
CHECK(policy.profile == "memory");
|
|
CHECK(policy.memoryProfileActive);
|
|
CHECK_FALSE(policy.suppressAutoRepair);
|
|
CHECK(policy.suppressSimeonLexicalBuild);
|
|
CHECK(policy.suppressVectorIndexBuild);
|
|
CHECK(policy.mslStackLogWarnBytes == 1536ULL * 1024ULL * 1024ULL);
|
|
}
|
|
}
|