yams/tests/unit/cli/graph_explore_presenter_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

131 lines
5 KiB
C++

// Copyright (c) 2025 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <catch2/catch_test_macros.hpp>
#include <yams/cli/graph_explore_presenter.h>
#include <filesystem>
#include <sstream>
TEST_CASE("Graph explore presenter maps daemon response to app response", "[cli][graph]") {
yams::daemon::GraphExploreResponse in;
in.query = "processTask";
in.totalSymbolsConsidered = 2;
in.totalFilesConsidered = 1;
in.emittedChars = 32;
in.snippetRenderMicros = 123;
in.snippetsRendered = 1;
in.kgAvailable = true;
in.truncated = false;
in.warnings.push_back("fallback warning");
yams::daemon::GraphExploreSymbol symbol;
symbol.nodeKey = "function:demo::processTask@/repo/src/process.cpp";
symbol.label = "processTask";
symbol.qualifiedName = "demo::processTask";
symbol.kind = "function";
symbol.filePath = "/repo/src/process.cpp";
symbol.startLine = 10;
symbol.endLine = 12;
symbol.score = 99.0;
symbol.exactMatch = true;
in.entrySymbols.push_back(symbol);
yams::daemon::GraphExploreSnippet omittedSnippet;
omittedSnippet.filePath = symbol.filePath;
omittedSnippet.language = "cpp";
omittedSnippet.mode = "omitted";
omittedSnippet.startLine = 10;
omittedSnippet.endLine = 12;
omittedSnippet.heading = "process.cpp";
omittedSnippet.content = "";
omittedSnippet.truncated = true;
omittedSnippet.symbols.push_back(symbol);
in.files.push_back(omittedSnippet);
yams::daemon::GraphExploreSnippet fullSnippet;
fullSnippet.filePath = "/repo/src/helper.cpp";
fullSnippet.language = "cpp";
fullSnippet.mode = "full";
fullSnippet.startLine = 20;
fullSnippet.endLine = 24;
fullSnippet.heading = "helper.cpp";
fullSnippet.content = "20\tint helper() {\n21\t return 0;\n22\t}\n";
in.files.push_back(fullSnippet);
yams::daemon::GraphExploreRelation relation;
relation.relation = "calls";
relation.sourceNodeKey = symbol.nodeKey;
relation.sourceLabel = "processTask";
relation.targetNodeKey = "function:demo::helper@/repo/src/helper.cpp";
relation.targetLabel = "helper";
relation.weight = 1.0F;
relation.confidence = 1.0;
relation.provenance = "treesitter";
in.relationships.push_back(relation);
const auto out = yams::cli::mapGraphExploreResponseFromDaemon(in);
CHECK(out.query == in.query);
CHECK(out.snippetRenderMicros == 123);
CHECK(out.snippetsRendered == 1);
REQUIRE(out.entrySymbols.size() == 1);
CHECK(out.entrySymbols.front().qualifiedName == "demo::processTask");
REQUIRE(out.files.size() == 2);
CHECK(out.files.front().mode == yams::app::services::GraphContextSnippetMode::Omitted);
CHECK(out.files.back().mode == yams::app::services::GraphContextSnippetMode::Full);
REQUIRE(out.relationships.size() == 1);
REQUIRE(out.relationships.front().provenance.has_value());
CHECK(*out.relationships.front().provenance == "treesitter");
}
TEST_CASE("Graph explore presenter renders json and markdown", "[cli][graph]") {
yams::app::services::GraphExploreResponse response;
response.query = "processTask";
yams::app::services::GraphContextSymbol symbol;
const auto cwd = std::filesystem::current_path();
symbol.nodeKey = "function:demo::processTask@" +
(cwd / "src" / "cli" / "commands" / "graph_command.cpp").string();
symbol.label = "processTask";
symbol.qualifiedName = "demo::processTask";
symbol.kind = "function";
symbol.filePath = (cwd / "src" / "cli" / "commands" / "graph_command.cpp").string();
symbol.startLine = 1;
symbol.endLine = 2;
symbol.exactMatch = true;
response.entrySymbols.push_back(symbol);
yams::app::services::GraphContextSnippet snippet;
snippet.filePath = symbol.filePath;
snippet.language = "cpp";
snippet.mode = yams::app::services::GraphContextSnippetMode::Full;
snippet.startLine = 1;
snippet.endLine = 2;
snippet.heading = "graph_command.cpp";
snippet.content = "1\tint processTask() {\n2\t return 1;\n";
snippet.symbols.push_back(symbol);
response.files.push_back(snippet);
yams::app::services::GraphContextRelation relation;
relation.relation = "calls";
relation.sourceLabel = "processTask";
relation.targetLabel = "helper";
response.relationships.push_back(relation);
response.snippetRenderMicros = 123;
response.snippetsRendered = 1;
const auto json = yams::cli::makeGraphExploreJson(response);
CHECK(json["files"][0]["mode"] == "full");
CHECK(json["entrySymbols"][0]["label"] == "processTask");
CHECK(json["snippetRenderMicros"] == 123);
CHECK(json["snippetsRendered"] == 1);
std::ostringstream out;
yams::cli::renderGraphExploreMarkdown(out, response, cwd);
const auto rendered = out.str();
CHECK(rendered.find("Graph Explore") != std::string::npos);
CHECK(rendered.find("processTask --calls--> helper") != std::string::npos);
CHECK(rendered.find("src/cli/commands/graph_command.cpp") != std::string::npos);
}