yams/tests/integration/daemon/entity_graph_service_integration_catch2_test.cpp

1865 lines
64 KiB
C++

// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright 2025 YAMS Contributors
//
// Integration test: EntityGraphService → KnowledgeGraphStore pipeline
// Tests end-to-end flow: symbol extraction → KG node/edge creation → KG queries
// Part of yams-h54: Graph Navigation & Symbol Coverage Expansion
#include <nlohmann/json.hpp>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <set>
#include <thread>
#include <spdlog/spdlog.h>
#include <catch2/catch_test_macros.hpp>
#include "test_daemon_harness.h"
#include <yams/daemon/components/EntityGraphService.h>
#include <yams/daemon/components/GraphComponent.h>
#include <yams/daemon/components/ServiceManager.h>
#include <yams/daemon/daemon.h>
#include <yams/metadata/document_metadata.h>
#include <yams/metadata/knowledge_graph_store.h>
using namespace yams::metadata;
// Skip tests on Windows where daemon IPC shutdown is unstable (named pipe cleanup hangs)
// Linux Docker container runs these tests successfully
// TODO(yams-daemon): Fix Windows daemon shutdown race conditions
#ifdef _WIN32
#define SKIP_ON_WINDOWS_DAEMON_SHUTDOWN() \
SKIP("Windows daemon shutdown unstable - run in Docker container")
#else
#define SKIP_ON_WINDOWS_DAEMON_SHUTDOWN() ((void)0)
#endif
// ============================================================================
// Test Fixture: EntityGraphService Integration (using DaemonHarness)
// ============================================================================
class EntityGraphIntegrationFixture {
public:
EntityGraphIntegrationFixture() {
if (!harness_.start(std::chrono::seconds(30))) {
throw std::runtime_error("DaemonHarness failed to start");
}
daemon_ = harness_.daemon();
if (!daemon_) {
throw std::runtime_error("Daemon not available after harness start");
}
serviceManager_ = daemon_->getServiceManager();
if (!serviceManager_) {
throw std::runtime_error("ServiceManager not available");
}
testDir_ = harness_.dataDir();
spdlog::info("EntityGraphIntegrationFixture: Daemon started, dataDir={}",
testDir_.string());
}
~EntityGraphIntegrationFixture() {
// DaemonHarness RAII handles cleanup
spdlog::debug("EntityGraphIntegrationFixture: Cleanup via DaemonHarness");
}
// Store content and return hash
std::string storeContent(const std::string& filename, const std::string& content) {
auto contentStore = serviceManager_->getContentStore();
if (!contentStore) {
throw std::runtime_error("ContentStore not available");
}
auto meta = serviceManager_->getMetadataRepo();
if (!meta) {
throw std::runtime_error("MetadataRepository not available");
}
std::vector<std::byte> bytes;
bytes.reserve(content.size());
for (char c : content) {
bytes.push_back(static_cast<std::byte>(c));
}
auto storeResult = contentStore->storeBytes(bytes);
if (!storeResult) {
throw std::runtime_error("Failed to store content: " + storeResult.error().message);
}
std::string hash = storeResult.value().contentHash;
DocumentInfo doc;
doc.fileName = filename;
doc.filePath = (testDir_ / filename).string();
doc.sha256Hash = hash;
doc.fileSize = content.size();
doc.mimeType = "text/x-c++";
doc.fileExtension = ".cpp";
doc.modifiedTime =
std::chrono::time_point_cast<std::chrono::seconds>(std::chrono::system_clock::now());
auto insertResult = meta->insertDocument(doc);
if (!insertResult) {
throw std::runtime_error("Failed to insert document: " + insertResult.error().message);
}
std::ofstream out(doc.filePath);
out << content;
out.close();
return hash;
}
std::string buildFilePath(const std::string& filename) const {
return (testDir_ / filename).string();
}
// Get KG store for direct queries
std::shared_ptr<KnowledgeGraphStore> getKgStore() { return serviceManager_->getKgStore(); }
// Get symbol extractors availability
bool hasSymbolExtractors() {
const auto& extractors = serviceManager_->getSymbolExtractors();
return !extractors.empty();
}
// Check if C++ language is supported
bool supportsCpp() {
const auto& extractors = serviceManager_->getSymbolExtractors();
for (const auto& ex : extractors) {
if (ex && ex->supportsLanguage("cpp")) {
return true;
}
}
return false;
}
// Submit extraction job and wait for completion
bool submitAndWaitForExtraction(const std::string& hash, const std::string& filePath,
const std::string& /*content*/, const std::string& /*language*/,
int timeoutMs = 5000) {
auto graphComponent = serviceManager_->getGraphComponent();
if (!graphComponent) {
spdlog::warn("GraphComponent not available");
return false;
}
yams::daemon::GraphComponent::DocumentGraphContext ctx;
ctx.documentHash = hash;
ctx.filePath = filePath;
ctx.documentDbId = 0;
auto result = graphComponent->onDocumentIngested(ctx);
if (!result) {
spdlog::warn("onDocumentIngested failed: {}", result.error().message);
return false;
}
// Wait for async processing
std::this_thread::sleep_for(std::chrono::milliseconds(timeoutMs));
return true;
}
yams::test::DaemonHarness harness_;
yams::daemon::YamsDaemon* daemon_{nullptr};
yams::daemon::ServiceManager* serviceManager_{nullptr};
std::filesystem::path testDir_;
};
// ============================================================================
// Test Cases: EntityGraphService → KG Pipeline Validation
// ============================================================================
TEST_CASE("EntityGraphService: KG store is available", "[integration][daemon][kg][pbi-009]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
SECTION("KnowledgeGraphStore is initialized") {
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto healthResult = kg->healthCheck();
REQUIRE(healthResult.has_value());
spdlog::info("KG store health check passed");
}
}
TEST_CASE("EntityGraphService: Symbol extractors are loaded",
"[integration][daemon][kg][pbi-009]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
SECTION("At least one symbol extractor available") {
bool hasExtractors = fixture.hasSymbolExtractors();
if (!hasExtractors) {
SKIP("No symbol extractors loaded - grammars may not be installed");
}
REQUIRE(hasExtractors);
spdlog::info("Symbol extractors are loaded");
}
SECTION("C++ language is supported") {
bool supportsCpp = fixture.supportsCpp();
if (!supportsCpp) {
SKIP("C++ grammar not available - run 'yams init' to download grammars");
}
REQUIRE(supportsCpp);
spdlog::info("C++ language supported by symbol extractor");
}
}
TEST_CASE("EntityGraphService: Symbol extraction creates KG nodes",
"[integration][daemon][kg][pbi-009]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Extract C++ function creates function node") {
std::string cppCode = R"(
int add(int a, int b) {
return a + b;
}
TEST_CASE("EntityGraphService: Snapshot version nodes and pruning",
"[integration][daemon][kg][pbi-009][versioning]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
const std::string filename = "versioned_symbols.cpp";
const std::string filePath = fixture.buildFilePath(filename);
std::string cppCodeV1 = "int foo() { return 1; }";
std::string cppCodeV2 = "int foo() { return 2; }";
auto hashV1 = fixture.storeContent(filename, cppCodeV1);
REQUIRE(fixture.submitAndWaitForExtraction(hashV1, filePath, cppCodeV1, "cpp", 3000));
// Ensure the second version has a later timestamp
std::this_thread::sleep_for(std::chrono::milliseconds(50));
auto hashV2 = fixture.storeContent(filename, cppCodeV2);
REQUIRE(fixture.submitAndWaitForExtraction(hashV2, filePath, cppCodeV2, "cpp", 3000));
// Find canonical key for foo in this file
auto funcNodesResult = kg->findNodesByType("function", 200, 0);
REQUIRE(funcNodesResult.has_value());
std::optional<std::string> canonicalKey;
for (const auto& node : funcNodesResult.value()) {
if (node.nodeKey.find(std::string("@") + filePath) == std::string::npos) {
continue;
}
if (node.label.has_value() && node.label.value() == "foo") {
canonicalKey = node.nodeKey;
break;
}
if (node.nodeKey.find("foo") != std::string::npos) {
canonicalKey = node.nodeKey;
break;
}
}
REQUIRE(canonicalKey.has_value());
const std::string versionKeyV1 = canonicalKey.value() + "@snap:" + hashV1;
const std::string versionKeyV2 = canonicalKey.value() + "@snap:" + hashV2;
auto v1Node = kg->getNodeByKey(versionKeyV1);
REQUIRE(v1Node.has_value());
CHECK(v1Node.value().has_value());
auto v2Node = kg->getNodeByKey(versionKeyV2);
REQUIRE(v2Node.has_value());
CHECK(v2Node.value().has_value());
// Prune to keep only the latest version for this canonical node
yams::metadata::GraphVersionPruneConfig pruneCfg;
pruneCfg.keepLatestPerCanonical = 1;
auto pruneResult = kg->pruneVersionNodes(pruneCfg);
REQUIRE(pruneResult.has_value());
auto v1After = kg->getNodeByKey(versionKeyV1);
REQUIRE(v1After.has_value());
CHECK(!v1After.value().has_value());
auto v2After = kg->getNodeByKey(versionKeyV2);
REQUIRE(v2After.has_value());
CHECK(v2After.value().has_value());
// Ensure observed_as edges are now at most 1 for this canonical node
auto canonicalNodeRes = kg->getNodeByKey(canonicalKey.value());
REQUIRE(canonicalNodeRes.has_value());
REQUIRE(canonicalNodeRes.value().has_value());
auto edgesRes = kg->getEdgesFrom(canonicalNodeRes.value()->id, "observed_as", 10, 0);
REQUIRE(edgesRes.has_value());
CHECK(edgesRes.value().size() <= 1);
}
int multiply(int x, int y) {
return x * y;
}
)";
std::string filename = "math_functions.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
spdlog::info("Stored C++ file: hash={}", hash.substr(0, 12));
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Query for function nodes
auto nodesResult = kg->findNodesByType("function", 100, 0);
REQUIRE(nodesResult.has_value());
auto& nodes = nodesResult.value();
spdlog::info("Found {} function nodes in KG", nodes.size());
// Should have at least 2 functions (add, multiply)
REQUIRE(nodes.size() >= 2);
// Verify node properties
bool foundAdd = false;
bool foundMultiply = false;
for (const auto& node : nodes) {
spdlog::info("Function node: key={}, label={}", node.nodeKey,
node.label.value_or("<no label>"));
if (node.label == "add" || node.nodeKey.find("add") != std::string::npos) {
foundAdd = true;
}
if (node.label == "multiply" || node.nodeKey.find("multiply") != std::string::npos) {
foundMultiply = true;
}
}
REQUIRE(foundAdd);
REQUIRE(foundMultiply);
spdlog::info("Function nodes created correctly in KG");
}
SECTION("Extract C++ class creates class node") {
std::string cppCode = R"(
class Calculator {
public:
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
private:
int result_;
};
)";
std::string filename = "calculator.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
// Query for class nodes
auto classNodesResult = kg->findNodesByType("class", 100, 0);
REQUIRE(classNodesResult.has_value());
auto& classNodes = classNodesResult.value();
spdlog::info("Found {} class nodes in KG", classNodes.size());
bool foundCalculator = false;
for (const auto& node : classNodes) {
spdlog::info("Class node: key={}, label={}", node.nodeKey,
node.label.value_or("<no label>"));
if (node.label == "Calculator" ||
node.nodeKey.find("Calculator") != std::string::npos) {
foundCalculator = true;
}
}
REQUIRE(foundCalculator);
// Also check for method nodes
auto methodNodesResult = kg->findNodesByType("method", 100, 0);
if (methodNodesResult.has_value()) {
spdlog::info("Found {} method nodes in KG", methodNodesResult.value().size());
}
spdlog::info("Class and method nodes created correctly in KG");
}
}
TEST_CASE("EntityGraphService: Symbol extraction creates KG edges",
"[integration][daemon][kg][pbi-009]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Symbols have defined_in edges to document") {
std::string cppCode = R"(
void processData() {
// Process some data
}
)";
std::string filename = "process.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
// Find the function node first
auto funcNodes = kg->findNodesByType("function", 100, 0);
REQUIRE(funcNodes.has_value());
bool foundDefinedInEdge = false;
bool foundLocatedInEdge = false;
bool foundContainsEdge = false;
for (const auto& funcNode : funcNodes.value()) {
// Check edges from this function node
auto edgesResult = kg->getEdgesFrom(funcNode.id, std::nullopt, 100, 0);
if (edgesResult.has_value()) {
for (const auto& edge : edgesResult.value()) {
if (edge.relation == "defined_in") {
foundDefinedInEdge = true;
spdlog::info("Found 'defined_in' edge from function {}", funcNode.nodeKey);
}
if (edge.relation == "located_in") {
foundLocatedInEdge = true;
spdlog::info("Found 'located_in' edge from function {}", funcNode.nodeKey);
}
}
}
}
// At least one type of location edge should exist
CHECK((foundDefinedInEdge || foundLocatedInEdge));
auto fileNodeRes = kg->getNodeByKey("path:file:" + filePath);
REQUIRE(fileNodeRes.has_value());
REQUIRE(fileNodeRes.value().has_value());
auto fileEdgesRes = kg->getEdgesFrom(fileNodeRes.value()->id, std::nullopt, 100, 0);
if (fileEdgesRes.has_value()) {
for (const auto& edge : fileEdgesRes.value()) {
if (edge.relation == "contains") {
foundContainsEdge = true;
break;
}
}
}
CHECK(foundContainsEdge);
spdlog::info("Symbol edges created correctly in KG");
}
}
TEST_CASE("EntityGraphService: KG aliases for symbol lookup",
"[integration][daemon][kg][pbi-009]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Symbols have searchable aliases") {
std::string cppCode = R"(
namespace utils {
int helperFunction(int x) {
return x * 2;
}
}
)";
std::string filename = "utils.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
// Try to resolve alias by function name
auto aliasResult = kg->resolveAliasExact("helperFunction", 10);
if (aliasResult.has_value() && !aliasResult.value().empty()) {
spdlog::info("Found {} alias matches for 'helperFunction'", aliasResult.value().size());
REQUIRE(!aliasResult.value().empty());
} else {
spdlog::warn("No alias found for 'helperFunction' - alias may not be created yet");
}
}
}
TEST_CASE("EntityGraphService: Multiple files create connected graph",
"[integration][daemon][kg][pbi-009]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Multiple source files populate KG") {
std::string file1Code = R"(
class DataStore {
public:
void save(int id);
int load(int id);
};
)";
std::string file2Code = R"(
class DataProcessor {
public:
void process();
};
)";
auto hash1 = fixture.storeContent("data_store.cpp", file1Code);
auto hash2 = fixture.storeContent("data_processor.cpp", file2Code);
std::string path1 = (fixture.testDir_ / "data_store.cpp").string();
std::string path2 = (fixture.testDir_ / "data_processor.cpp").string();
REQUIRE(fixture.submitAndWaitForExtraction(hash1, path1, file1Code, "cpp", 2000));
REQUIRE(fixture.submitAndWaitForExtraction(hash2, path2, file2Code, "cpp", 2000));
auto kg = fixture.getKgStore();
// Get all class nodes
auto classNodesResult = kg->findNodesByType("class", 100, 0);
REQUIRE(classNodesResult.has_value());
auto& classNodes = classNodesResult.value();
spdlog::info("Total class nodes from multiple files: {}", classNodes.size());
// Should have at least 2 classes (DataStore, DataProcessor)
REQUIRE(classNodes.size() >= 2);
bool foundDataStore = false;
bool foundDataProcessor = false;
for (const auto& node : classNodes) {
if (node.label == "DataStore" || node.nodeKey.find("DataStore") != std::string::npos) {
foundDataStore = true;
}
if (node.label == "DataProcessor" ||
node.nodeKey.find("DataProcessor") != std::string::npos) {
foundDataProcessor = true;
}
}
REQUIRE(foundDataStore);
REQUIRE(foundDataProcessor);
// Get file nodes
auto fileNodesResult = kg->findNodesByType("file", 100, 0);
if (fileNodesResult.has_value()) {
spdlog::info("Total file nodes: {}", fileNodesResult.value().size());
REQUIRE(fileNodesResult.value().size() >= 2);
}
spdlog::info("Multiple files correctly populate KG");
}
}
TEST_CASE("EntityGraphService: Graph traversal from symbol", "[integration][daemon][kg][pbi-009]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Can traverse from function to containing file") {
std::string cppCode = R"(
void targetFunction() {
// This is the target
}
)";
std::string filename = "target.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
REQUIRE(fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000));
auto kg = fixture.getKgStore();
// Get function nodes
auto funcNodesResult = kg->findNodesByType("function", 100, 0);
REQUIRE(funcNodesResult.has_value());
auto& funcNodes = funcNodesResult.value();
std::optional<int64_t> targetNodeId;
for (const auto& node : funcNodes) {
if (node.label == "targetFunction" ||
node.nodeKey.find("targetFunction") != std::string::npos) {
targetNodeId = node.id;
spdlog::info("Found targetFunction with id={}", node.id);
break;
}
}
REQUIRE(targetNodeId.has_value());
// Get outgoing edges from this node
auto edgesResult = kg->getEdgesFrom(*targetNodeId, std::nullopt, 100, 0);
if (edgesResult.has_value()) {
auto& edges = edgesResult.value();
spdlog::info("targetFunction has {} outgoing edges", edges.size());
// Should have edges to file/document
bool hasLocationEdge = false;
for (const auto& edge : edges) {
spdlog::info("Edge: relation={}, dstNodeId={}", edge.relation, edge.dstNodeId);
if (edge.relation == "located_in" || edge.relation == "defined_in") {
hasLocationEdge = true;
}
}
REQUIRE(hasLocationEdge);
}
spdlog::info("Graph traversal from symbol works correctly");
}
}
// ============================================================================
// Multi-Language Entity Extraction Tests (yams-h54.9)
// Verifies all supported tree-sitter languages trigger KG entity extraction
// ============================================================================
struct LanguageTestCase {
std::string language;
std::string extension;
std::string code;
std::string expectedNodeType;
std::string expectedSymbolName;
};
// Sample code snippets for each supported language
static const std::vector<LanguageTestCase> LANGUAGE_TEST_CASES = {
{"c", ".c", "int add(int x, int y) { return x + y; }", "function", "add"},
{"cpp", ".cpp", "class Calculator { public: int compute() { return 0; } };", "class",
"Calculator"},
{"python", ".py", "def greet(name):\n return f'Hello {name}'", "function", "greet"},
{"rust", ".rs", "fn hello(x: i32) -> i32 { x + 1 }", "function", "hello"},
{"go", ".go", "package main\nfunc Hello(x int) int { return x + 1 }", "function", "Hello"},
{"javascript", ".js", "function processData(input) { return input.trim(); }", "function",
"processData"},
{"typescript", ".ts", "function calculate(x: number): number { return x * 2; }", "function",
"calculate"},
{"java", ".java", "public class Handler { public void process() {} }", "class", "Handler"},
{"csharp", ".cs", "public class Service { public void Execute() {} }", "class", "Service"},
{"php", ".php", "<?php function validate($input) { return true; }", "function", "validate"},
{"kotlin", ".kt", "class Worker { fun execute() {} }", "class", "Worker"},
};
TEST_CASE("EntityGraphService: Multi-language symbol extraction",
"[integration][daemon][kg][pbi-009][multilang]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.hasSymbolExtractors()) {
SKIP("No symbol extractors loaded");
}
for (const auto& testCase : LANGUAGE_TEST_CASES) {
DYNAMIC_SECTION("Language: " << testCase.language) {
// Check if this language is supported
const auto& extractors = fixture.serviceManager_->getSymbolExtractors();
bool languageSupported = false;
for (const auto& ex : extractors) {
if (ex && ex->supportsLanguage(testCase.language)) {
languageSupported = true;
break;
}
}
if (!languageSupported) {
spdlog::info("Skipping {} - grammar not available", testCase.language);
SKIP("Grammar not available for " + testCase.language);
}
// Create test file
std::string filename = "test_" + testCase.language + testCase.extension;
auto hash = fixture.storeContent(filename, testCase.code);
std::string filePath = (fixture.testDir_ / filename).string();
spdlog::info("Testing {}: file={}, hash={}", testCase.language, filename,
hash.substr(0, 12));
// Submit for extraction
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, testCase.code,
testCase.language, 3000);
REQUIRE(extracted);
// Query KG for nodes of expected type
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto nodesResult = kg->findNodesByType(testCase.expectedNodeType, 100, 0);
REQUIRE(nodesResult.has_value());
auto& nodes = nodesResult.value();
spdlog::info("{}: Found {} {} nodes", testCase.language, nodes.size(),
testCase.expectedNodeType);
// Check for expected symbol
bool foundExpectedSymbol = false;
for (const auto& node : nodes) {
std::string label = node.label.value_or("");
if (label == testCase.expectedSymbolName ||
node.nodeKey.find(testCase.expectedSymbolName) != std::string::npos) {
foundExpectedSymbol = true;
spdlog::info("{}: Found expected symbol '{}' (nodeKey={})", testCase.language,
testCase.expectedSymbolName, node.nodeKey);
break;
}
}
CHECK(foundExpectedSymbol);
if (foundExpectedSymbol) {
spdlog::info("{}: Entity extraction successful", testCase.language);
} else {
spdlog::warn("{}: Expected symbol '{}' not found in KG", testCase.language,
testCase.expectedSymbolName);
}
}
}
}
TEST_CASE("EntityGraphService: Verify language extension mapping",
"[integration][daemon][kg][pbi-009][multilang]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.hasSymbolExtractors()) {
SKIP("No symbol extractors loaded");
}
SECTION("File extension triggers correct language detection") {
// Test that .py extension triggers Python extraction
std::string pyCode = "def test_function(): pass";
std::string filename = "extension_test.py";
auto hash = fixture.storeContent(filename, pyCode);
std::string filePath = (fixture.testDir_ / filename).string();
// Check if Python is supported
const auto& extractors = fixture.serviceManager_->getSymbolExtractors();
bool pythonSupported = false;
for (const auto& ex : extractors) {
if (ex && ex->supportsLanguage("python")) {
pythonSupported = true;
break;
}
}
if (!pythonSupported) {
SKIP("Python grammar not available");
}
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, pyCode, "python", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
auto nodesResult = kg->findNodesByType("function", 100, 0);
if (nodesResult.has_value()) {
bool foundTestFunction = false;
for (const auto& node : nodesResult.value()) {
if (node.label == "test_function" ||
node.nodeKey.find("test_function") != std::string::npos) {
foundTestFunction = true;
break;
}
}
CHECK(foundTestFunction);
}
}
}
TEST_CASE("EntityGraphService: All languages create file nodes",
"[integration][daemon][kg][pbi-009][multilang]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.hasSymbolExtractors()) {
SKIP("No symbol extractors loaded");
}
// Test a subset of languages to verify file node creation
std::vector<std::pair<std::string, std::string>> languageSamples = {
{"cpp", "void test() {}"},
{"python", "def test(): pass"},
{"javascript", "function test() {}"},
};
int filesProcessed = 0;
for (const auto& [lang, code] : languageSamples) {
// Check if language is supported
const auto& extractors = fixture.serviceManager_->getSymbolExtractors();
bool supported = false;
for (const auto& ex : extractors) {
if (ex && ex->supportsLanguage(lang)) {
supported = true;
break;
}
}
if (!supported) {
spdlog::info("Skipping {} - grammar not available", lang);
continue;
}
std::string ext = (lang == "cpp") ? ".cpp" : (lang == "python") ? ".py" : ".js";
std::string filename = "filenode_test_" + lang + ext;
auto hash = fixture.storeContent(filename, code);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, code, lang, 2000);
if (extracted) {
filesProcessed++;
}
}
// Verify file nodes were created
auto kg = fixture.getKgStore();
auto fileNodesResult = kg->findNodesByType("file", 100, 0);
if (fileNodesResult.has_value()) {
spdlog::info("Total file nodes created: {}", fileNodesResult.value().size());
CHECK(fileNodesResult.value().size() >= static_cast<size_t>(filesProcessed));
}
REQUIRE(filesProcessed > 0);
spdlog::info("Processed {} language files, all created file nodes", filesProcessed);
}
// ============================================================================
// Expanded Entity Symbol Extraction Tests (yams-h54.10)
// Tests nested symbols, relationships, namespaces, templates, and edge cases
// ============================================================================
TEST_CASE("EntityGraphService: Nested class extraction",
"[integration][daemon][kg][pbi-009][nested]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Inner class within outer class creates separate nodes") {
std::string cppCode = R"(
class OuterContainer {
public:
class InnerWorker {
public:
void execute() {}
};
struct NestedData {
int value;
};
void process() {}
};
)";
std::string filename = "nested_classes.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 4000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Query for class nodes
auto classNodesResult = kg->findNodesByType("class", 100, 0);
REQUIRE(classNodesResult.has_value());
auto& classNodes = classNodesResult.value();
spdlog::info("Found {} class nodes for nested test", classNodes.size());
bool foundOuter = false;
bool foundInner = false;
for (const auto& node : classNodes) {
std::string label = node.label.value_or("");
std::string key = node.nodeKey;
spdlog::info("Nested test class: label={}, key={}", label, key);
if (label.find("OuterContainer") != std::string::npos ||
key.find("OuterContainer") != std::string::npos) {
foundOuter = true;
}
if (label.find("InnerWorker") != std::string::npos ||
key.find("InnerWorker") != std::string::npos) {
foundInner = true;
}
}
CHECK(foundOuter);
// Inner class extraction depends on grammar capabilities
if (foundInner) {
spdlog::info("Inner class correctly extracted");
} else {
spdlog::warn("Inner class not extracted - may be grammar limitation");
}
}
}
TEST_CASE("EntityGraphService: Namespace symbol extraction",
"[integration][daemon][kg][pbi-009][namespace]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Namespaced symbols are extracted with qualified names") {
std::string cppCode = R"(
namespace company {
namespace product {
class ServiceHandler {
public:
void handleRequest() {}
};
void globalHelper() {}
} // namespace product
} // namespace company
)";
std::string filename = "namespaced.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Look for class and function nodes
auto classNodesResult = kg->findNodesByType("class", 100, 0);
auto funcNodesResult = kg->findNodesByType("function", 100, 0);
bool foundServiceHandler = false;
bool foundGlobalHelper = false;
if (classNodesResult.has_value()) {
for (const auto& node : classNodesResult.value()) {
std::string label = node.label.value_or("");
std::string key = node.nodeKey;
spdlog::info("Namespace test class: label={}, key={}", label, key);
if (label.find("ServiceHandler") != std::string::npos ||
key.find("ServiceHandler") != std::string::npos) {
foundServiceHandler = true;
// Check if qualified name includes namespace
if (key.find("company") != std::string::npos ||
key.find("product") != std::string::npos) {
spdlog::info("Namespace found in qualified name: {}", key);
}
}
}
}
if (funcNodesResult.has_value()) {
for (const auto& node : funcNodesResult.value()) {
std::string label = node.label.value_or("");
std::string key = node.nodeKey;
spdlog::info("Namespace test function: label={}, key={}", label, key);
if (label.find("globalHelper") != std::string::npos ||
key.find("globalHelper") != std::string::npos) {
foundGlobalHelper = true;
}
}
}
CHECK(foundServiceHandler);
CHECK(foundGlobalHelper);
}
}
TEST_CASE("EntityGraphService: Template class extraction",
"[integration][daemon][kg][pbi-009][template]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Template class creates a class node") {
std::string cppCode = R"(
template<typename T, typename Allocator = std::allocator<T>>
class GenericContainer {
public:
void add(const T& item) {}
T get(size_t index) const { return T{}; }
private:
T* data_;
};
template<typename K, typename V>
struct KeyValue {
K key;
V value;
};
)";
std::string filename = "templates.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto classNodesResult = kg->findNodesByType("class", 100, 0);
REQUIRE(classNodesResult.has_value());
bool foundGenericContainer = false;
bool foundKeyValue = false;
for (const auto& node : classNodesResult.value()) {
std::string label = node.label.value_or("");
std::string key = node.nodeKey;
spdlog::info("Template test: label={}, key={}", label, key);
if (label.find("GenericContainer") != std::string::npos ||
key.find("GenericContainer") != std::string::npos) {
foundGenericContainer = true;
}
if (label.find("KeyValue") != std::string::npos ||
key.find("KeyValue") != std::string::npos) {
foundKeyValue = true;
}
}
// At least template class should be detected
CHECK((foundGenericContainer || foundKeyValue));
spdlog::info("Template extraction: GenericContainer={}, KeyValue={}", foundGenericContainer,
foundKeyValue);
}
}
TEST_CASE("EntityGraphService: Symbol relationships extraction",
"[integration][daemon][kg][pbi-009][relations]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Inheritance creates 'inherits' edge") {
std::string cppCode = R"(
class BaseEntity {
public:
virtual void update() {}
};
class DerivedComponent : public BaseEntity {
public:
void update() override {}
void render() {}
};
)";
std::string filename = "inheritance.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto classNodesResult = kg->findNodesByType("class", 100, 0);
REQUIRE(classNodesResult.has_value());
// Find DerivedComponent node
std::optional<int64_t> derivedNodeId;
for (const auto& node : classNodesResult.value()) {
std::string label = node.label.value_or("");
std::string key = node.nodeKey;
if (label.find("DerivedComponent") != std::string::npos ||
key.find("DerivedComponent") != std::string::npos) {
derivedNodeId = node.id;
spdlog::info("Found DerivedComponent with id={}", node.id);
break;
}
}
if (derivedNodeId.has_value()) {
// Check for inherits edge
auto edgesResult = kg->getEdgesFrom(*derivedNodeId, std::nullopt, 100, 0);
if (edgesResult.has_value()) {
bool foundInheritsEdge = false;
for (const auto& edge : edgesResult.value()) {
spdlog::info("Edge from DerivedComponent: relation={}", edge.relation);
if (edge.relation == "inherits" || edge.relation == "extends" ||
edge.relation == "derives_from") {
foundInheritsEdge = true;
}
}
// Inheritance edge extraction depends on grammar/extractor capability
if (foundInheritsEdge) {
spdlog::info("Inheritance relationship correctly captured");
} else {
spdlog::warn("Inheritance edge not found - may require enhanced extractor");
}
}
}
}
SECTION("Function calls create 'calls' edge") {
std::string cppCode = R"(
void helperFunction() {}
void callerFunction() {
helperFunction();
helperFunction();
}
)";
std::string filename = "calls.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto funcNodesResult = kg->findNodesByType("function", 100, 0);
REQUIRE(funcNodesResult.has_value());
// Find callerFunction node
std::optional<int64_t> callerNodeId;
for (const auto& node : funcNodesResult.value()) {
std::string label = node.label.value_or("");
std::string key = node.nodeKey;
if (label.find("callerFunction") != std::string::npos ||
key.find("callerFunction") != std::string::npos) {
callerNodeId = node.id;
spdlog::info("Found callerFunction with id={}", node.id);
break;
}
}
if (callerNodeId.has_value()) {
auto edgesResult = kg->getEdgesFrom(*callerNodeId, std::nullopt, 100, 0);
if (edgesResult.has_value()) {
bool foundCallsEdge = false;
for (const auto& edge : edgesResult.value()) {
spdlog::info("Edge from callerFunction: relation={}", edge.relation);
if (edge.relation == "calls" || edge.relation == "invokes") {
foundCallsEdge = true;
}
}
if (foundCallsEdge) {
spdlog::info("Call relationship correctly captured");
} else {
spdlog::warn("Call edge not found - may require call graph extraction enabled");
}
}
}
}
}
TEST_CASE("EntityGraphService: Cross-file call linking via alias resolution",
"[integration][daemon][kg][pbi-009][relations][cross-file]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
const std::string calleeCode = R"(
int sharedHelper() {
return 7;
}
)";
const std::string callerCode = R"(
int callerFunction() {
return sharedHelper();
}
)";
auto calleeHash = fixture.storeContent("cross_helper.cpp", calleeCode);
auto callerHash = fixture.storeContent("cross_caller.cpp", callerCode);
const std::string calleePath = fixture.buildFilePath("cross_helper.cpp");
const std::string callerPath = fixture.buildFilePath("cross_caller.cpp");
REQUIRE(fixture.submitAndWaitForExtraction(calleeHash, calleePath, calleeCode, "cpp", 3000));
REQUIRE(fixture.submitAndWaitForExtraction(callerHash, callerPath, callerCode, "cpp", 3000));
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto functionNodes = kg->findNodesByType("function_version", 500, 0);
REQUIRE(functionNodes.has_value());
std::optional<int64_t> callerNodeId;
std::optional<int64_t> calleeNodeId;
for (const auto& node : functionNodes.value()) {
if (node.label.has_value() && node.label.value() == "callerFunction" &&
node.nodeKey.find("cross_caller.cpp") != std::string::npos) {
callerNodeId = node.id;
}
if (node.label.has_value() && node.label.value() == "sharedHelper" &&
node.nodeKey.find("cross_helper.cpp") != std::string::npos) {
calleeNodeId = node.id;
}
}
REQUIRE(callerNodeId.has_value());
REQUIRE(calleeNodeId.has_value());
auto callerEdges = kg->getEdgesFrom(*callerNodeId, std::nullopt, 200, 0);
REQUIRE(callerEdges.has_value());
bool foundCrossFileCall = false;
bool foundProvenance = false;
bool foundResolution = false;
for (const auto& edge : callerEdges.value()) {
if (edge.relation != "calls" || edge.dstNodeId != *calleeNodeId) {
continue;
}
foundCrossFileCall = true;
REQUIRE(edge.properties.has_value());
auto props = nlohmann::json::parse(*edge.properties, nullptr, false);
REQUIRE(!props.is_discarded());
if (props.contains("provenance") && props["provenance"].is_object()) {
foundProvenance = props["provenance"].value("source", std::string{}) == "treesitter";
}
if (props.contains("resolution") && props["resolution"].is_string()) {
const std::string resolution = props["resolution"].get<std::string>();
foundResolution =
resolution == "alias_exact" || resolution == "alias_tail" || resolution == "local";
}
break;
}
REQUIRE(foundCrossFileCall);
CHECK(foundProvenance);
CHECK(foundResolution);
}
TEST_CASE("EntityGraphService: Member variables extraction",
"[integration][daemon][kg][pbi-009][members]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Class members are extracted") {
std::string cppCode = R"(
class DataContainer {
public:
int publicValue;
std::string name;
void setName(const std::string& n) { name = n; }
private:
double privateData;
static int instanceCount;
};
)";
std::string filename = "members.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Check for class node
auto classNodesResult = kg->findNodesByType("class", 100, 0);
REQUIRE(classNodesResult.has_value());
bool foundDataContainer = false;
for (const auto& node : classNodesResult.value()) {
if (node.label.value_or("").find("DataContainer") != std::string::npos ||
node.nodeKey.find("DataContainer") != std::string::npos) {
foundDataContainer = true;
break;
}
}
REQUIRE(foundDataContainer);
// Check for variable/field nodes (depends on extractor capability)
auto varNodesResult = kg->findNodesByType("variable", 100, 0);
auto fieldNodesResult = kg->findNodesByType("field", 100, 0);
size_t varCount = varNodesResult.has_value() ? varNodesResult.value().size() : 0;
size_t fieldCount = fieldNodesResult.has_value() ? fieldNodesResult.value().size() : 0;
spdlog::info("Member extraction: {} variables, {} fields detected", varCount, fieldCount);
// Member extraction is optional - just log for visibility
}
}
TEST_CASE("EntityGraphService: Error recovery for malformed code",
"[integration][daemon][kg][pbi-009][error-recovery]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Partial parsing extracts valid symbols") {
// Code with syntax errors but some valid symbols
std::string cppCode = R"(
// Valid function
void validFunction() {
// Valid body
}
// Invalid/incomplete class
class BrokenClass {
// Missing closing brace
// Another valid function
int anotherValid(int x) {
return x * 2;
}
)";
std::string filename = "malformed.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
// Should not crash - may extract some or no symbols
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000);
// Extraction should complete (not crash)
CHECK(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Check if any valid symbols were recovered
auto funcNodesResult = kg->findNodesByType("function", 100, 0);
if (funcNodesResult.has_value()) {
spdlog::info("Error recovery: {} functions extracted from malformed code",
funcNodesResult.value().size());
// May or may not find validFunction depending on parser recovery
for (const auto& node : funcNodesResult.value()) {
spdlog::info("Recovered function: {}", node.label.value_or(node.nodeKey));
}
}
}
}
TEST_CASE("EntityGraphService: Large file symbol extraction",
"[integration][daemon][kg][pbi-009][large-file]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Large file with many symbols extracts efficiently") {
// Generate a file with many functions
std::string cppCode;
const int NUM_FUNCTIONS = 100;
for (int i = 0; i < NUM_FUNCTIONS; i++) {
cppCode += "void function_" + std::to_string(i) + "(int x) { /* body */ }\n\n";
}
std::string filename = "large_file.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
auto startTime = std::chrono::steady_clock::now();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 10000);
auto endTime = std::chrono::steady_clock::now();
auto durationMs =
std::chrono::duration_cast<std::chrono::milliseconds>(endTime - startTime).count();
REQUIRE(extracted);
spdlog::info("Large file extraction completed in {}ms", durationMs);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto funcNodesResult = kg->findNodesByType("function", 500, 0);
REQUIRE(funcNodesResult.has_value());
spdlog::info("Extracted {} functions from {} generated functions",
funcNodesResult.value().size(), NUM_FUNCTIONS);
// Should extract a significant portion of functions
CHECK(funcNodesResult.value().size() >= NUM_FUNCTIONS / 2);
}
}
TEST_CASE("EntityGraphService: Symbol update/deduplication",
"[integration][daemon][kg][pbi-009][dedup]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Re-extracting same file updates rather than duplicates") {
std::string cppCode = R"(
void uniqueTestFunction() {
// Original version
}
)";
std::string filename = "dedup_test.cpp";
auto hash1 = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
REQUIRE(fixture.submitAndWaitForExtraction(hash1, filePath, cppCode, "cpp", 3000));
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Count function nodes before second extraction
auto funcNodesBefore = kg->findNodesByType("function", 500, 0);
size_t countBefore = funcNodesBefore.has_value() ? funcNodesBefore.value().size() : 0;
spdlog::info("Function count before re-extraction: {}", countBefore);
// Update content slightly (simulating file modification)
std::string cppCode2 = R"(
void uniqueTestFunction() {
// Modified version with new comment
}
)";
auto hash2 = fixture.storeContent(filename + ".v2", cppCode2);
std::string filePath2 = (fixture.testDir_ / (filename + ".v2")).string();
REQUIRE(fixture.submitAndWaitForExtraction(hash2, filePath2, cppCode2, "cpp", 3000));
// Count function nodes after second extraction
auto funcNodesAfter = kg->findNodesByType("function", 500, 0);
size_t countAfter = funcNodesAfter.has_value() ? funcNodesAfter.value().size() : 0;
spdlog::info("Function count after re-extraction: {}", countAfter);
// Nodes should be deduplicated or updated, not massively duplicated
// Allow for some growth (different files may create separate nodes)
CHECK(countAfter <= countBefore + 5);
}
}
TEST_CASE("EntityGraphService: Python-specific symbol extraction",
"[integration][daemon][kg][pbi-009][python]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
// Check for Python support
const auto& extractors = fixture.serviceManager_->getSymbolExtractors();
bool pythonSupported = false;
for (const auto& ex : extractors) {
if (ex && ex->supportsLanguage("python")) {
pythonSupported = true;
break;
}
}
if (!pythonSupported) {
SKIP("Python grammar not available");
}
SECTION("Python decorators and class methods") {
std::string pyCode = R"(
class ServiceController:
"""Controller for handling service requests."""
def __init__(self, config):
self.config = config
@staticmethod
def create_default():
return ServiceController({})
@classmethod
def from_file(cls, path):
return cls({})
def handle_request(self, request):
"""Process an incoming request."""
return {'status': 'ok'}
def standalone_function(x, y):
"""A standalone function."""
return x + y
)";
std::string filename = "controller.py";
auto hash = fixture.storeContent(filename, pyCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, pyCode, "python", 4000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Check for class
auto classNodes = kg->findNodesByType("class", 100, 0);
bool foundServiceController = false;
if (classNodes.has_value()) {
for (const auto& node : classNodes.value()) {
if (node.label.value_or("").find("ServiceController") != std::string::npos ||
node.nodeKey.find("ServiceController") != std::string::npos) {
foundServiceController = true;
break;
}
}
}
CHECK(foundServiceController);
// Check for functions/methods
auto funcNodes = kg->findNodesByType("function", 100, 0);
size_t funcCount = funcNodes.has_value() ? funcNodes.value().size() : 0;
spdlog::info("Python extraction: found {} functions/methods", funcCount);
CHECK(funcCount >= 1); // At least standalone_function
}
}
TEST_CASE("EntityGraphService: JavaScript/TypeScript extraction",
"[integration][daemon][kg][pbi-009][javascript]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
// Check for JavaScript support
const auto& extractors = fixture.serviceManager_->getSymbolExtractors();
bool jsSupported = false;
for (const auto& ex : extractors) {
if (ex && ex->supportsLanguage("javascript")) {
jsSupported = true;
break;
}
}
if (!jsSupported) {
SKIP("JavaScript grammar not available");
}
SECTION("ES6 classes and arrow functions") {
std::string jsCode = R"(
class DataManager {
constructor(options) {
this.options = options;
}
async fetchData(url) {
const response = await fetch(url);
return response.json();
}
processItems = (items) => {
return items.map(item => item.id);
};
}
const helperFunction = (x) => x * 2;
function traditionalFunction(a, b) {
return a + b;
}
export default DataManager;
)";
std::string filename = "data_manager.js";
auto hash = fixture.storeContent(filename, jsCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted =
fixture.submitAndWaitForExtraction(hash, filePath, jsCode, "javascript", 4000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Check for class
auto classNodes = kg->findNodesByType("class", 100, 0);
bool foundDataManager = false;
if (classNodes.has_value()) {
for (const auto& node : classNodes.value()) {
if (node.label.value_or("").find("DataManager") != std::string::npos ||
node.nodeKey.find("DataManager") != std::string::npos) {
foundDataManager = true;
break;
}
}
}
CHECK(foundDataManager);
// Check for functions
auto funcNodes = kg->findNodesByType("function", 100, 0);
size_t funcCount = funcNodes.has_value() ? funcNodes.value().size() : 0;
spdlog::info("JavaScript extraction: found {} functions", funcCount);
CHECK(funcCount >= 1);
}
}
TEST_CASE("EntityGraphService: Cross-file symbol references",
"[integration][daemon][kg][pbi-009][cross-file]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Symbols from multiple files can be queried together") {
// File 1: Interface definition
std::string headerCode = R"(
class IRepository {
public:
virtual void save() = 0;
virtual void load() = 0;
};
)";
// File 2: Implementation
std::string implCode = R"(
class FileRepository : public IRepository {
public:
void save() override {}
void load() override {}
private:
std::string path_;
};
)";
// File 3: Usage
std::string usageCode = R"(
class DataService {
public:
void process(IRepository& repo) {
repo.load();
// do work
repo.save();
}
};
)";
auto hash1 = fixture.storeContent("repository.h", headerCode);
auto hash2 = fixture.storeContent("file_repository.cpp", implCode);
auto hash3 = fixture.storeContent("data_service.cpp", usageCode);
std::string path1 = (fixture.testDir_ / "repository.h").string();
std::string path2 = (fixture.testDir_ / "file_repository.cpp").string();
std::string path3 = (fixture.testDir_ / "data_service.cpp").string();
REQUIRE(fixture.submitAndWaitForExtraction(hash1, path1, headerCode, "cpp", 2000));
REQUIRE(fixture.submitAndWaitForExtraction(hash2, path2, implCode, "cpp", 2000));
REQUIRE(fixture.submitAndWaitForExtraction(hash3, path3, usageCode, "cpp", 2000));
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
// Get all class nodes
auto classNodes = kg->findNodesByType("class", 100, 0);
REQUIRE(classNodes.has_value());
std::set<std::string> foundClasses;
for (const auto& node : classNodes.value()) {
std::string label = node.label.value_or(node.nodeKey);
spdlog::info("Cross-file class: {}", label);
if (label.find("IRepository") != std::string::npos)
foundClasses.insert("IRepository");
if (label.find("FileRepository") != std::string::npos)
foundClasses.insert("FileRepository");
if (label.find("DataService") != std::string::npos)
foundClasses.insert("DataService");
}
spdlog::info("Found {} out of 3 expected classes across files", foundClasses.size());
CHECK(foundClasses.size() >= 2); // At least 2 of 3 classes found
}
}
// ============================================================================
// KG Cleanup Tests (yams-3m8, yams-qel, yams-ye3)
// Tests document deletion cascade, stale edge cleanup, and isolated node queries
// ============================================================================
TEST_CASE("KnowledgeGraphStore: findIsolatedNodes optimization",
"[integration][daemon][kg][isolated]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Finds functions with no incoming call edges") {
std::string cppCode = R"(
void calledFunction() {}
void callerFunction() {
calledFunction();
}
void isolatedFunction() {}
void anotherIsolated() {}
)";
std::string filename = "isolated_test.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
bool extracted = fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 4000);
REQUIRE(extracted);
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto isolatedResult = kg->findIsolatedNodes("function", "calls", 100);
REQUIRE(isolatedResult.has_value());
spdlog::info("findIsolatedNodes returned {} nodes", isolatedResult.value().size());
for (const auto& node : isolatedResult.value()) {
spdlog::info("Isolated function: label={}", node.label.value_or(node.nodeKey));
}
CHECK(!isolatedResult.value().empty());
}
}
TEST_CASE("KnowledgeGraphStore: deleteEdgesForSourceFile cleans stale edges",
"[integration][daemon][kg][cleanup]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Re-indexing file cleans up old edges") {
std::string cppCode1 = R"(
void originalFunction() {}
)";
std::string filename = "cleanup_test.cpp";
auto hash1 = fixture.storeContent(filename, cppCode1);
std::string filePath = (fixture.testDir_ / filename).string();
REQUIRE(fixture.submitAndWaitForExtraction(hash1, filePath, cppCode1, "cpp", 3000));
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto funcNodes = kg->findNodesByType("function", 100, 0);
REQUIRE(funcNodes.has_value());
std::string cppCode2 = R"(
void renamedFunction() {}
)";
auto hash2 = fixture.storeContent(filename + ".v2", cppCode2);
std::string filePath2 = (fixture.testDir_ / (filename + ".v2")).string();
REQUIRE(fixture.submitAndWaitForExtraction(hash2, filePath2, cppCode2, "cpp", 3000));
spdlog::info("Stale edge cleanup test completed");
}
}
TEST_CASE("KnowledgeGraphStore: deleteNodesForDocumentHash cascades",
"[integration][daemon][kg][cascade]") {
SKIP_ON_WINDOWS_DAEMON_SHUTDOWN();
EntityGraphIntegrationFixture fixture;
if (!fixture.supportsCpp()) {
SKIP("C++ grammar not available");
}
SECTION("Document deletion triggers KG node cleanup") {
std::string cppCode = R"(
void deletionTestFunction() {}
)";
std::string filename = "deletion_test.cpp";
auto hash = fixture.storeContent(filename, cppCode);
std::string filePath = (fixture.testDir_ / filename).string();
REQUIRE(fixture.submitAndWaitForExtraction(hash, filePath, cppCode, "cpp", 3000));
auto kg = fixture.getKgStore();
REQUIRE(kg != nullptr);
auto funcNodesBefore = kg->findNodesByType("function", 500, 0);
REQUIRE(funcNodesBefore.has_value());
size_t countBefore = funcNodesBefore.value().size();
spdlog::info("Function nodes before deletion: {}", countBefore);
auto deleteResult = kg->deleteNodesForDocumentHash(hash);
REQUIRE(deleteResult.has_value());
spdlog::info("Deleted {} nodes for document hash {}", deleteResult.value(),
hash.substr(0, 12));
auto funcNodesAfter = kg->findNodesByType("function", 500, 0);
REQUIRE(funcNodesAfter.has_value());
size_t countAfter = funcNodesAfter.value().size();
spdlog::info("Function nodes after deletion: {}", countAfter);
CHECK(countAfter <= countBefore);
}
}