yams/tests/unit/vector/simeon_backend_catch2_test.cpp
Trevon 3f37e8f564
Sync experimental into main: Search retrieval improvements (#60)
* fix(graph): handle qualified C++ symbols

Signed-off-by: trvon <me@trevon.dev>

* refactor(vector)!: remove dead transition APIs

Split Simeon vector and retrieval linkage so consumers inherit only the runtime surface they use.

BREAKING CHANGE: remove unused async/model/config helpers and the undefined vector-backend factory from public headers.

Signed-off-by: trvon <me@trevon.dev>

* fix(vector): harden PQ snapshot lifecycle

Validate and atomically install persisted generations, revalidate after races, and fall back to exact search when a snapshot is stale. Add the exact-scan reference lane and distinguish observed work counters from unavailable backend metrics.

Signed-off-by: trvon <me@trevon.dev>

* feat(topology): expose route coordinates

Persist chart distortion and local-dimension evidence, retain pre-scalarization route axes, and emit detailed trace rows without treating unavailable vector work as zero.

Signed-off-by: trvon <me@trevon.dev>

* feat(xplan): add vector decision gates

Track binary and corpus identity, preserve missing-work semantics, and add repeated PQ routing, crossover, and rerank plans with quality and cost summaries.

Signed-off-by: trvon <me@trevon.dev>

* feat(topology): formalize coordinate routing

Separate chart geometry from route-risk admission and prove candidate-stage recall and strict-focus boundaries. Record the measured atlas limitations and literature basis for the next construction experiment.

Signed-off-by: trvon <me@trevon.dev>

* fix(search): serialize SymSpell readers

SQLite-backed lookups reuse mutable prepared statements, so shared-reader locking is unsafe. Advance the owned SymSpell revision for batched delete insertion and cover concurrent root lookups under TSAN.

Signed-off-by: trvon <me@trevon.dev>

* fix(vector): make mutations atomic

Preserve stored vectors on replacement and commit failures, reject cross-thread writes during explicit transactions, and restore exact reranking for quantized-primary PQ rows.

* fix(search)!: surface SymSpell write errors

Propagate persistent dictionary failures through metadata ingestion, roll back failed batches, and make clear remove stored terms.

BREAKING CHANGE: SymSpellSearch mutations now return Result values.

* feat(topology): formalize candidate expansion

* feat(vector): add coordinate-aware embeddings

* feat(topology): preserve protected relation cover

* feat(search): add graph candidate rescue

* feat(daemon): expose retrieval hydration telemetry

* feat(xplan): add rescue and hydration gates

* refactor(content): dedupe media handlers

Reuse shared graph presenter helpers and centralize repeated audio/video/archive buffer processing, validation, lifecycle, and stats helper logic.

* feat(lifecycle): prune stale corpus data

Keep cleanup preview-first while retention stays opt-in and long prune work no longer holds IPC responses open.

* feat(search): add topology candidate rescue

Preserve high-value routed candidates across vector backends while keeping exact rerank diagnostics explicit.

* feat(search): expose evidence paths

* docs: define mobile host contract

* formal: model bounded candidate rescue

* fix(prune): inspect extensionless signatures

* fix(prune): tolerate stale extensions

---------

Signed-off-by: trvon <me@trevon.dev>
2026-07-24 19:00:22 -04:00

191 lines
6.1 KiB
C++

#include <catch2/catch_test_macros.hpp>
#include <yams/compat/unistd.h>
#include <yams/vector/embedding_generator.h>
#include <yams/vector/simeon_embedding_backend.h>
#include <array>
#include <cmath>
#include <cstdlib>
#include <string>
#include <utility>
#include <vector>
#include <catch2/matchers/catch_matchers_string.hpp>
using namespace yams::vector;
using Catch::Matchers::StartsWith;
namespace {
EmbeddingConfig make_cfg(size_t dim = 256) {
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::Simeon;
cfg.embedding_dim = dim;
cfg.normalize_embeddings = true;
cfg.max_sequence_length = 512;
return cfg;
}
} // namespace
TEST_CASE("SimeonBackend initializes and reports its dimension", "[vector][simeon]") {
auto backend = makeSimeonBackend(make_cfg(384));
REQUIRE(backend != nullptr);
REQUIRE(backend->isAvailable());
REQUIRE(backend->initialize());
REQUIRE(backend->isInitialized());
REQUIRE(backend->getEmbeddingDimension() == 384u);
REQUIRE(backend->getBackendName() == "Simeon");
}
TEST_CASE("SimeonBackend fixed hash profile exposes the frozen retrieval space",
"[vector][simeon][identity]") {
auto cfg = make_cfg(1024);
cfg.simeon_encoder_profile = EmbeddingConfig::SimeonEncoderProfile::FixedHash384;
auto backend = makeSimeonBackend(cfg);
REQUIRE(backend != nullptr);
REQUIRE(backend->initialize());
CHECK(backend->getEmbeddingDimension() == 384u);
CHECK(backend->getEmbeddingSpaceIdentity() == "simeon-v1-384");
CHECK(simeonEmbeddingSpaceIdentity(cfg) == "simeon-v1-384");
}
TEST_CASE("SimeonBackend produces deterministic, unit-norm embeddings", "[vector][simeon]") {
auto backend = makeSimeonBackend(make_cfg(256));
REQUIRE(backend->initialize());
const std::string text = "The quick brown fox jumps over the lazy dog.";
auto r1 = backend->generateEmbedding(text);
auto r2 = backend->generateEmbedding(text);
REQUIRE(r1);
REQUIRE(r2);
REQUIRE(r1.value().size() == 256u);
REQUIRE(r1.value() == r2.value());
double acc = 0.0;
for (float f : r1.value())
acc += static_cast<double>(f) * static_cast<double>(f);
REQUIRE(std::fabs(std::sqrt(acc) - 1.0) < 1e-4);
}
TEST_CASE("SimeonBackend batch matches per-item encoding", "[vector][simeon]") {
auto backend = makeSimeonBackend(make_cfg(128));
REQUIRE(backend->initialize());
const std::vector<std::string> texts = {"alpha bravo charlie", "delta echo foxtrot",
"golf hotel india"};
auto per = std::vector<std::vector<float>>{};
for (const auto& t : texts) {
auto r = backend->generateEmbedding(t);
REQUIRE(r);
per.push_back(std::move(r).value());
}
auto batch = backend->generateEmbeddings(std::span<const std::string>(texts));
REQUIRE(batch);
REQUIRE(batch.value().size() == per.size());
for (size_t i = 0; i < per.size(); ++i) {
REQUIRE(batch.value()[i] == per[i]);
}
}
TEST_CASE("EmbeddingGenerator selects Simeon via env override", "[vector][simeon]") {
setenv("YAMS_EMBED_BACKEND", "simeon", 1);
EmbeddingConfig cfg;
cfg.embedding_dim = 384;
cfg.normalize_embeddings = true;
EmbeddingGenerator gen(cfg);
REQUIRE(gen.initialize());
REQUIRE(gen.getBackendName() == "Simeon");
REQUIRE(gen.getEmbeddingDimension() == 384u);
auto v = gen.generateEmbedding("hello world");
REQUIRE(v.size() == 384u);
unsetenv("YAMS_EMBED_BACKEND");
}
TEST_CASE("EmbeddingGenerator selects Simeon when configured directly", "[vector][simeon]") {
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::Simeon;
cfg.embedding_dim = 128;
cfg.normalize_embeddings = true;
EmbeddingGenerator gen(cfg);
REQUIRE(gen.initialize());
REQUIRE(gen.getBackendName() == "Simeon");
REQUIRE(gen.getEmbeddingDimension() == 128u);
}
TEST_CASE("EmbeddingGenerator moved-from observers report neutral state",
"[vector][embedding-generator][move][catch2]") {
EmbeddingGenerator source(make_cfg(128));
EmbeddingGenerator destination(std::move(source));
CHECK_FALSE(source.isInitialized());
CHECK((source.estimateTokenCount("moved-from") == 0));
CHECK((source.getLastError().empty()));
CHECK_FALSE(source.hasError());
CHECK((destination.getConfig().embedding_dim == 128));
}
TEST_CASE("EmbeddingGenerator selects Daemon via env override", "[vector][daemon]") {
setenv("YAMS_EMBED_BACKEND", "daemon", 1);
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::Simeon;
EmbeddingGenerator gen(cfg);
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
unsetenv("YAMS_EMBED_BACKEND");
}
TEST_CASE("EmbeddingGenerator maps legacy onnx env override to Daemon", "[vector][daemon]") {
setenv("YAMS_EMBED_BACKEND", "onnx", 1);
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::Simeon;
EmbeddingGenerator gen(cfg);
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
unsetenv("YAMS_EMBED_BACKEND");
}
TEST_CASE("EmbeddingGenerator maps onnxruntime env override to Daemon path", "[vector][daemon]") {
setenv("YAMS_EMBED_BACKEND", "onnxruntime", 1);
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::Simeon;
EmbeddingGenerator gen(cfg);
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
unsetenv("YAMS_EMBED_BACKEND");
}
TEST_CASE("EmbeddingGenerator maps OnnxRuntime config to Daemon path", "[vector][daemon]") {
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::OnnxRuntime;
EmbeddingGenerator gen(cfg);
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
}
TEST_CASE("EmbeddingGenerator maps legacy Hybrid config to Daemon path", "[vector][daemon]") {
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::Hybrid;
EmbeddingGenerator gen(cfg);
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
}
TEST_CASE("EmbeddingGenerator selects Daemon when configured directly", "[vector][daemon]") {
EmbeddingConfig cfg;
cfg.backend = EmbeddingConfig::Backend::Daemon;
EmbeddingGenerator gen(cfg);
REQUIRE_THAT(gen.getBackendName(), StartsWith("Daemon"));
}