* 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>
434 lines
22 KiB
C++
434 lines
22 KiB
C++
#include <catch2/catch_approx.hpp>
|
|
#include <catch2/catch_test_macros.hpp>
|
|
|
|
#include <yams/search/search_engine.h>
|
|
#include <yams/search/search_metric_keys.h>
|
|
#include <yams/search/search_tracing.h>
|
|
|
|
using yams::search::ComponentResult;
|
|
using yams::search::SearchEngineConfig;
|
|
using yams::search::SearchTraceCollector;
|
|
|
|
namespace {
|
|
|
|
ComponentResult makeComponent(std::string path, float score, ComponentResult::Source source,
|
|
size_t rank) {
|
|
ComponentResult c;
|
|
c.filePath = std::move(path);
|
|
c.documentHash = c.filePath;
|
|
c.score = score;
|
|
c.source = source;
|
|
c.rank = rank;
|
|
return c;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
TEST_CASE("buildComponentHitSummaryJson preserves ranked top hit details",
|
|
"[search][tracing][catch2]") {
|
|
std::vector<ComponentResult> componentResults;
|
|
componentResults.push_back(
|
|
makeComponent("/tmp/31715818.txt", 0.91f, ComponentResult::Source::Vector, 150));
|
|
componentResults.push_back(
|
|
makeComponent("/tmp/31715818.txt", 0.89f, ComponentResult::Source::Vector, 151));
|
|
componentResults.push_back(
|
|
makeComponent("/tmp/11111111.txt", 0.95f, ComponentResult::Source::Vector, 5));
|
|
componentResults.push_back(
|
|
makeComponent("/tmp/22222222.txt", 0.72f, ComponentResult::Source::Text, 12));
|
|
|
|
const auto summary = yams::search::buildComponentHitSummaryJson(componentResults, 3);
|
|
|
|
REQUIRE(summary.contains("vector"));
|
|
const auto& vectorSummary = summary.at("vector");
|
|
REQUIRE(vectorSummary.contains("unique_top_doc_ids"));
|
|
REQUIRE(vectorSummary.contains("top_hits"));
|
|
|
|
const auto& vectorIds = vectorSummary.at("unique_top_doc_ids");
|
|
REQUIRE(vectorIds.is_array());
|
|
REQUIRE(vectorIds.size() == 2);
|
|
CHECK(vectorIds.at(0).get<std::string>() == "11111111");
|
|
CHECK(vectorIds.at(1).get<std::string>() == "31715818");
|
|
|
|
const auto& topHits = vectorSummary.at("top_hits");
|
|
REQUIRE(topHits.is_array());
|
|
REQUIRE(topHits.size() == 2);
|
|
CHECK(topHits.at(0).at("doc_id").get<std::string>() == "11111111");
|
|
CHECK(topHits.at(0).at("rank").get<size_t>() == 6);
|
|
CHECK(topHits.at(1).at("doc_id").get<std::string>() == "31715818");
|
|
CHECK(topHits.at(1).at("rank").get<size_t>() == 151);
|
|
CHECK(topHits.at(1).at("score").get<double>() == Catch::Approx(0.91));
|
|
|
|
REQUIRE(summary.contains("text"));
|
|
const auto& textHits = summary.at("text").at("top_hits");
|
|
REQUIRE(textHits.is_array());
|
|
REQUIRE(textHits.size() == 1);
|
|
CHECK(textHits.at(0).at("doc_id").get<std::string>() == "22222222");
|
|
CHECK(textHits.at(0).at("rank").get<size_t>() == 13);
|
|
}
|
|
|
|
TEST_CASE("SearchTraceCollector records per-surface counters under stage[\"counters\"]",
|
|
"[search][tracing][catch2]") {
|
|
SearchEngineConfig config;
|
|
SearchTraceCollector collector(config);
|
|
|
|
collector.markStageConfigured("vector", true);
|
|
collector.markStageAttempted("vector");
|
|
collector.recordStageCounter("vector", "backend_raw_return", 300);
|
|
collector.recordStageCounter("vector", "post_threshold", 86);
|
|
collector.recordStageCounter("vector", "should_narrow_applied", 1);
|
|
collector.recordStageCounter("vector", "post_threshold", 92);
|
|
|
|
const auto summary = collector.buildStageSummaryJson();
|
|
REQUIRE(summary.contains("vector"));
|
|
const auto& vectorStage = summary.at("vector");
|
|
REQUIRE(vectorStage.contains("counters"));
|
|
const auto& counters = vectorStage.at("counters");
|
|
REQUIRE(counters.is_object());
|
|
CHECK(counters.at("backend_raw_return").get<std::int64_t>() == 300);
|
|
CHECK(counters.at("post_threshold").get<std::int64_t>() == 92);
|
|
CHECK(counters.at("should_narrow_applied").get<std::int64_t>() == 1);
|
|
|
|
REQUIRE(summary.contains("vector"));
|
|
CHECK(summary.at("vector").at("attempted").get<bool>() == true);
|
|
}
|
|
|
|
TEST_CASE("SearchTraceCollector counters are per-stage isolated", "[search][tracing][catch2]") {
|
|
SearchEngineConfig config;
|
|
SearchTraceCollector collector(config);
|
|
|
|
collector.recordStageCounter("vector", "shouldSkipSemantic", 0);
|
|
collector.recordStageCounter("text", "shouldSkipSemantic", 1);
|
|
|
|
const auto summary = collector.buildStageSummaryJson();
|
|
REQUIRE(summary.at("vector").at("counters").at("shouldSkipSemantic").get<std::int64_t>() == 0);
|
|
REQUIRE(summary.at("text").at("counters").at("shouldSkipSemantic").get<std::int64_t>() == 1);
|
|
}
|
|
|
|
TEST_CASE("SearchTraceCollector emits empty counters object when none recorded",
|
|
"[search][tracing][catch2]") {
|
|
SearchEngineConfig config;
|
|
SearchTraceCollector collector(config);
|
|
|
|
collector.markStageConfigured("embedding", true);
|
|
collector.markStageAttempted("embedding");
|
|
|
|
const auto summary = collector.buildStageSummaryJson();
|
|
REQUIRE(summary.at("embedding").contains("counters"));
|
|
CHECK(summary.at("embedding").at("counters").is_object());
|
|
CHECK(summary.at("embedding").at("counters").empty());
|
|
}
|
|
|
|
TEST_CASE("SearchTraceCollector only retains document identifiers for explicit traces",
|
|
"[search][tracing][catch2]") {
|
|
SearchEngineConfig config;
|
|
const std::vector<ComponentResult> results{
|
|
makeComponent("/tmp/b.txt", 0.8F, ComponentResult::Source::Vector, 0),
|
|
makeComponent("/tmp/a.txt", 0.7F, ComponentResult::Source::Vector, 1),
|
|
};
|
|
|
|
SearchTraceCollector compactCollector(config, false);
|
|
compactCollector.markStageResult("vector", results, 10, true);
|
|
const auto compact = compactCollector.buildStageSummaryJson().at("vector");
|
|
CHECK(compact.at("unique_doc_count").get<std::size_t>() == 2);
|
|
CHECK(compact.at("unique_doc_ids").empty());
|
|
|
|
SearchTraceCollector tracedCollector(config, true);
|
|
tracedCollector.markStageResult("vector", results, 10, true);
|
|
const auto traced = tracedCollector.buildStageSummaryJson().at("vector");
|
|
CHECK(traced.at("unique_doc_count").get<std::size_t>() == 2);
|
|
CHECK(traced.at("unique_doc_ids") == nlohmann::json::array({"a", "b"}));
|
|
}
|
|
|
|
TEST_CASE("SearchTraceCollector records non-document stage outcomes without fake hits",
|
|
"[search][tracing][catch2]") {
|
|
SearchEngineConfig config;
|
|
SearchTraceCollector collector(config);
|
|
|
|
collector.markValueStageResult("embedding", true, 2500);
|
|
collector.markValueStageResult("concepts", false, 1000);
|
|
|
|
const auto summary = collector.buildStageSummaryJson();
|
|
const auto& embedding = summary.at("embedding");
|
|
CHECK(embedding.at("attempted").get<bool>());
|
|
CHECK(embedding.at("contributed").get<bool>());
|
|
CHECK(embedding.at("raw_hit_count").get<std::size_t>() == 1);
|
|
CHECK(embedding.at("unique_doc_count").get<std::size_t>() == 0);
|
|
CHECK(embedding.at("score_stats_valid").get<bool>());
|
|
CHECK(embedding.at("min_score").get<double>() == Catch::Approx(1.0));
|
|
CHECK(embedding.at("max_score").get<double>() == Catch::Approx(1.0));
|
|
CHECK(embedding.at("duration_ms").get<double>() == Catch::Approx(2.5));
|
|
|
|
const auto& concepts = summary.at("concepts");
|
|
CHECK(concepts.at("attempted").get<bool>());
|
|
CHECK_FALSE(concepts.at("contributed").get<bool>());
|
|
CHECK(concepts.at("raw_hit_count").get<std::size_t>() == 0);
|
|
CHECK_FALSE(concepts.at("score_stats_valid").get<bool>());
|
|
}
|
|
|
|
TEST_CASE("recordTopologyRoutingDebug emits the legacy topology key set",
|
|
"[search][tracing][topology][catch2]") {
|
|
SearchEngineConfig config;
|
|
config.topologyRouteScoringMode = SearchEngineConfig::TopologyRouteScoringMode::SizeWeighted;
|
|
config.topologySparseDenseAlpha = 0.25f;
|
|
config.topologyMinRouteScore = 0.5f;
|
|
|
|
yams::search::TopologyRoutingSessionResult session;
|
|
session.loadAttempted = true;
|
|
session.loadSucceeded = true;
|
|
session.artifactAdmitted = true;
|
|
session.applied = true;
|
|
session.artifactsFresh = true;
|
|
session.topologyEpoch = 7;
|
|
session.certificate.constructionFingerprint = "0123456789abcdef";
|
|
session.certificate.allowedDocumentHashes = {"hash-a", "hash-c"};
|
|
session.certificate.selectedCoverIds = {"cluster-a"};
|
|
session.certificate.selectedProtectedRelationFiberIds = {"fiber-a", "fiber-b"};
|
|
session.routedClusters = 2;
|
|
session.availableRoutes = 3;
|
|
session.routedDocs = 5;
|
|
session.routesRejected = 1;
|
|
session.addedCandidates = 4;
|
|
session.duplicateCandidates = 3;
|
|
session.staleCandidates = 1;
|
|
session.routeBoundaryScoreMargin = 0.17F;
|
|
session.confidenceAbstained = true;
|
|
session.routeRepresentativeDistanceEvaluations = 7;
|
|
session.routeRepresentativeCountMax = 4;
|
|
session.routeAnnUsed = true;
|
|
session.routeAnnCandidates = 6;
|
|
session.routeAnnDistanceEvaluations = 4;
|
|
session.routeExactRepresentativeDistanceEvaluations = 3;
|
|
session.certificate.admission.coverMaterialization =
|
|
yams::search::TopologyProofObligationStatus::Violated;
|
|
session.certificate.admission.protectedFibersRepresented =
|
|
yams::search::TopologyProofObligationStatus::Satisfied;
|
|
session.certificate.admission.certificateSaturatesProtectedFibers =
|
|
yams::search::TopologyProofObligationStatus::Violated;
|
|
session.certificate.admission.selectedCoverDocuments = 5;
|
|
session.certificate.admission.materializedCoverDocuments = 4;
|
|
session.routeEvidence = {
|
|
yams::search::TopologyRouteEvidence{
|
|
.clusterId = "cluster-a",
|
|
.semanticCost = 0.1F,
|
|
.sparseCost = 0.2F,
|
|
.persistencePenalty = 0.3F,
|
|
.cohesionPenalty = 0.4F,
|
|
.sizePenalty = 0.5F,
|
|
.routeScore = 0.9F,
|
|
.scoreEligible = true,
|
|
.inSelectedPrefix = true,
|
|
},
|
|
yams::search::TopologyRouteEvidence{
|
|
.clusterId = "cluster-b",
|
|
.sparseCost = 0.8F,
|
|
.persistencePenalty = 0.7F,
|
|
.cohesionPenalty = 0.6F,
|
|
.sizePenalty = 0.5F,
|
|
.routeScore = 0.3F,
|
|
.scoreEligible = false,
|
|
.inSelectedPrefix = false,
|
|
},
|
|
};
|
|
session.addedCandidateHashes = {"hash-a", "hash-b"};
|
|
session.routedCandidateDocIds = {"doc-c", "doc-a", "doc-a"};
|
|
session.routedCandidateHashes = {"hash-c", "hash-a"};
|
|
session.graphNeighborTrace.collected = true;
|
|
session.graphNeighborTrace.edgeFetchLimit = 128;
|
|
session.graphNeighborTrace.fetchTruncatedSeedCount = 1;
|
|
session.graphNeighborTrace.seedDocumentIds = {"seed-a", "seed-b"};
|
|
session.graphNeighborTrace.seedUnresolvedCount = 1;
|
|
session.graphNeighborTrace.relationCandidateCount = 4;
|
|
session.graphNeighborTrace.relationCandidateDocumentIds = {"raw-b", "raw-a"};
|
|
session.graphNeighborTrace.relationUnresolvedCount = 2;
|
|
session.graphNeighborTrace.fetchedCandidateCount = 3;
|
|
session.graphNeighborTrace.fetchedCandidateDocumentIds = {"fetched-b", "fetched-a"};
|
|
session.graphNeighborTrace.fetchedUnresolvedCount = 1;
|
|
session.graphNeighborTrace.eligibleCandidateCount = 2;
|
|
session.graphNeighborTrace.eligibleCandidateDocumentIds = {"eligible-b", "eligible-a"};
|
|
session.graphNeighborTrace.eligibleUnresolvedCount = 0;
|
|
session.candidateStructureEvidence.emplace("hash-a",
|
|
yams::search::TopologyCandidateStructureEvidence{
|
|
.scaleAgreement = 0.8F,
|
|
.overlapSupport = 0.4F,
|
|
.persistenceSupport = 0.6F,
|
|
.cohesionSupport = 0.9F,
|
|
.bridgeSupport = 0.3F,
|
|
.densitySupport = 0.7F,
|
|
});
|
|
session.candidateStructureEvidence.emplace("hash-c",
|
|
yams::search::TopologyCandidateStructureEvidence{
|
|
.scaleAgreement = 0.2F,
|
|
.overlapSupport = 0.2F,
|
|
.persistenceSupport = 0.8F,
|
|
.cohesionSupport = 0.3F,
|
|
.bridgeSupport = 0.2F,
|
|
.densitySupport = 0.6F,
|
|
});
|
|
session.timings.totalMicros = 100;
|
|
session.timings.loadMicros = 10;
|
|
session.timings.validateMicros = 20;
|
|
session.timings.requestPrepMicros = 30;
|
|
session.timings.routeMicros = 40;
|
|
session.timings.clusterLookupMicros = 50;
|
|
session.timings.docLookupMicros = 60;
|
|
session.timings.candidateInsertMicros = 70;
|
|
|
|
yams::search::SearchResponse response;
|
|
yams::search::recordTopologyRoutingDebug(
|
|
response, config, SearchEngineConfig::TopologyRoutingMode::HybridAssist, session,
|
|
"skip-reason", 42, yams::search::TopologyRoutingDebugOptions{.includeDetailedTrace = true});
|
|
|
|
const auto& debug = response.debugStats;
|
|
CHECK(debug.at("topology_routing_mode") == "hybrid_assist");
|
|
CHECK(debug.at("topology_route_scoring_mode") == "size_weighted");
|
|
CHECK(debug.at("topology_sparse_dense_alpha") == std::to_string(0.25f));
|
|
CHECK(debug.at("topology_min_route_score") == std::to_string(0.5f));
|
|
CHECK(debug.at("topology_weak_query_enabled") == "1");
|
|
CHECK(debug.at("topology_weak_query_load_attempted") == "1");
|
|
CHECK(debug.at("topology_weak_query_load_succeeded") == "1");
|
|
CHECK(debug.at("topology_artifact_admitted") == "1");
|
|
CHECK(debug.at("topology_weak_query_applied") == "1");
|
|
CHECK(debug.at("topology_weak_query_narrow_applied") == "0");
|
|
CHECK(debug.at("topology_weak_query_skip_reason") == "skip-reason");
|
|
CHECK(debug.at("topology_weak_query_routes_rejected") == "1");
|
|
CHECK(debug.at("topology_weak_query_routed_clusters") == "2");
|
|
CHECK(debug.at("topology_route_available_count") == "3");
|
|
CHECK(debug.at("topology_route_boundary_score_margin") == std::to_string(0.17F));
|
|
CHECK(debug.at("topology_route_confidence_abstained") == "1");
|
|
CHECK(debug.at("topology_route_representative_distance_evaluations") == "7");
|
|
CHECK(debug.at("topology_route_representative_count_max") == "4");
|
|
CHECK(debug.at("topology_route_ann_used") == "1");
|
|
CHECK(debug.at("topology_route_ann_candidates") == "6");
|
|
CHECK(debug.at("topology_route_ann_distance_evaluations") == "4");
|
|
CHECK(debug.at("topology_route_exact_representative_distance_evaluations") == "3");
|
|
CHECK(debug.at("topology_route_evidence_count") == "2");
|
|
CHECK(debug.at("topology_route_narrow_proposed") == "1");
|
|
CHECK(debug.at("topology_route_action") == "augment");
|
|
CHECK(debug.at("topology_route_selected_cover_count") == "1");
|
|
CHECK(debug.at("topology_route_selected_protected_relation_fiber_count") == "2");
|
|
CHECK(debug.at("topology_route_admission_eligible") == "0");
|
|
CHECK(debug.at("topology_route_admission_denial_reason") == "cover_materialization_incomplete");
|
|
CHECK(debug.at("topology_route_protected_relation_coverage_status") == "unavailable");
|
|
CHECK(debug.at("topology_route_risk_status") == "unavailable");
|
|
CHECK(debug.at("topology_route_work_status") == "unavailable");
|
|
CHECK(debug.at("topology_route_cover_materialization_status") == "violated");
|
|
CHECK(debug.at("topology_route_protected_fibers_represented_status") == "satisfied");
|
|
CHECK(debug.at("topology_route_certificate_saturates_protected_fibers_status") == "violated");
|
|
CHECK(debug.at("topology_route_selected_cover_documents") == "5");
|
|
CHECK(debug.at("topology_route_materialized_cover_documents") == "4");
|
|
const auto evidenceRows = nlohmann::json::parse(debug.at("topology_route_evidence_rows"));
|
|
REQUIRE(evidenceRows.size() == 2);
|
|
CHECK(evidenceRows.at(0).at("cluster_id") == "cluster-a");
|
|
CHECK(evidenceRows.at(0).at("semantic_cost").get<float>() == Catch::Approx(0.1F));
|
|
CHECK(evidenceRows.at(0).at("in_selected_prefix") == true);
|
|
CHECK(evidenceRows.at(0).at("graph_cost").is_null());
|
|
CHECK(evidenceRows.at(1).at("semantic_cost").is_null());
|
|
CHECK(evidenceRows.at(1).at("score_eligible") == false);
|
|
CHECK(evidenceRows.at(1).at("in_selected_prefix") == false);
|
|
CHECK(debug.at("topology_weak_query_routed_docs") == "5");
|
|
CHECK(debug.at("topology_weak_query_added_candidates") == "4");
|
|
CHECK(debug.at("topology_weak_query_duplicate_candidates") == "3");
|
|
CHECK(debug.at("topology_weak_query_stale_candidates") == "1");
|
|
CHECK(debug.at("topology_weak_query_added_candidate_hashes") == "hash-a\thash-b");
|
|
CHECK(debug.at("topology_weak_query_allowed_candidate_hashes") == "hash-a\thash-c");
|
|
CHECK(debug.at("topology_weak_query_allowed_candidate_doc_ids") == "doc-a\tdoc-c");
|
|
CHECK(debug.at("topology_graph_trace_collected") == "1");
|
|
CHECK(debug.at("topology_graph_edge_fetch_limit") == "128");
|
|
CHECK(debug.at("topology_graph_fetch_truncated_seed_count") == "1");
|
|
CHECK(debug.at("topology_graph_seed_doc_ids") == "seed-a\tseed-b");
|
|
CHECK(debug.at("topology_graph_seed_unresolved_count") == "1");
|
|
CHECK(debug.at("topology_graph_relation_candidate_count") == "4");
|
|
CHECK(debug.at("topology_graph_relation_candidate_doc_ids") == "raw-b\traw-a");
|
|
CHECK(debug.at("topology_graph_relation_unresolved_count") == "2");
|
|
CHECK(debug.at("topology_graph_fetched_candidate_count") == "3");
|
|
CHECK(debug.at("topology_graph_fetched_candidate_doc_ids") == "fetched-b\tfetched-a");
|
|
CHECK(debug.at("topology_graph_fetched_unresolved_count") == "1");
|
|
CHECK(debug.at("topology_graph_eligible_candidate_count") == "2");
|
|
CHECK(debug.at("topology_graph_eligible_candidate_doc_ids") == "eligible-b\teligible-a");
|
|
CHECK(debug.at("topology_graph_eligible_unresolved_count") == "0");
|
|
CHECK(debug.at("topology_weak_query_total_candidates") == "42");
|
|
CHECK(debug.at("topology_structure_candidate_count") == "2");
|
|
CHECK(std::stof(debug.at("topology_structure_scale_agreement_mean")) == Catch::Approx(0.5F));
|
|
CHECK(std::stof(debug.at("topology_structure_overlap_support_mean")) == Catch::Approx(0.3F));
|
|
CHECK(std::stof(debug.at("topology_structure_persistence_support_mean")) ==
|
|
Catch::Approx(0.7F));
|
|
CHECK(std::stof(debug.at("topology_structure_cohesion_support_mean")) == Catch::Approx(0.6F));
|
|
CHECK(std::stof(debug.at("topology_structure_bridge_support_mean")) == Catch::Approx(0.25F));
|
|
CHECK(std::stof(debug.at("topology_structure_density_support_mean")) == Catch::Approx(0.65F));
|
|
CHECK(debug.at("topology_ready") == "1");
|
|
CHECK(debug.at("topology_artifacts_fresh") == "1");
|
|
CHECK(debug.at("topology_epoch") == "7");
|
|
CHECK(debug.at("topology_construction_fingerprint") == "0123456789abcdef");
|
|
|
|
const auto& timing = response.componentTimingMicros;
|
|
CHECK(timing.at("topology_weak_query") == 100);
|
|
CHECK(timing.at("topology_load") == 10);
|
|
CHECK(timing.at("topology_validate") == 20);
|
|
CHECK(timing.at("topology_request_prep") == 30);
|
|
CHECK(timing.at("topology_route") == 40);
|
|
CHECK(timing.at("topology_cluster_lookup") == 50);
|
|
CHECK(timing.at("topology_doc_lookup") == 60);
|
|
CHECK(timing.at("topology_candidate_insert") == 70);
|
|
}
|
|
|
|
TEST_CASE("recordTopologyRoutingDebug omits readiness and timing when load not attempted",
|
|
"[search][tracing][topology][catch2]") {
|
|
SearchEngineConfig config;
|
|
yams::search::TopologyRoutingSessionResult session;
|
|
|
|
yams::search::SearchResponse response;
|
|
yams::search::recordTopologyRoutingDebug(response, config,
|
|
SearchEngineConfig::TopologyRoutingMode::Disabled,
|
|
session, "disabled", 0);
|
|
|
|
const auto& debug = response.debugStats;
|
|
CHECK(debug.at("topology_weak_query_enabled") == "0");
|
|
CHECK(debug.at("topology_weak_query_load_attempted") == "0");
|
|
CHECK(debug.at("topology_artifact_admitted") == "0");
|
|
CHECK(debug.at("topology_weak_query_skip_reason") == "disabled");
|
|
CHECK_FALSE(debug.contains("topology_ready"));
|
|
CHECK_FALSE(debug.contains("topology_epoch"));
|
|
CHECK_FALSE(debug.contains("topology_weak_query_added_candidate_hashes"));
|
|
CHECK_FALSE(debug.contains("topology_weak_query_allowed_candidate_hashes"));
|
|
CHECK_FALSE(debug.contains("topology_weak_query_allowed_candidate_doc_ids"));
|
|
CHECK(response.componentTimingMicros.empty());
|
|
}
|
|
|
|
TEST_CASE("recordIndexReadinessDebug emits readiness flags for both pipelines",
|
|
"[search][tracing][catch2]") {
|
|
yams::search::IndexFreshnessSnapshot freshness;
|
|
freshness.lexicalReady = true;
|
|
freshness.vectorReady = false;
|
|
freshness.kgReady = true;
|
|
freshness.topologyReady = false;
|
|
freshness.topologyArtifactsFresh = true;
|
|
freshness.topologyEpoch = 12;
|
|
|
|
std::unordered_map<std::string, std::string> debug;
|
|
yams::search::recordIndexReadinessDebug(debug, freshness);
|
|
|
|
CHECK(debug.at("search_engine_ready") == "1");
|
|
CHECK(debug.at("vector_ready") == "0");
|
|
CHECK(debug.at("kg_ready") == "1");
|
|
CHECK(debug.at("topology_ready") == "0");
|
|
CHECK(debug.at("topology_artifacts_fresh") == "1");
|
|
CHECK(debug.at("topology_epoch") == "12");
|
|
CHECK(debug.size() == 6);
|
|
}
|
|
|
|
TEST_CASE("metric key constants are stable strings", "[search][tracing][catch2]") {
|
|
namespace metrics = yams::search::metrics;
|
|
CHECK(metrics::kSearchPipelineName == "search_pipeline_name");
|
|
CHECK(metrics::kTopologyArtifactAdmitted == "topology_artifact_admitted");
|
|
CHECK(metrics::kTopologyWeakQueryApplied == "topology_weak_query_applied");
|
|
CHECK(metrics::kTopologyWeakQueryAddedCandidates == "topology_weak_query_added_candidates");
|
|
CHECK(metrics::kTopologyWeakQueryDuplicateCandidates ==
|
|
"topology_weak_query_duplicate_candidates");
|
|
CHECK(metrics::kTopologyRouteBestScore == "topology_route_best_score");
|
|
CHECK(metrics::kTopologyRouteMeanAcceptedScore == "topology_route_mean_accepted_score");
|
|
CHECK(metrics::kTopologyRouteAcceptedCount == "topology_route_accepted_count");
|
|
CHECK(metrics::kTopologyRouteEvidenceCount == "topology_route_evidence_count");
|
|
CHECK(metrics::kTopologyRouteEvidenceRows == "topology_route_evidence_rows");
|
|
CHECK(metrics::kTopologySeedCount == "topology_seed_count");
|
|
CHECK(metrics::kTopologySeedCoverageCount == "topology_seed_coverage_count");
|
|
}
|