yams/tests/unit/app/services/graph_query_service_catch2_test.cpp
Trevon 090a5ca69c
fix(testing): stabilize TSAN coverage, harden daemon shutdown, and speed up KG/storage paths (#53)
* perf(search, metadata): add KG write buffer, score cache, adaptive budgets

* perf(indexing): wire KGWriteBuffer into indexing service

   Converts per-document addEdgesUnique calls into batched flush via
   KGWriteBuffer.  500-doc ingest: 500 SQL round-trips → 1 batch.

   Verified: 14.2× edge insert gap (14.8K→195K edges/s) in kg_edge bench.
   Tests: 10/10 Catch2 pass (105 assertions).

* perf(storage): add fsync durability, parallel batch, read integrity

* test(storage): add stress, crash-sim, cold-cache, batch benchmarks

* fix: CI optimizations and work coordinator shutdown fix

* fix(testing): stabilize TSAN coverage gate

* fix: harden KG flush, score cache, and benches

   - keep KG buffers intact when batch commit fails
   - count flushed edges per batch instead of lifetime totals
   - guard the Simeon score cache and clear it on rebuild
   - fall back to fsync when F_FULLFSYNC fails on macOS
   - remove interactive sudo from cold-cache benchmarks
2026-06-26 17:57:02 -04:00

564 lines
20 KiB
C++

// Copyright (c) 2025 YAMS Contributors
// SPDX-License-Identifier: GPL-3.0-or-later
#include <catch2/catch_test_macros.hpp>
#include <yams/app/services/graph_query_service.hpp>
#include <yams/metadata/connection_pool.h>
#include <yams/metadata/knowledge_graph_store.h>
#include <yams/metadata/metadata_repository.h>
#include "../../../common/metadata_test_db.h"
#include <filesystem>
#include <memory>
using namespace yams::app::services;
using namespace yams::metadata;
namespace {
struct GraphQueryServiceFixture {
GraphQueryServiceFixture() {
dbPath = yams::test::migrated_metadata_db_template().clone("yams_graph_query_service_db_");
testDir = dbPath.parent_path() / dbPath.stem();
std::filesystem::create_directories(testDir);
ConnectionPoolConfig poolConfig;
poolConfig.minConnections = 1;
poolConfig.maxConnections = 2;
pool = std::make_shared<ConnectionPool>(dbPath.string(), poolConfig);
auto poolInitRes = pool->initialize();
REQUIRE((poolInitRes.has_value()));
metadataRepo = std::make_shared<MetadataRepository>(
*pool, nullptr, MetadataRepository::SchemaBootstrapMode::AssumeReady);
KnowledgeGraphStoreConfig kgConfig;
kgConfig.enable_alias_fts = true;
kgConfig.enable_wal = false;
auto kgResult = makeSqliteKnowledgeGraphStore(*pool, kgConfig);
REQUIRE((kgResult.has_value()));
kgStore = std::shared_ptr<KnowledgeGraphStore>(std::move(kgResult).value());
metadataRepo->setKnowledgeGraphStore(kgStore);
}
~GraphQueryServiceFixture() {
kgStore.reset();
metadataRepo.reset();
pool.reset();
std::error_code ec;
std::filesystem::remove_all(testDir, ec);
yams::test::remove_sqlite_artifacts(dbPath);
}
std::filesystem::path testDir;
std::filesystem::path dbPath;
std::shared_ptr<ConnectionPool> pool;
std::shared_ptr<MetadataRepository> metadataRepo;
std::shared_ptr<KnowledgeGraphStore> kgStore;
};
} // namespace
TEST_CASE("GraphQueryService: relation name filters apply", "[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode caller;
caller.nodeKey = "symbol:caller";
caller.label = "caller";
caller.type = "function";
auto callerId = fixture.kgStore->upsertNode(caller);
REQUIRE((callerId.has_value()));
KGNode callee;
callee.nodeKey = "symbol:callee";
callee.label = "callee";
callee.type = "function";
auto calleeId = fixture.kgStore->upsertNode(callee);
REQUIRE((calleeId.has_value()));
KGNode includeTarget;
includeTarget.nodeKey = "path:file:/include/header.h";
includeTarget.label = "header.h";
includeTarget.type = "file";
auto includeId = fixture.kgStore->upsertNode(includeTarget);
REQUIRE((includeId.has_value()));
KGEdge callEdge;
callEdge.srcNodeId = callerId.value();
callEdge.dstNodeId = calleeId.value();
callEdge.relation = "calls";
callEdge.weight = 1.0f;
REQUIRE((fixture.kgStore->addEdge(callEdge).has_value()));
KGEdge includeEdge;
includeEdge.srcNodeId = callerId.value();
includeEdge.dstNodeId = includeId.value();
includeEdge.relation = "includes";
includeEdge.weight = 1.0f;
REQUIRE((fixture.kgStore->addEdge(includeEdge).has_value()));
GraphQueryRequest req;
req.nodeId = callerId.value();
req.maxDepth = 1;
req.maxResults = 10;
req.relationNames = {"calls"};
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 1));
REQUIRE((resp.allConnectedNodes[0].nodeMetadata.node.nodeKey == "symbol:callee"));
}
TEST_CASE("GraphQueryService: PathVersion includes has_version edges", "[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode pathNode;
pathNode.nodeKey = "path:abc:/repo/file.cpp";
pathNode.label = "/repo/file.cpp";
pathNode.type = "path";
auto pathId = fixture.kgStore->upsertNode(pathNode);
REQUIRE((pathId.has_value()));
KGNode blobNode;
blobNode.nodeKey = "blob:deadbeef";
blobNode.label = "deadbeef";
blobNode.type = "blob";
auto blobId = fixture.kgStore->upsertNode(blobNode);
REQUIRE((blobId.has_value()));
KGEdge versionEdge;
versionEdge.srcNodeId = pathId.value();
versionEdge.dstNodeId = blobId.value();
versionEdge.relation = "has_version";
versionEdge.weight = 1.0f;
REQUIRE((fixture.kgStore->addEdge(versionEdge).has_value()));
GraphQueryRequest req;
req.nodeId = pathId.value();
req.maxDepth = 1;
req.maxResults = 10;
req.relationFilters = {GraphRelationType::PathVersion};
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 1));
REQUIRE((resp.allConnectedNodes[0].nodeMetadata.node.nodeKey == "blob:deadbeef"));
}
TEST_CASE("GraphQueryService: RenamedFrom includes renamed_to edges", "[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode oldPath;
oldPath.nodeKey = "path:snap1:/repo/old.cpp";
oldPath.label = "/repo/old.cpp";
oldPath.type = "path";
auto oldId = fixture.kgStore->upsertNode(oldPath);
REQUIRE((oldId.has_value()));
KGNode newPath;
newPath.nodeKey = "path:snap2:/repo/new.cpp";
newPath.label = "/repo/new.cpp";
newPath.type = "path";
auto newId = fixture.kgStore->upsertNode(newPath);
REQUIRE((newId.has_value()));
KGEdge renameEdge;
renameEdge.srcNodeId = oldId.value();
renameEdge.dstNodeId = newId.value();
renameEdge.relation = "renamed_to";
renameEdge.weight = 1.0f;
REQUIRE((fixture.kgStore->addEdge(renameEdge).has_value()));
GraphQueryRequest req;
req.nodeId = oldId.value();
req.maxDepth = 1;
req.maxResults = 10;
req.relationFilters = {GraphRelationType::RenamedFrom};
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 1));
REQUIRE((resp.allConnectedNodes[0].nodeMetadata.node.nodeKey == "path:snap2:/repo/new.cpp"));
}
TEST_CASE("GraphQueryService: relation names are canonicalized", "[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode caller;
caller.nodeKey = "symbol:canonical:caller";
caller.label = "caller";
caller.type = "function";
auto callerId = fixture.kgStore->upsertNode(caller);
REQUIRE((callerId.has_value()));
KGNode callee;
callee.nodeKey = "symbol:canonical:callee";
callee.label = "callee";
callee.type = "function";
auto calleeId = fixture.kgStore->upsertNode(callee);
REQUIRE((calleeId.has_value()));
KGEdge callEdge;
callEdge.srcNodeId = callerId.value();
callEdge.dstNodeId = calleeId.value();
callEdge.relation = "calls";
callEdge.weight = 1.0f;
REQUIRE((fixture.kgStore->addEdge(callEdge).has_value()));
GraphQueryRequest req;
req.nodeId = callerId.value();
req.maxDepth = 1;
req.maxResults = 10;
req.relationNames = {"CALL"};
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 1));
REQUIRE((resp.allConnectedNodes[0].nodeMetadata.node.nodeKey == "symbol:canonical:callee"));
}
TEST_CASE("GraphQueryService: SymbolReference enum includes calls", "[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode caller;
caller.nodeKey = "symbol:enum:caller";
caller.label = "caller";
caller.type = "function";
auto callerId = fixture.kgStore->upsertNode(caller);
REQUIRE((callerId.has_value()));
KGNode callee;
callee.nodeKey = "symbol:enum:callee";
callee.label = "callee";
callee.type = "function";
auto calleeId = fixture.kgStore->upsertNode(callee);
REQUIRE((calleeId.has_value()));
KGEdge callEdge;
callEdge.srcNodeId = callerId.value();
callEdge.dstNodeId = calleeId.value();
callEdge.relation = "calls";
callEdge.weight = 1.0f;
REQUIRE((fixture.kgStore->addEdge(callEdge).has_value()));
GraphQueryRequest req;
req.nodeId = callerId.value();
req.maxDepth = 1;
req.maxResults = 10;
req.relationFilters = {GraphRelationType::SymbolReference};
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 1));
REQUIRE((resp.allConnectedNodes[0].nodeMetadata.node.nodeKey == "symbol:enum:callee"));
}
TEST_CASE("GraphQueryService: BFS uses discovered parent edge for attribution",
"[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode origin;
origin.nodeKey = "symbol:path:origin";
origin.label = "origin";
origin.type = "function";
auto originId = fixture.kgStore->upsertNode(origin);
REQUIRE((originId.has_value()));
KGNode mid;
mid.nodeKey = "symbol:path:mid";
mid.label = "mid";
mid.type = "function";
auto midId = fixture.kgStore->upsertNode(mid);
REQUIRE((midId.has_value()));
KGNode target;
target.nodeKey = "symbol:path:target";
target.label = "target";
target.type = "function";
auto targetId = fixture.kgStore->upsertNode(target);
REQUIRE((targetId.has_value()));
KGEdge edge1;
edge1.srcNodeId = originId.value();
edge1.dstNodeId = midId.value();
edge1.relation = "contains";
edge1.weight = 1.0f;
REQUIRE((fixture.kgStore->addEdge(edge1).has_value()));
KGEdge edge2;
edge2.srcNodeId = midId.value();
edge2.dstNodeId = targetId.value();
edge2.relation = "calls";
edge2.weight = 0.9f;
edge2.properties = R"({"source":"unit","confidence":0.73})";
REQUIRE((fixture.kgStore->addEdge(edge2).has_value()));
GraphQueryRequest req;
req.nodeId = originId.value();
req.maxDepth = 2;
req.maxResults = 10;
req.hydrateFully = false;
req.includeEdgeProperties = true;
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 2));
const auto* targetNode = [&]() -> const GraphConnectedNode* {
for (const auto& node : resp.allConnectedNodes) {
if (node.nodeMetadata.node.nodeKey == "symbol:path:target") {
return &node;
}
}
return nullptr;
}();
REQUIRE((targetNode != nullptr));
CHECK((targetNode->distance == 2));
REQUIRE((targetNode->connectingEdges.size() == 1));
CHECK((targetNode->connectingEdges[0].srcNodeId == midId.value()));
CHECK((targetNode->connectingEdges[0].dstNodeId == targetId.value()));
CHECK((targetNode->connectingEdges[0].relation == "calls"));
REQUIRE((targetNode->connectingEdges[0].properties.has_value()));
CHECK((targetNode->connectingEdges[0].properties.value().find("\"confidence\":0.73") !=
std::string::npos));
}
TEST_CASE("GraphQueryService: multi-path chooses higher-signal parent relation",
"[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode origin{.nodeKey = "symbol:multi:origin", .label = "origin", .type = "function"};
KGNode structHop{.nodeKey = "symbol:multi:struct", .label = "structHop", .type = "file"};
KGNode semanticHop{
.nodeKey = "symbol:multi:semantic", .label = "semanticHop", .type = "function"};
KGNode target{.nodeKey = "symbol:multi:target", .label = "target", .type = "function"};
auto originId = fixture.kgStore->upsertNode(origin);
auto structHopId = fixture.kgStore->upsertNode(structHop);
auto semanticHopId = fixture.kgStore->upsertNode(semanticHop);
auto targetId = fixture.kgStore->upsertNode(target);
REQUIRE((originId.has_value()));
REQUIRE((structHopId.has_value()));
REQUIRE((semanticHopId.has_value()));
REQUIRE((targetId.has_value()));
KGEdge e1{.srcNodeId = originId.value(),
.dstNodeId = structHopId.value(),
.relation = "contains",
.weight = 1.0f};
KGEdge e2{.srcNodeId = originId.value(),
.dstNodeId = semanticHopId.value(),
.relation = "calls",
.weight = 1.0f};
KGEdge e3{.srcNodeId = structHopId.value(),
.dstNodeId = targetId.value(),
.relation = "contains",
.weight = 1.0f};
KGEdge e4{.srcNodeId = semanticHopId.value(),
.dstNodeId = targetId.value(),
.relation = "calls",
.weight = 1.0f};
REQUIRE((fixture.kgStore->addEdge(e1).has_value()));
REQUIRE((fixture.kgStore->addEdge(e2).has_value()));
REQUIRE((fixture.kgStore->addEdge(e3).has_value()));
REQUIRE((fixture.kgStore->addEdge(e4).has_value()));
GraphQueryRequest req;
req.nodeId = originId.value();
req.maxDepth = 2;
req.maxResults = 10;
req.hydrateFully = false;
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
const auto* targetNode = [&]() -> const GraphConnectedNode* {
for (const auto& node : resp.allConnectedNodes) {
if (node.nodeMetadata.node.nodeKey == "symbol:multi:target") {
return &node;
}
}
return nullptr;
}();
REQUIRE((targetNode != nullptr));
REQUIRE((targetNode->connectingEdges.size() == 1));
CHECK((targetNode->connectingEdges[0].srcNodeId == semanticHopId.value()));
CHECK((targetNode->connectingEdges[0].relation == "calls"));
}
TEST_CASE("GraphQueryService: cycles keep path attribution stable", "[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode origin{.nodeKey = "symbol:cycle:origin", .label = "origin", .type = "function"};
KGNode mid{.nodeKey = "symbol:cycle:mid", .label = "mid", .type = "function"};
KGNode target{.nodeKey = "symbol:cycle:target", .label = "target", .type = "function"};
auto originId = fixture.kgStore->upsertNode(origin);
auto midId = fixture.kgStore->upsertNode(mid);
auto targetId = fixture.kgStore->upsertNode(target);
REQUIRE((originId.has_value()));
REQUIRE((midId.has_value()));
REQUIRE((targetId.has_value()));
REQUIRE((fixture.kgStore
->addEdge(KGEdge{.srcNodeId = originId.value(),
.dstNodeId = midId.value(),
.relation = "calls",
.weight = 1.0f})
.has_value()));
REQUIRE((fixture.kgStore
->addEdge(KGEdge{.srcNodeId = midId.value(),
.dstNodeId = originId.value(),
.relation = "calls",
.weight = 1.0f})
.has_value()));
REQUIRE((fixture.kgStore
->addEdge(KGEdge{.srcNodeId = midId.value(),
.dstNodeId = targetId.value(),
.relation = "references",
.weight = 1.0f})
.has_value()));
GraphQueryRequest req;
req.nodeId = originId.value();
req.maxDepth = 2;
req.maxResults = 10;
req.hydrateFully = false;
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 2));
const auto& depthOne = resp.nodesByDistance.at(1);
REQUIRE((depthOne.size() == 1));
CHECK((depthOne.front().nodeMetadata.node.nodeKey == "symbol:cycle:mid"));
const auto& depthTwo = resp.nodesByDistance.at(2);
REQUIRE((depthTwo.size() == 1));
CHECK((depthTwo.front().nodeMetadata.node.nodeKey == "symbol:cycle:target"));
REQUIRE((depthTwo.front().connectingEdges.size() == 1));
CHECK((depthTwo.front().connectingEdges.front().srcNodeId == midId.value()));
CHECK((depthTwo.front().connectingEdges.front().relation == "references"));
}
TEST_CASE("GraphQueryService: relation filters apply before traversal expansion",
"[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode origin{.nodeKey = "symbol:filter:path:origin", .label = "origin", .type = "function"};
KGNode mid{.nodeKey = "symbol:filter:path:mid", .label = "mid", .type = "file"};
KGNode target{.nodeKey = "symbol:filter:path:target", .label = "target", .type = "function"};
auto originId = fixture.kgStore->upsertNode(origin);
auto midId = fixture.kgStore->upsertNode(mid);
auto targetId = fixture.kgStore->upsertNode(target);
REQUIRE((originId.has_value()));
REQUIRE((midId.has_value()));
REQUIRE((targetId.has_value()));
REQUIRE((fixture.kgStore
->addEdge(KGEdge{.srcNodeId = originId.value(),
.dstNodeId = midId.value(),
.relation = "contains",
.weight = 1.0f})
.has_value()));
REQUIRE((fixture.kgStore
->addEdge(KGEdge{.srcNodeId = midId.value(),
.dstNodeId = targetId.value(),
.relation = "calls",
.weight = 1.0f})
.has_value()));
GraphQueryRequest req;
req.nodeId = originId.value();
req.maxDepth = 2;
req.maxResults = 10;
req.hydrateFully = false;
req.relationNames = {"contains"};
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 1));
CHECK((resp.allConnectedNodes.front().nodeMetadata.node.nodeKey == "symbol:filter:path:mid"));
}
TEST_CASE("GraphQueryService: semantic relations rank above structural at same depth",
"[services][graph]") {
GraphQueryServiceFixture fixture;
auto service = makeGraphQueryService(fixture.kgStore, fixture.metadataRepo);
REQUIRE((service != nullptr));
KGNode origin{.nodeKey = "symbol:rank:origin", .label = "origin", .type = "function"};
KGNode semantic{.nodeKey = "symbol:rank:semantic", .label = "semantic", .type = "function"};
KGNode structural{.nodeKey = "symbol:rank:structural", .label = "structural", .type = "file"};
auto originId = fixture.kgStore->upsertNode(origin);
auto semanticId = fixture.kgStore->upsertNode(semantic);
auto structuralId = fixture.kgStore->upsertNode(structural);
REQUIRE((originId.has_value()));
REQUIRE((semanticId.has_value()));
REQUIRE((structuralId.has_value()));
REQUIRE((fixture.kgStore
->addEdge(KGEdge{.srcNodeId = originId.value(),
.dstNodeId = structuralId.value(),
.relation = "contains",
.weight = 1.0f})
.has_value()));
REQUIRE((fixture.kgStore
->addEdge(KGEdge{.srcNodeId = originId.value(),
.dstNodeId = semanticId.value(),
.relation = "calls",
.weight = 1.0f})
.has_value()));
GraphQueryRequest req;
req.nodeId = originId.value();
req.maxDepth = 1;
req.maxResults = 10;
req.hydrateFully = false;
auto result = service->query(req);
REQUIRE((result.has_value()));
const auto& resp = result.value();
REQUIRE((resp.allConnectedNodes.size() == 2));
CHECK((resp.allConnectedNodes.front().nodeMetadata.node.nodeKey == "symbol:rank:semantic"));
CHECK((resp.allConnectedNodes.back().nodeMetadata.node.nodeKey == "symbol:rank:structural"));
}