// 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 #include #include #include #include #include #include #include #include "test_daemon_harness.h" #include #include #include #include #include #include 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 bytes; bytes.reserve(content.size()); for (char c : content) { bytes.push_back(static_cast(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::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 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 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("")); 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("")); 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 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 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", "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> 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(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> class GenericContainer { public: void add(const T& item) {} T get(size_t index) const { return T{}; } private: T* data_; }; template 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 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 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 callerNodeId; std::optional 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(); 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(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 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); } }