yams/tests/unit/cli/graph_query_presenter_catch2_test.cpp
Trevon 44b2f4f046
fix(graph): document lookup and --scope-cwd (#280) (#282)
* test(graph): cover document lookup and --scope-cwd listing (#280)

Red tests for the two graph CLI bugs in #280:
- GetResponse graph fields and file name must survive the proto wire.
- DocumentService graph lookup by hash and by name must return the
  same documents as traversing the doc:<hash> node.
- The document graph JSON must name the document when only name is set.
- KG path-range scoping must match document nodes by label and
  snapshot path nodes by $.path.
- listByType --scope-cwd must keep document and path nodes under the
  client cwd (including via a symlinked cwd) outside src/ and include/.

The existing code-roots dispatcher case now places its out-of-scope
file outside the scope root, since the scope is the whole cwd.

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

* fix(graph): fill document lookup and scope --scope-cwd to the cwd (#280)

yams graph <hash> and --name returned graphEnabled=false with no
related documents while --node-key doc:<hash> listed neighbours:
- The GetResponse proto binding dropped graphEnabled, related and
  fileName, so socket clients never saw graph data.
- DocumentService started traversal at blob:<hash>, which has no
  outgoing edges, and filtered out semantic_neighbor. Start at the
  doc:<hash> node and allow semantic_neighbor.
- GraphQueryService did not hydrate documentHash for doc: nodes, so
  reached documents were skipped.
- The presenter read only fileName for "document"; fall back to name.

--list-type <t> --scope-cwd returned nothing:
- The daemon scoped to <cwd>/src and <cwd>/include only. Scope to the
  whole cwd, lexical and symlink-resolved.
- The KG range predicate ignored document labels and $.path on
  snapshot path nodes. Match both.

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

* docs(skills): correct graph document lookup examples

--format dot is only rendered for node traversal, so show it with
--node-key doc:<hash>. Describe --scope-cwd as cwd-scoped.

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

---------

Signed-off-by: trvon <me@trevon.dev>
2026-10-09 00:01:24 -06:00

114 lines
4.6 KiB
C++

// Copyright (c) 2025 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <nlohmann/json.hpp>
#include <catch2/catch_test_macros.hpp>
#include <yams/cli/graph_query_presenter.h>
#include <sstream>
TEST_CASE("Graph query presenter renders graph responses across formats", "[cli][graph]") {
yams::daemon::GraphQueryResponse response;
response.kgAvailable = true;
response.originNode.nodeId = 1;
response.originNode.nodeKey = "function:demo::caller@/repo/src/caller.cpp";
response.originNode.label = "/repo/src/caller.cpp";
response.originNode.type = "function";
response.totalNodesFound = 2;
response.totalEdgesTraversed = 1;
yams::daemon::GraphNode node;
node.nodeId = 2;
node.nodeKey = "function:demo::callee@/repo/src/callee.cpp";
node.label = "callee";
node.type = "function";
node.distance = 1;
node.documentHash = "abcdef1234567890";
node.documentPath = "/repo/src/callee.cpp";
node.properties = R"({"language":"cpp"})";
response.connectedNodes.push_back(node);
yams::daemon::GraphEdge edge;
edge.edgeId = 42;
edge.srcNodeId = 1;
edge.dstNodeId = 2;
edge.relation = "calls";
edge.weight = 1.0f;
response.edges.push_back(edge);
std::ostringstream jsonOut;
auto jsonResult = yams::cli::renderGraphQueryResponse(
jsonOut, response, yams::cli::GraphQueryRenderOptions{.jsonOutput = true, .cwd = "/repo"});
REQUIRE(jsonResult.has_value());
CHECK(jsonOut.str().find("\"connectedNodes\"") != std::string::npos);
CHECK(jsonOut.str().find("\"via\": \"calls(1)\"") != std::string::npos);
std::ostringstream tableOut;
auto tableResult = yams::cli::renderGraphQueryResponse(
tableOut, response, yams::cli::GraphQueryRenderOptions{.verbose = true, .cwd = "/repo"});
REQUIRE(tableResult.has_value());
CHECK(tableOut.str().find("Knowledge Graph Query") != std::string::npos);
CHECK(tableOut.str().find("src/callee.cpp") != std::string::npos);
CHECK(tableOut.str().find("Node Properties") != std::string::npos);
std::ostringstream dotOut;
auto dotResult = yams::cli::renderGraphQueryResponse(
dotOut, response,
yams::cli::GraphQueryRenderOptions{.outputFormat = "dot", .cwd = "/repo"});
REQUIRE(dotResult.has_value());
CHECK(dotOut.str().find("digraph G") != std::string::npos);
CHECK(dotOut.str().find("calls") != std::string::npos);
}
TEST_CASE("Graph query presenter renders document graph fallbacks and related docs",
"[cli][graph]") {
yams::daemon::GetResponse disabled;
disabled.fileName = "src/new_file.cpp";
disabled.path = "/repo/src/new_file.cpp";
disabled.hash = "1234567890abcdef";
disabled.graphEnabled = false;
std::ostringstream disabledOut;
auto disabledResult = yams::cli::renderDocumentGraphResponse(
disabledOut, disabled, yams::cli::DocumentGraphRenderOptions{.depth = 2, .cwd = "/repo"});
REQUIRE(disabledResult.has_value());
CHECK(disabledOut.str().find("yams add \"src/new_file.cpp\" --sync") != std::string::npos);
CHECK(disabledOut.str().find("yams graph --search \"*new_file*\"") != std::string::npos);
yams::daemon::GetResponse related;
related.fileName = "src/main.cpp";
related.path = "/repo/src/main.cpp";
related.hash = "fedcba0987654321";
related.graphEnabled = true;
yams::daemon::RelatedDocumentEntry rel;
rel.hash = "abcd1234efgh5678";
rel.name = "src/helper.cpp";
rel.relationship = "calls";
rel.distance = 1;
related.related.push_back(rel);
std::ostringstream relatedOut;
auto relatedResult = yams::cli::renderDocumentGraphResponse(
relatedOut, related, yams::cli::DocumentGraphRenderOptions{.depth = 2, .cwd = "/repo"});
REQUIRE(relatedResult.has_value());
CHECK(relatedOut.str().find("Related Documents (depth 2)") != std::string::npos);
CHECK(relatedOut.str().find("src/helper.cpp") != std::string::npos);
}
TEST_CASE("Document graph JSON names the document when only name is set", "[cli][graph]") {
// Issue #280: the daemon fills GetResponse.name, not fileName, so "document" came back "".
yams::daemon::GetResponse resp;
resp.name = "bm25.hpp";
resp.path = "/corpus/simeon/include/simeon/bm25.hpp";
resp.hash = "12be4dc5f2d4a975";
resp.graphEnabled = true;
std::ostringstream out;
auto result = yams::cli::renderDocumentGraphResponse(
out, resp, yams::cli::DocumentGraphRenderOptions{.jsonOutput = true, .cwd = "/corpus"});
REQUIRE(result.has_value());
const auto payload = nlohmann::json::parse(out.str());
CHECK(payload.at("document") == "bm25.hpp");
CHECK(payload.at("graphEnabled") == true);
}