diff --git a/pom.xml b/pom.xml
index 10b26e9a..16b0fa67 100644
--- a/pom.xml
+++ b/pom.xml
@@ -266,6 +266,12 @@
org.jacoco
jacoco-maven-plugin
${jacoco.version}
+
+
+
+ io/github/randomcodespace/iq/grammar/**
+
+
prepare-agent
diff --git a/src/test/java/io/github/randomcodespace/iq/analyzer/linker/LinkersCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/analyzer/linker/LinkersCoverageTest.java
new file mode 100644
index 00000000..07e8a171
--- /dev/null
+++ b/src/test/java/io/github/randomcodespace/iq/analyzer/linker/LinkersCoverageTest.java
@@ -0,0 +1,377 @@
+package io.github.randomcodespace.iq.analyzer.linker;
+
+import io.github.randomcodespace.iq.model.CodeEdge;
+import io.github.randomcodespace.iq.model.CodeNode;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Additional coverage tests for linker classes — branches not hit by
+ * existing tests.
+ */
+class LinkersCoverageTest {
+
+ // =====================================================================
+ // GuardLinker
+ // =====================================================================
+ @Nested
+ class GuardLinkerCoverage {
+ private final GuardLinker linker = new GuardLinker();
+
+ @Test
+ void linksGuardToEndpointInSameFile() {
+ var guard = new CodeNode("guard:auth1", NodeKind.GUARD, "AuthGuard");
+ guard.setFilePath("src/UserController.java");
+
+ var endpoint = new CodeNode("ep:getUser", NodeKind.ENDPOINT, "GET /users/{id}");
+ endpoint.setFilePath("src/UserController.java");
+
+ LinkResult result = linker.link(List.of(guard, endpoint), List.of());
+
+ assertEquals(1, result.edges().size());
+ CodeEdge edge = result.edges().getFirst();
+ assertEquals(EdgeKind.PROTECTS, edge.getKind());
+ assertEquals("guard:auth1", edge.getSourceId());
+ assertEquals("ep:getUser", edge.getTarget().getId());
+ assertEquals(true, edge.getProperties().get("inferred"));
+ }
+
+ @Test
+ void linksMiddlewareToEndpoint() {
+ var middleware = new CodeNode("mw:jwt", NodeKind.MIDDLEWARE, "JwtMiddleware");
+ middleware.setFilePath("src/SecureController.java");
+
+ var endpoint = new CodeNode("ep:secure", NodeKind.ENDPOINT, "POST /secure");
+ endpoint.setFilePath("src/SecureController.java");
+
+ LinkResult result = linker.link(List.of(middleware, endpoint), List.of());
+
+ assertEquals(1, result.edges().size());
+ assertEquals(EdgeKind.PROTECTS, result.edges().getFirst().getKind());
+ }
+
+ @Test
+ void noLinkBetweenDifferentFiles() {
+ var guard = new CodeNode("guard:g1", NodeKind.GUARD, "Guard");
+ guard.setFilePath("src/GuardConfig.java");
+
+ var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /data");
+ endpoint.setFilePath("src/DataController.java");
+
+ LinkResult result = linker.link(List.of(guard, endpoint), List.of());
+
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void noGuardsReturnsEmpty() {
+ var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x");
+ endpoint.setFilePath("src/Ctrl.java");
+
+ LinkResult result = linker.link(List.of(endpoint), List.of());
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void noEndpointsReturnsEmpty() {
+ var guard = new CodeNode("g:g1", NodeKind.GUARD, "G");
+ guard.setFilePath("src/Ctrl.java");
+
+ LinkResult result = linker.link(List.of(guard), List.of());
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void nodeWithNullFilePathSkipped() {
+ var guard = new CodeNode("g:g1", NodeKind.GUARD, "G");
+ // no filePath set
+
+ var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x");
+ // no filePath set
+
+ LinkResult result = linker.link(List.of(guard, endpoint), List.of());
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void nodeWithBlankFilePathSkipped() {
+ var guard = new CodeNode("g:g1", NodeKind.GUARD, "G");
+ guard.setFilePath(" ");
+
+ var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x");
+ endpoint.setFilePath(" ");
+
+ LinkResult result = linker.link(List.of(guard, endpoint), List.of());
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void avoidsDuplicateProtectsEdges() {
+ var guard = new CodeNode("g:g1", NodeKind.GUARD, "G");
+ guard.setFilePath("src/Ctrl.java");
+
+ var endpoint = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /x");
+ endpoint.setFilePath("src/Ctrl.java");
+
+ // Pre-existing PROTECTS edge
+ var existing = new CodeEdge();
+ existing.setId("existing");
+ existing.setKind(EdgeKind.PROTECTS);
+ existing.setSourceId("g:g1");
+ existing.setTarget(endpoint);
+
+ LinkResult result = linker.link(List.of(guard, endpoint), List.of(existing));
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void multipleGuardsAndEndpointsCrossLinked() {
+ var guard1 = new CodeNode("g:g1", NodeKind.GUARD, "Auth");
+ guard1.setFilePath("src/Ctrl.java");
+ var guard2 = new CodeNode("g:g2", NodeKind.MIDDLEWARE, "Logging");
+ guard2.setFilePath("src/Ctrl.java");
+ var ep1 = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /a");
+ ep1.setFilePath("src/Ctrl.java");
+ var ep2 = new CodeNode("ep:e2", NodeKind.ENDPOINT, "POST /b");
+ ep2.setFilePath("src/Ctrl.java");
+
+ LinkResult result = linker.link(List.of(guard1, guard2, ep1, ep2), List.of());
+ // 2 guards x 2 endpoints = 4 edges
+ assertEquals(4, result.edges().size());
+ }
+
+ @Test
+ void deterministic() {
+ var guard = new CodeNode("g:g1", NodeKind.GUARD, "G");
+ guard.setFilePath("f.java");
+ var ep1 = new CodeNode("ep:e1", NodeKind.ENDPOINT, "GET /a");
+ ep1.setFilePath("f.java");
+ var ep2 = new CodeNode("ep:e2", NodeKind.ENDPOINT, "GET /b");
+ ep2.setFilePath("f.java");
+
+ LinkResult r1 = linker.link(List.of(guard, ep1, ep2), List.of());
+ LinkResult r2 = linker.link(List.of(guard, ep1, ep2), List.of());
+
+ assertEquals(r1.edges().size(), r2.edges().size());
+ for (int i = 0; i < r1.edges().size(); i++) {
+ assertEquals(r1.edges().get(i).getId(), r2.edges().get(i).getId());
+ }
+ }
+ }
+
+ // =====================================================================
+ // EntityLinker — additional branches
+ // =====================================================================
+ @Nested
+ class EntityLinkerCoverage {
+ private final EntityLinker linker = new EntityLinker();
+
+ @Test
+ void matchesFqnSimpleNameForEntity() {
+ // Entity has fqn "com.example.User" — repo uses label "User"
+ var entity = new CodeNode("entity:com.example.User", NodeKind.ENTITY, "UserEntity");
+ entity.setFqn("com.example.User");
+ var repo = new CodeNode("repo:UserRepository", NodeKind.REPOSITORY, "UserRepository");
+
+ LinkResult result = linker.link(List.of(entity, repo), List.of());
+
+ // "user" (from fqn "com.example.User" -> "user") should match "user" (from "UserRepository" - "Repository")
+ assertEquals(1, result.edges().size());
+ }
+
+ @Test
+ void matchesByLabelLowercase() {
+ var entity = new CodeNode("entity:Product", NodeKind.ENTITY, "Product");
+ var repo = new CodeNode("repo:ProductRepository", NodeKind.REPOSITORY, "ProductRepository");
+
+ LinkResult result = linker.link(List.of(entity, repo), List.of());
+ assertEquals(1, result.edges().size());
+ assertEquals("entity:Product", result.edges().getFirst().getTarget().getId());
+ }
+
+ @Test
+ void multipleReposForSameEntity() {
+ var entity = new CodeNode("entity:Order", NodeKind.ENTITY, "Order");
+ var repo1 = new CodeNode("repo:OrderRepository", NodeKind.REPOSITORY, "OrderRepository");
+ var repo2 = new CodeNode("repo:OrderRepo", NodeKind.REPOSITORY, "OrderRepo");
+
+ LinkResult result = linker.link(List.of(entity, repo1, repo2), List.of());
+ assertEquals(2, result.edges().size());
+ }
+
+ @Test
+ void entityWithNullFqnUsesLabel() {
+ var entity = new CodeNode("entity:Item", NodeKind.ENTITY, "Item");
+ // fqn is null
+ var repo = new CodeNode("repo:ItemDao", NodeKind.REPOSITORY, "ItemDao");
+
+ LinkResult result = linker.link(List.of(entity, repo), List.of());
+ assertEquals(1, result.edges().size());
+ }
+
+ @Test
+ void noPrefixMatchSkips() {
+ // "SalesDAO" doesn't match "Product" entity
+ var entity = new CodeNode("entity:Product", NodeKind.ENTITY, "Product");
+ var repo = new CodeNode("repo:SalesDAO", NodeKind.REPOSITORY, "SalesDAO");
+
+ LinkResult result = linker.link(List.of(entity, repo), List.of());
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ var entity1 = new CodeNode("entity:Alpha", NodeKind.ENTITY, "Alpha");
+ var entity2 = new CodeNode("entity:Beta", NodeKind.ENTITY, "Beta");
+ var repo1 = new CodeNode("repo:AlphaRepository", NodeKind.REPOSITORY, "AlphaRepository");
+ var repo2 = new CodeNode("repo:BetaRepo", NodeKind.REPOSITORY, "BetaRepo");
+
+ LinkResult r1 = linker.link(List.of(entity1, entity2, repo1, repo2), List.of());
+ LinkResult r2 = linker.link(List.of(entity1, entity2, repo1, repo2), List.of());
+
+ assertEquals(r1.edges().size(), r2.edges().size());
+ for (int i = 0; i < r1.edges().size(); i++) {
+ assertEquals(r1.edges().get(i).getId(), r2.edges().get(i).getId());
+ }
+ }
+ }
+
+ // =====================================================================
+ // ModuleContainmentLinker — additional branches
+ // =====================================================================
+ @Nested
+ class ModuleContainmentCoverage {
+ private final ModuleContainmentLinker linker = new ModuleContainmentLinker();
+
+ @Test
+ void multipleKindsInSameModule() {
+ var cls = new CodeNode("cls:A", NodeKind.CLASS, "A");
+ cls.setModule("org.example");
+ var iface = new CodeNode("iface:B", NodeKind.INTERFACE, "B");
+ iface.setModule("org.example");
+ var enm = new CodeNode("enum:C", NodeKind.ENUM, "C");
+ enm.setModule("org.example");
+
+ LinkResult result = linker.link(List.of(cls, iface, enm), List.of());
+
+ assertEquals(1, result.nodes().size()); // one module node
+ assertEquals(3, result.edges().size()); // 3 CONTAINS edges
+ }
+
+ @Test
+ void nullModuleSkipped() {
+ var node = new CodeNode("cls:A", NodeKind.CLASS, "A");
+ // module not set — should be null
+
+ LinkResult result = linker.link(List.of(node), List.of());
+ assertTrue(result.nodes().isEmpty());
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ var n1 = new CodeNode("cls:X", NodeKind.CLASS, "X");
+ n1.setModule("com.mod");
+ var n2 = new CodeNode("cls:Y", NodeKind.CLASS, "Y");
+ n2.setModule("com.mod");
+
+ LinkResult r1 = linker.link(List.of(n1, n2), List.of());
+ LinkResult r2 = linker.link(List.of(n1, n2), List.of());
+
+ assertEquals(r1.nodes().size(), r2.nodes().size());
+ assertEquals(r1.edges().size(), r2.edges().size());
+ }
+ }
+
+ // =====================================================================
+ // TopicLinker — additional branches
+ // =====================================================================
+ @Nested
+ class TopicLinkerCoverage {
+ private final TopicLinker linker = new TopicLinker();
+
+ @Test
+ void emptyNodesAndEdgesReturnsEmpty() {
+ LinkResult result = linker.link(List.of(), List.of());
+ assertTrue(result.edges().isEmpty());
+ assertTrue(result.nodes().isEmpty());
+ }
+
+ @Test
+ void topicWithNoProducersOrConsumersReturnsEmpty() {
+ var topic = new CodeNode("topic:orphan", NodeKind.TOPIC, "orphan");
+ LinkResult result = linker.link(List.of(topic), List.of());
+ assertTrue(result.edges().isEmpty());
+ }
+
+ @Test
+ void multipleConsumersForOneTopic() {
+ var topic = new CodeNode("topic:updates", NodeKind.TOPIC, "updates");
+ var producer = new CodeNode("svc:Prod", NodeKind.CLASS, "Prod");
+ var consumer1 = new CodeNode("svc:Con1", NodeKind.CLASS, "Con1");
+ var consumer2 = new CodeNode("svc:Con2", NodeKind.CLASS, "Con2");
+
+ var producesEdge = new CodeEdge();
+ producesEdge.setId("e1");
+ producesEdge.setKind(EdgeKind.PRODUCES);
+ producesEdge.setSourceId("svc:Prod");
+ producesEdge.setTarget(topic);
+
+ var consumesEdge1 = new CodeEdge();
+ consumesEdge1.setId("e2");
+ consumesEdge1.setKind(EdgeKind.CONSUMES);
+ consumesEdge1.setSourceId("svc:Con1");
+ consumesEdge1.setTarget(topic);
+
+ var consumesEdge2 = new CodeEdge();
+ consumesEdge2.setId("e3");
+ consumesEdge2.setKind(EdgeKind.CONSUMES);
+ consumesEdge2.setSourceId("svc:Con2");
+ consumesEdge2.setTarget(topic);
+
+ LinkResult result = linker.link(
+ List.of(topic, producer, consumer1, consumer2),
+ List.of(producesEdge, consumesEdge1, consumesEdge2));
+
+ assertEquals(2, result.edges().size());
+ }
+
+ @Test
+ void multipleProducersForOneTopic() {
+ var topic = new CodeNode("topic:orders", NodeKind.TOPIC, "orders");
+ var prod1 = new CodeNode("svc:Prod1", NodeKind.CLASS, "Prod1");
+ var prod2 = new CodeNode("svc:Prod2", NodeKind.CLASS, "Prod2");
+ var consumer = new CodeNode("svc:Con", NodeKind.CLASS, "Con");
+
+ var e1 = new CodeEdge();
+ e1.setId("e1");
+ e1.setKind(EdgeKind.PRODUCES);
+ e1.setSourceId("svc:Prod1");
+ e1.setTarget(topic);
+
+ var e2 = new CodeEdge();
+ e2.setId("e2");
+ e2.setKind(EdgeKind.PRODUCES);
+ e2.setSourceId("svc:Prod2");
+ e2.setTarget(topic);
+
+ var e3 = new CodeEdge();
+ e3.setId("e3");
+ e3.setKind(EdgeKind.CONSUMES);
+ e3.setSourceId("svc:Con");
+ e3.setTarget(topic);
+
+ LinkResult result = linker.link(
+ List.of(topic, prod1, prod2, consumer),
+ List.of(e1, e2, e3));
+
+ assertEquals(2, result.edges().size());
+ }
+ }
+}
diff --git a/src/test/java/io/github/randomcodespace/iq/cache/CacheCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/cache/CacheCoverageTest.java
new file mode 100644
index 00000000..a34839da
--- /dev/null
+++ b/src/test/java/io/github/randomcodespace/iq/cache/CacheCoverageTest.java
@@ -0,0 +1,245 @@
+package io.github.randomcodespace.iq.cache;
+
+import io.github.randomcodespace.iq.model.CodeEdge;
+import io.github.randomcodespace.iq.model.CodeNode;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Additional coverage tests for the cache package — branches not hit by
+ * existing tests.
+ */
+class CacheCoverageTest {
+
+ // =====================================================================
+ // FileHasher
+ // =====================================================================
+ @Nested
+ class FileHasherCoverage {
+
+ @Test
+ void hashEmptyFile(@TempDir Path tempDir) throws IOException {
+ Path empty = tempDir.resolve("empty.txt");
+ Files.writeString(empty, "", StandardCharsets.UTF_8);
+
+ String hash = FileHasher.hash(empty);
+ assertNotNull(hash);
+ assertEquals(32, hash.length());
+ assertTrue(hash.matches("[0-9a-f]+"));
+ }
+
+ @Test
+ void hashEmptyString() {
+ String hash = FileHasher.hashString("");
+ assertNotNull(hash);
+ assertEquals(32, hash.length());
+ }
+
+ @Test
+ void hashOfSameStringIsAlwaysSame() {
+ String s = "deterministic content";
+ assertEquals(FileHasher.hashString(s), FileHasher.hashString(s));
+ }
+
+ @Test
+ void hashDiffersForUnicode() {
+ String a = "hello";
+ String b = "héllo"; // accented é
+ assertNotEquals(FileHasher.hashString(a), FileHasher.hashString(b));
+ }
+
+ @Test
+ void hashLargeContent() {
+ // 1 MB string
+ String large = "x".repeat(1_000_000);
+ String hash = FileHasher.hashString(large);
+ assertEquals(32, hash.length());
+ }
+
+ @Test
+ void hashFileAndStringProduceSameResultForSameContent(@TempDir Path tempDir) throws IOException {
+ String content = "match me";
+ Path file = tempDir.resolve("match.txt");
+ Files.writeString(file, content, StandardCharsets.UTF_8);
+
+ String fileHash = FileHasher.hash(file);
+ String stringHash = FileHasher.hashString(content);
+ assertEquals(fileHash, stringHash);
+ }
+ }
+
+ // =====================================================================
+ // AnalysisCache — additional branches
+ // =====================================================================
+ @Nested
+ class AnalysisCacheCoverage {
+
+ private AnalysisCache cache;
+
+ @BeforeEach
+ void setUp(@TempDir Path tempDir) {
+ cache = new AnalysisCache(tempDir.resolve("cov-test.db"));
+ }
+
+ @AfterEach
+ void tearDown() {
+ if (cache != null) cache.close();
+ }
+
+ @Test
+ void storeAndLoadNodeWithAllProperties() {
+ CodeNode node = new CodeNode("cls:X", NodeKind.CLASS, "X");
+ node.setFqn("com.example.X");
+ node.setFilePath("src/X.java");
+ node.setModule("com.example");
+ node.setLayer("backend");
+ node.setLineStart(10);
+ node.setLineEnd(50);
+ node.setAnnotations(List.of("@Service", "@Transactional"));
+ node.setProperties(Map.of("framework", "spring_boot", "layer", "backend"));
+
+ cache.storeResults("hash-full", "src/X.java", "java", List.of(node), List.of());
+ var result = cache.loadCachedResults("hash-full");
+
+ assertNotNull(result);
+ CodeNode loaded = result.nodes().getFirst();
+ assertEquals("com.example.X", loaded.getFqn());
+ assertEquals("com.example", loaded.getModule());
+ assertEquals("backend", loaded.getLayer());
+ assertEquals(10, loaded.getLineStart());
+ assertEquals(50, loaded.getLineEnd());
+ assertTrue(loaded.getAnnotations().contains("@Service"));
+ assertEquals("spring_boot", loaded.getProperties().get("framework"));
+ }
+
+ @Test
+ void storeEdgeAndLoadBackWithProperties() {
+ CodeNode src = new CodeNode("cls:A", NodeKind.CLASS, "A");
+ CodeNode tgt = new CodeNode("cls:B", NodeKind.CLASS, "B");
+ CodeEdge edge = new CodeEdge("e:A->B", EdgeKind.DEPENDS_ON, "cls:A", tgt);
+ edge.setProperties(Map.of("inferred", true, "reason", "naming_convention"));
+
+ cache.storeResults("hash-edge", "src/A.java", "java",
+ List.of(src, tgt), List.of(edge));
+ var result = cache.loadCachedResults("hash-edge");
+
+ assertNotNull(result);
+ assertEquals(1, result.edges().size());
+ CodeEdge loaded = result.edges().getFirst();
+ assertEquals(EdgeKind.DEPENDS_ON, loaded.getKind());
+ assertEquals("cls:A", loaded.getSourceId());
+ assertEquals("cls:B", loaded.getTarget().getId());
+ }
+
+ @Test
+ void multipleStoresAndLoadsSeparateHashes() {
+ var nodeA = new CodeNode("n:A", NodeKind.CLASS, "A");
+ var nodeB = new CodeNode("n:B", NodeKind.CLASS, "B");
+
+ cache.storeResults("hash-a", "A.java", "java", List.of(nodeA), List.of());
+ cache.storeResults("hash-b", "B.java", "java", List.of(nodeB), List.of());
+
+ var rA = cache.loadCachedResults("hash-a");
+ var rB = cache.loadCachedResults("hash-b");
+
+ assertNotNull(rA);
+ assertNotNull(rB);
+ assertEquals("n:A", rA.nodes().getFirst().getId());
+ assertEquals("n:B", rB.nodes().getFirst().getId());
+ }
+
+ @Test
+ void clearResetsAllCounters() {
+ var n = new CodeNode("n:X", NodeKind.CLASS, "X");
+ cache.storeResults("h1", "X.java", "java", List.of(n), List.of());
+ cache.storeResults("h2", "Y.java", "java", List.of(n), List.of());
+ cache.recordRun("sha1", 5);
+
+ cache.clear();
+
+ var stats = cache.getStats();
+ assertEquals(0L, stats.get("cached_files"));
+ assertEquals(0L, stats.get("cached_nodes"));
+ assertEquals(0L, stats.get("total_runs"));
+ assertNull(cache.getLastCommit());
+ }
+
+ @Test
+ void multipleRunsGetLastCommitReturnsLatest() {
+ cache.recordRun("sha-first", 1);
+ cache.recordRun("sha-second", 2);
+ cache.recordRun("sha-third", 3);
+
+ assertEquals("sha-third", cache.getLastCommit());
+ }
+
+ @Test
+ void isCachedReturnsTrueAfterStore() {
+ var n = new CodeNode("n:A", NodeKind.CLASS, "A");
+ cache.storeResults("tracked-hash", "A.java", "java", List.of(n), List.of());
+ assertTrue(cache.isCached("tracked-hash"));
+ }
+
+ @Test
+ void isCachedReturnsFalseForRemovedFile() {
+ var n = new CodeNode("n:A", NodeKind.CLASS, "A");
+ cache.storeResults("remove-hash", "A.java", "java", List.of(n), List.of());
+ cache.removeFile("remove-hash");
+ assertFalse(cache.isCached("remove-hash"));
+ }
+
+ @Test
+ void storeEmptyNodeListIsCachedButLoadReturnsNull() {
+ // Store with empty nodes and edges — isCached checks the files table
+ cache.storeResults("empty-nodes-hash", "Empty.java", "java", List.of(), List.of());
+ // The file entry is recorded
+ assertTrue(cache.isCached("empty-nodes-hash"));
+ // But loadCachedResults returns null when nodes AND edges are both empty
+ var result = cache.loadCachedResults("empty-nodes-hash");
+ assertNull(result);
+ }
+
+ @Test
+ void statsTotalNodesAcrossMultipleFiles() {
+ var n1 = new CodeNode("n:1", NodeKind.CLASS, "A");
+ var n2 = new CodeNode("n:2", NodeKind.CLASS, "B");
+ var n3 = new CodeNode("n:3", NodeKind.METHOD, "m");
+
+ cache.storeResults("file1", "A.java", "java", List.of(n1, n2), List.of());
+ cache.storeResults("file2", "B.java", "java", List.of(n3), List.of());
+
+ var stats = cache.getStats();
+ assertEquals(2L, stats.get("cached_files"));
+ assertEquals(3L, stats.get("cached_nodes"));
+ }
+
+ @Test
+ void deterministic() {
+ var n = new CodeNode("n:D", NodeKind.CLASS, "D");
+ cache.storeResults("det-hash", "D.java", "java", List.of(n), List.of());
+
+ var r1 = cache.loadCachedResults("det-hash");
+ var r2 = cache.loadCachedResults("det-hash");
+
+ assertNotNull(r1);
+ assertNotNull(r2);
+ assertEquals(r1.nodes().size(), r2.nodes().size());
+ assertEquals(r1.nodes().getFirst().getId(), r2.nodes().getFirst().getId());
+ }
+ }
+}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/auth/AuthDetectorsCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/detector/auth/AuthDetectorsCoverageTest.java
new file mode 100644
index 00000000..73e8413b
--- /dev/null
+++ b/src/test/java/io/github/randomcodespace/iq/detector/auth/AuthDetectorsCoverageTest.java
@@ -0,0 +1,471 @@
+package io.github.randomcodespace.iq.detector.auth;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Additional coverage for auth detectors — branches not hit by existing tests.
+ */
+class AuthDetectorsCoverageTest {
+
+ // =====================================================================
+ // CertificateAuthDetector
+ // =====================================================================
+ @Nested
+ class CertificateCoverage {
+ private final CertificateAuthDetector d = new CertificateAuthDetector();
+
+ @Test
+ void detectsRequestCertMtls() {
+ DetectorResult r = d.detect(ctx("typescript", "const opts = { requestCert: true, rejectUnauthorized: true };"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("mtls", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsClientAuthEqualsTrueMtls() {
+ // Pattern is: clientAuth = "true" (literal double-quote around true)
+ DetectorResult r = d.detect(ctx("yaml", "clientAuth = \"true\""));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("mtls", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsX509AuthenticationFilter() {
+ DetectorResult r = d.detect(ctx("java",
+ "http.addFilter(new X509AuthenticationFilter());"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsAddCertificateForwarding() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "services.AddCertificateForwarding(opts => {});"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("mtls", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsCertificateAuthenticationDefaults() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme);"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("x509", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsX509Fluent() {
+ DetectorResult r = d.detect(ctx("java",
+ "auth.x509().subjectPrincipalRegex(\"CN=(.*?)(?:,|$)\");"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("x509", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsJavaxKeyStore() {
+ DetectorResult r = d.detect(ctx("java",
+ "System.setProperty(\"javax.net.ssl.keyStore\", \"/certs/server.jks\");"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsSslSSLContext() {
+ DetectorResult r = d.detect(ctx("python",
+ "ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT)"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsTlsCreateServer() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const server = tls.createServer({ key, cert });"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsCertPathInTlsConfig() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const cert = fs.readFileSync('/etc/certs/server.pem');"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type"));
+ assertNotNull(r.nodes().get(0).getProperties().get("cert_path"));
+ }
+
+ @Test
+ void detectsTrustStore() {
+ DetectorResult r = d.detect(ctx("java", "System.setProperty(\"trustStore\", \"/path/store.jks\");"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("tls_config", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsAzureAd() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "// AzureAd section configured in appsettings.json"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("azure_ad", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsAzureTenantId() {
+ DetectorResult r = d.detect(ctx("yaml",
+ "AZURE_TENANT_ID: abc-def-123"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("azure_ad", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsClientCertificateCredentialWithAuthFlow() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "var cred = new ClientCertificateCredential(tenantId, clientId, cert);"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("client_certificate", r.nodes().get(0).getProperties().get("auth_flow"));
+ }
+
+ @Test
+ void detectsMsalWithMsalAuthFlow() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "import * as msal from '@azure/msal-browser';"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("msal", r.nodes().get(0).getProperties().get("auth_flow"));
+ }
+
+ @Test
+ void detectsAddMicrosoftIdentityWebApi() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "services.AddMicrosoftIdentityWebApi(config);"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("azure_ad", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void multipleMatchesOnSameLineOnlyOneNode() {
+ // A line matching two patterns — only one node per line
+ DetectorResult r = d.detect(ctx("yaml", " AZURE_TENANT_ID: abc\n AZURE_CLIENT_ID: xyz"));
+ assertEquals(2, r.nodes().size()); // two lines, one match each
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx("java", ""));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("test.java", "java", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("java",
+ "X509AuthenticationFilter f;\nssl_verify_client on;\ntrustStore=/path/store.jks"));
+ }
+ }
+
+ // =====================================================================
+ // LdapAuthDetector
+ // =====================================================================
+ @Nested
+ class LdapCoverage {
+ private final LdapAuthDetector d = new LdapAuthDetector();
+
+ @Test
+ void detectsLdapTemplateInJava() {
+ DetectorResult r = d.detect(ctx("java", "@Autowired LdapTemplate ldapTemplate;"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("ldap", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsActiveDirectoryLdapAuthProviderInJava() {
+ DetectorResult r = d.detect(ctx("java",
+ "new ActiveDirectoryLdapAuthenticationProvider(domain, url)"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsEnableLdapRepositoriesInJava() {
+ DetectorResult r = d.detect(ctx("java",
+ "@EnableLdapRepositories\n@SpringBootApplication\npublic class App {}"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsLdap3ConnectionInPython() {
+ DetectorResult r = d.detect(ctx("python",
+ "conn = ldap3.Connection(server, user=dn, password=pw)"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("ldap", r.nodes().get(0).getProperties().get("auth_type"));
+ assertEquals("python", r.nodes().get(0).getProperties().get("language"));
+ }
+
+ @Test
+ void detectsLdap3ServerInPython() {
+ DetectorResult r = d.detect(ctx("python",
+ "server = ldap3.Server('ldap.example.com', port=389)"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsAuthLdapBindDnInPython() {
+ DetectorResult r = d.detect(ctx("python",
+ "AUTH_LDAP_BIND_DN = 'cn=django-agent,dc=example,dc=com'"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsLdapjsRequireInTypeScript() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const ldap = require('ldapjs');"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsLdapjsImportInTypeScript() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "import ldapjs from 'ldapjs';"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsPassportLdapauthInTypeScript() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const LdapStrategy = require('passport-ldapauth');"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsSystemDirectoryServicesInCSharp() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "using System.DirectoryServices;"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsLdapConnectionInCSharp() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "LdapConnection conn = new LdapConnection(new LdapDirectoryIdentifier(server));"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsDirectoryEntryInCSharp() {
+ DetectorResult r = d.detect(ctx("csharp",
+ "DirectoryEntry entry = new DirectoryEntry(path, user, password);"));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void unsupportedLanguageReturnsEmpty() {
+ DetectorResult r = d.detect(ctx("go", "ldap.Connect(\"server\", 389)"));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx("java", ""));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("f.java", "java", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("java",
+ "LdapContextSource src;\nLdapTemplate tmpl;\nActiveDirectoryLdapAuthenticationProvider prov;"));
+ }
+ }
+
+ // =====================================================================
+ // SessionHeaderAuthDetector
+ // =====================================================================
+ @Nested
+ class SessionHeaderCoverage {
+ private final SessionHeaderAuthDetector d = new SessionHeaderAuthDetector();
+
+ @Test
+ void detectsCookieSession() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const session = require('cookie-session');"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("session", r.nodes().get(0).getProperties().get("auth_type"));
+ assertEquals(NodeKind.MIDDLEWARE, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void detectsSessionAttributesInJava() {
+ DetectorResult r = d.detect(ctx("java",
+ "@SessionAttributes(\"user\")\npublic class UserController {}"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("session", r.nodes().get(0).getProperties().get("auth_type"));
+ assertEquals(NodeKind.GUARD, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void detectsSessionMiddlewareInTypeScript() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "app.use(SessionMiddleware({ secret: 'key' }));"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("session", r.nodes().get(0).getProperties().get("auth_type"));
+ assertEquals(NodeKind.MIDDLEWARE, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void detectsHttpSessionInJava() {
+ DetectorResult r = d.detect(ctx("java",
+ "HttpSession session = request.getSession();"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("session", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsSessionEngineInPython() {
+ DetectorResult r = d.detect(ctx("python",
+ "SESSION_ENGINE = 'django.contrib.sessions.backends.db'"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("session", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsXApiKeyHeader() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const key = req.headers['X-API-Key'];"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("header", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsAuthorizationHeaderAccess() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const token = req.headers['authorization'];"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("header", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsGetHeaderAuthorization() {
+ DetectorResult r = d.detect(ctx("java",
+ "String token = request.getHeader(\"Authorization\");"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("header", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsXApiKeyLowercase() {
+ // The HEADER pattern '['"X-API-Key'"]' (CASE_INSENSITIVE) fires before the API_KEY
+ // req.headers pattern, so 'x-api-key' is classified as "header" auth_type
+ DetectorResult r = d.detect(ctx("typescript",
+ "const key = req.headers['x-api-key'];"));
+ assertFalse(r.nodes().isEmpty());
+ // Either header or api_key is valid — just verify detection occurred
+ String authType = (String) r.nodes().get(0).getProperties().get("auth_type");
+ assertTrue("header".equals(authType) || "api_key".equals(authType));
+ }
+
+ @Test
+ void detectsApiKeyAssignment() {
+ // "api_key = " pattern (API_KEY) fires for this content
+ DetectorResult r = d.detect(ctx("python",
+ "api_key = os.getenv('SERVICE_KEY')"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("api_key", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsValidateApiKey() {
+ DetectorResult r = d.detect(ctx("python",
+ "if not validate_api_key(request):\n raise Unauthorized"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("api_key", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsCsrfProtectDecorator() {
+ DetectorResult r = d.detect(ctx("python",
+ "@csrf_protect\ndef my_view(request): pass"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsCsrfExempt() {
+ DetectorResult r = d.detect(ctx("python",
+ "@csrf_exempt\ndef public_api(request): pass"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void detectsCsrfViewMiddleware() {
+ DetectorResult r = d.detect(ctx("python",
+ "MIDDLEWARE = ['django.middleware.csrf.CsrfViewMiddleware']"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type"));
+ assertEquals(NodeKind.MIDDLEWARE, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void detectsCsurfMiddleware() {
+ DetectorResult r = d.detect(ctx("typescript",
+ "const csrf = require('csurf');"));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("csrf", r.nodes().get(0).getProperties().get("auth_type"));
+ }
+
+ @Test
+ void unsupportedLanguageReturnsEmpty() {
+ // csharp not in supported languages list
+ DetectorResult r = d.detect(ctx("csharp",
+ "app.UseSession();"));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx("java", ""));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("f.ts", "typescript", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void multipleMatchesOnMultipleLines() {
+ DetectorResult r = d.detect(ctx("java",
+ "HttpSession s = req.getSession();\nrequest.getHeader(\"Authorization\");"));
+ assertEquals(2, r.nodes().size());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("typescript",
+ "require('express-session');\nreq.headers['authorization'];\nrequire('csurf');"));
+ }
+ }
+
+ // =====================================================================
+ // Helpers
+ // =====================================================================
+
+ private static DetectorContext ctx(String language, String content) {
+ return DetectorTestUtils.contextFor(language, content);
+ }
+}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java
index be315dbf..f5773797 100644
--- a/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java
+++ b/src/test/java/io/github/randomcodespace/iq/detector/cpp/CppStructuresDetectorTest.java
@@ -1,11 +1,128 @@
package io.github.randomcodespace.iq.detector.cpp;
-import io.github.randomcodespace.iq.detector.*; import io.github.randomcodespace.iq.model.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
class CppStructuresDetectorTest {
+
private final CppStructuresDetector d = new CppStructuresDetector();
- @Test void detectsClassAndNamespace() {
- DetectorResult r = d.detect(DetectorTestUtils.contextFor("cpp", "#include \nnamespace app {\nclass User : public Entity {\n};\n}"));
+
+ @Test
+ void detectsClass() {
+ String code = "#include \nclass User {\npublic:\n std::string name;\n};\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "User".equals(n.getLabel())));
+ }
+
+ @Test
+ void detectsClassWithBaseClass() {
+ String code = "class AdminUser : public User {\n};\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.EXTENDS));
+ }
+
+ @Test
+ void detectsStruct() {
+ String code = "struct Point {\n int x;\n int y;\n};\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "Point".equals(n.getLabel())));
+ }
+
+ @Test
+ void detectsStructWithBase() {
+ String code = "struct ColoredPoint : public Point {\n int color;\n};\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.EXTENDS));
+ }
+
+ @Test
+ void detectsNamespace() {
+ String code = "namespace app {\n class Service {};\n}\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE && "app".equals(n.getLabel())));
+ }
+
+ @Test
+ void detectsEnum() {
+ String code = "enum Status {\n ACTIVE,\n INACTIVE\n};\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENUM));
+ }
+
+ @Test
+ void detectsEnumClass() {
+ String code = "enum class Color {\n RED,\n GREEN,\n BLUE\n};\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENUM));
+ }
+
+ @Test
+ void detectsInclude() {
+ String code = "#include \n#include \n#include \"myheader.hpp\"\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(3, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count());
+ }
+
+ @Test
+ void detectsClassAndNamespace() {
+ DetectorResult r = d.detect(ctx("#include \nnamespace app {\nclass User : public Entity {\n};\n}"));
assertTrue(r.nodes().size() >= 2);
}
- @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("cpp", "")).nodes().size()); }
- @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("cpp", "#include \nclass A {\n};\nstruct B {\n};")); }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx(""));
+ assertTrue(r.nodes().isEmpty());
+ assertTrue(r.edges().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorContext ctxNull = new DetectorContext("test.cpp", "cpp", null);
+ DetectorResult r = d.detect(ctxNull);
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void returnsCorrectName() {
+ assertEquals("cpp_structures", d.getName());
+ }
+
+ @Test
+ void supportedLanguagesContainsCpp() {
+ assertTrue(d.getSupportedLanguages().contains("cpp"));
+ }
+
+ @Test
+ void deterministic() {
+ String code = """
+ #include
+ #include
+ namespace myapp {
+ class Vehicle {
+ public:
+ std::string make;
+ };
+ class Car : public Vehicle {
+ };
+ struct Point {
+ int x;
+ int y;
+ };
+ enum class Color { RED, GREEN, BLUE };
+ }
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx(code));
+ }
+
+ private static DetectorContext ctx(String content) {
+ return DetectorTestUtils.contextFor("cpp", content);
+ }
}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/csharp/CSharpDetectorsCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/detector/csharp/CSharpDetectorsCoverageTest.java
new file mode 100644
index 00000000..1187075e
--- /dev/null
+++ b/src/test/java/io/github/randomcodespace/iq/detector/csharp/CSharpDetectorsCoverageTest.java
@@ -0,0 +1,413 @@
+package io.github.randomcodespace.iq.detector.csharp;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Additional coverage tests for C# detectors — branches not hit by existing tests.
+ */
+class CSharpDetectorsCoverageTest {
+
+ // =====================================================================
+ // CSharpStructuresDetector
+ // =====================================================================
+ @Nested
+ class StructuresCoverage {
+ private final CSharpStructuresDetector d = new CSharpStructuresDetector();
+
+ @Test
+ void detectsNamespaceNode() {
+ String code = """
+ namespace MyApp.Controllers
+ {
+ public class HomeController {}
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE));
+ }
+
+ @Test
+ void detectsUsingStatements() {
+ String code = """
+ using System;
+ using System.Collections.Generic;
+ using Microsoft.AspNetCore.Mvc;
+
+ public class UserController {}
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ // Edges for using statements
+ List importEdges = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.IMPORTS)
+ .toList();
+ assertFalse(importEdges.isEmpty());
+ }
+
+ @Test
+ void detectsAbstractClass() {
+ String code = """
+ public abstract class Animal
+ {
+ public abstract string Sound();
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ABSTRACT_CLASS));
+ }
+
+ @Test
+ void detectsClassWithBaseClassAndInterface() {
+ String code = """
+ public class UserService : BaseService, IUserService, IDisposable
+ {
+ public void Dispose() {}
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ // Should have EXTENDS and IMPLEMENTS edges
+ List extendsEdges = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.EXTENDS)
+ .toList();
+ List implementsEdges = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.IMPLEMENTS)
+ .toList();
+ assertFalse(extendsEdges.isEmpty());
+ assertFalse(implementsEdges.isEmpty());
+ }
+
+ @Test
+ void detectsApiControllerEndpoints() {
+ String code = """
+ using Microsoft.AspNetCore.Mvc;
+
+ [ApiController]
+ [Route("api/[controller]")]
+ public class ProductsController : ControllerBase
+ {
+ [HttpGet]
+ public IActionResult GetAll() => Ok();
+
+ [HttpPost]
+ public IActionResult Create(Product p) => CreatedAtAction(nameof(GetAll), p);
+
+ [HttpGet("{id}")]
+ public IActionResult GetById(int id) => Ok();
+
+ [HttpDelete("{id}")]
+ public IActionResult Delete(int id) => NoContent();
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ long endpoints = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count();
+ assertTrue(endpoints >= 3, "Expected at least 3 endpoint nodes, got " + endpoints);
+ }
+
+ @Test
+ void detectsInterfaceWithGenericTypeParam() {
+ String code = """
+ public interface IRepository where T : class
+ {
+ T FindById(int id);
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE));
+ }
+
+ @Test
+ void detectsEnum() {
+ String code = """
+ public enum OrderStatus
+ {
+ Pending,
+ Processing,
+ Shipped,
+ Delivered
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENUM));
+ }
+
+ @Test
+ void detectsMultipleClassesInSameFile() {
+ String code = """
+ public class Request { }
+ public class Response { }
+ public class Handler { }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ long classes = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).count();
+ assertEquals(3, classes);
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx("csharp", ""));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("f.cs", "csharp", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("csharp", """
+ namespace App;
+ using System;
+ public class Foo : Bar, IBaz {}
+ public interface IBaz { void Run(); }
+ public enum Status { A, B, C }
+ """));
+ }
+ }
+
+ // =====================================================================
+ // CSharpEfcoreDetector
+ // =====================================================================
+ @Nested
+ class EfcoreCoverage {
+ private final CSharpEfcoreDetector d = new CSharpEfcoreDetector();
+
+ @Test
+ void detectsDbContextWithNamespaceQualifiedBase() {
+ // "Microsoft.EntityFrameworkCore.DbContext"
+ String code = """
+ public class OrderContext : Microsoft.EntityFrameworkCore.DbContext
+ {
+ public DbSet Orders { get; set; }
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertFalse(r.nodes().isEmpty());
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.REPOSITORY));
+ }
+
+ @Test
+ void detectsDbSetAndCreatesQueriesEdge() {
+ String code = """
+ public class AppContext : DbContext
+ {
+ public DbSet Customers { get; set; }
+ public DbSet Invoices { get; set; }
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ // Should have QUERIES edges
+ List queriesEdges = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.QUERIES)
+ .toList();
+ assertEquals(2, queriesEdges.size());
+ }
+
+ @Test
+ void detectsMigrationClass() {
+ String code = """
+ public class AddUserTable : Migration
+ {
+ protected override void Up(MigrationBuilder mb) {}
+ protected override void Down(MigrationBuilder mb) {}
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MIGRATION));
+ assertEquals("AddUserTable", r.nodes().stream()
+ .filter(n -> n.getKind() == NodeKind.MIGRATION)
+ .findFirst()
+ .orElseThrow()
+ .getLabel());
+ }
+
+ @Test
+ void detectsCreateTableInMigration() {
+ String code = """
+ public class InitialCreate : Migration
+ {
+ protected override void Up(MigrationBuilder mb)
+ {
+ mb.CreateTable(name: "Products", columns: table => new {});
+ mb.CreateTable(name: "Categories", columns: table => new {});
+ }
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ long entities = r.nodes().stream()
+ .filter(n -> n.getKind() == NodeKind.ENTITY)
+ .count();
+ assertEquals(2, entities);
+ }
+
+ @Test
+ void createTableWithExistingEntityNotDuplicated() {
+ // If DbSet already creates an ENTITY node, CreateTable("Products") should not duplicate
+ String code = """
+ public class ShopCtx : DbContext
+ {
+ public DbSet Products { get; set; }
+ }
+ public class CreateProductsMigration : Migration
+ {
+ protected override void Up(MigrationBuilder mb)
+ {
+ mb.CreateTable(name: "Products", columns: table => new {});
+ }
+ }
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ long productEntities = r.nodes().stream()
+ .filter(n -> n.getKind() == NodeKind.ENTITY && "Products".equals(n.getLabel()))
+ .count();
+ // Should be 1 (no duplicate)
+ assertEquals(1, productEntities);
+ }
+
+ @Test
+ void noEfCorePatternReturnsEmpty() {
+ String code = "public class RegularService { public void Run() {} }";
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("f.cs", "csharp", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("csharp", """
+ public class Ctx : DbContext {
+ public DbSet Foos { get; set; }
+ }
+ public class Init : Migration {}
+ """));
+ }
+ }
+
+ // =====================================================================
+ // CSharpMinimalApisDetector
+ // =====================================================================
+ @Nested
+ class MinimalApisCoverage {
+ private final CSharpMinimalApisDetector d = new CSharpMinimalApisDetector();
+
+ @Test
+ void detectsMapPatchEndpoint() {
+ String code = """
+ var builder = WebApplication.CreateBuilder(args);
+ var app = builder.Build();
+ app.MapPatch("/api/users/{id}", (int id, User u) => Results.Ok(u));
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT
+ && "PATCH".equals(n.getProperties().get("http_method"))));
+ }
+
+ @Test
+ void webApplicationCreateBuilderCreatesModuleNode() {
+ String code = """
+ var builder = WebApplication.CreateBuilder(args);
+ var app = builder.Build();
+ app.MapGet("/health", () => "ok");
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ // Module node from WebApplication.CreateBuilder
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE));
+ // Endpoint linked to module
+ assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.EXPOSES).toList().isEmpty());
+ }
+
+ @Test
+ void detectsUseAuthenticationGuard() {
+ String code = """
+ app.UseAuthentication();
+ app.UseAuthorization();
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ long guards = r.nodes().stream().filter(n -> n.getKind() == NodeKind.GUARD).count();
+ assertEquals(2, guards);
+ }
+
+ @Test
+ void detectsAddAuthenticationGuard() {
+ String code = """
+ builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme);
+ builder.Services.AddAuthorization();
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ long guards = r.nodes().stream().filter(n -> n.getKind() == NodeKind.GUARD).count();
+ assertEquals(2, guards);
+ }
+
+ @Test
+ void endpointsWithoutBuilderHaveNoExposesEdge() {
+ // No WebApplication.CreateBuilder => no module node, no EXPOSES edges
+ String code = """
+ app.MapGet("/ping", () => "pong");
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertEquals(1, r.nodes().size());
+ assertTrue(r.edges().isEmpty());
+ }
+
+ @Test
+ void detectsAllHttpMethods() {
+ String code = """
+ app.MapGet("/a", () => {});
+ app.MapPost("/b", () => {});
+ app.MapPut("/c", () => {});
+ app.MapDelete("/d", () => {});
+ app.MapPatch("/e", () => {});
+ """;
+ DetectorResult r = d.detect(ctx("csharp", code));
+ long endpoints = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count();
+ assertEquals(5, endpoints);
+ }
+
+ @Test
+ void noPatternReturnsEmpty() {
+ String code = "var x = 1;";
+ DetectorResult r = d.detect(ctx("csharp", code));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("prog.cs", "csharp", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("csharp", """
+ var builder = WebApplication.CreateBuilder(args);
+ var app = builder.Build();
+ app.UseAuthentication();
+ app.MapGet("/a", () => {});
+ app.MapPost("/b", () => {});
+ """));
+ }
+ }
+
+ // =====================================================================
+ // Helpers
+ // =====================================================================
+
+ private static DetectorContext ctx(String language, String content) {
+ return DetectorTestUtils.contextFor(language, content);
+ }
+}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java
index 34bc4740..7bf35be8 100644
--- a/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java
+++ b/src/test/java/io/github/randomcodespace/iq/detector/docs/MarkdownStructureDetectorTest.java
@@ -1,11 +1,144 @@
package io.github.randomcodespace.iq.detector.docs;
-import io.github.randomcodespace.iq.detector.*; import io.github.randomcodespace.iq.model.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
class MarkdownStructureDetectorTest {
+
private final MarkdownStructureDetector d = new MarkdownStructureDetector();
- @Test void detectsHeadings() {
- DetectorResult r = d.detect(DetectorTestUtils.contextFor("markdown", "# My Doc\n## Section 1\nSome text\n## Section 2\n[link](other.md)"));
+
+ @Test
+ void detectsModuleNodeForAnyContent() {
+ String code = "plain text without headings";
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(1, r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).count());
+ }
+
+ @Test
+ void moduleNodeLabelIsFilenameWhenNoH1() {
+ DetectorResult r = d.detect(DetectorTestUtils.contextFor("test/README.md", "markdown",
+ "## Section without H1\nSome content"));
+ var module = r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).findFirst().orElseThrow();
+ // label should be the filename since no H1
+ assertNotNull(module.getLabel());
+ }
+
+ @Test
+ void detectsH1AsModuleLabel() {
+ String code = "# My Project\n## Overview\nSome text\n";
+ DetectorResult r = d.detect(ctx(code));
+ var module = r.nodes().stream().filter(n -> n.getKind() == NodeKind.MODULE).findFirst().orElseThrow();
+ assertEquals("My Project", module.getLabel());
+ }
+
+ @Test
+ void detectsHeadings() {
+ String code = "# My Doc\n## Section 1\nSome text\n## Section 2\n[link](other.md)";
+ DetectorResult r = d.detect(ctx(code));
assertTrue(r.nodes().size() >= 3);
}
- @Test void noMatch() { assertEquals(1, d.detect(DetectorTestUtils.contextFor("markdown", "plain text")).nodes().size()); }
- @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("markdown", "# Title\n## A\n## B")); }
+
+ @Test
+ void detectsH1ToH3Headings() {
+ String code = """
+ # Title
+ ## Chapter 1
+ ### Section 1.1
+ ## Chapter 2
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ var headings = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CONFIG_KEY).toList();
+ assertEquals(4, headings.size());
+ }
+
+ @Test
+ void headingNodeHasLevelProperty() {
+ String code = "# Title\n## SubTitle\n";
+ DetectorResult r = d.detect(ctx(code));
+ var h1 = r.nodes().stream()
+ .filter(n -> n.getKind() == NodeKind.CONFIG_KEY && "Title".equals(n.getLabel()))
+ .findFirst().orElseThrow();
+ assertEquals(1, h1.getProperties().get("level"));
+ var h2 = r.nodes().stream()
+ .filter(n -> n.getKind() == NodeKind.CONFIG_KEY && "SubTitle".equals(n.getLabel()))
+ .findFirst().orElseThrow();
+ assertEquals(2, h2.getProperties().get("level"));
+ }
+
+ @Test
+ void detectsContainsEdgeForHeadings() {
+ String code = "# Doc\n## Section\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CONTAINS));
+ }
+
+ @Test
+ void detectsInternalLinks() {
+ String code = "# Doc\nSee [installation guide](installation.md) for details.\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.DEPENDS_ON));
+ }
+
+ @Test
+ void skipsExternalLinks() {
+ String code = "# Doc\nSee [external](https://example.com) for details.\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.edges().stream().noneMatch(e -> e.getKind() == EdgeKind.DEPENDS_ON));
+ }
+
+ @Test
+ void skipsAnchorOnlyLinks() {
+ String code = "# Doc\nJump to [section](#section)\n";
+ DetectorResult r = d.detect(ctx(code));
+ // link target is just #section, so linkPath="" -> skipped
+ assertTrue(r.edges().stream().noneMatch(e -> e.getKind() == EdgeKind.DEPENDS_ON));
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx(""));
+ assertTrue(r.nodes().isEmpty());
+ assertTrue(r.edges().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorContext ctxNull = new DetectorContext("test.md", "markdown", null);
+ DetectorResult r = d.detect(ctxNull);
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void returnsCorrectName() {
+ assertEquals("markdown_structure", d.getName());
+ }
+
+ @Test
+ void supportedLanguagesContainsMarkdown() {
+ assertTrue(d.getSupportedLanguages().contains("markdown"));
+ }
+
+ @Test
+ void deterministic() {
+ String code = """
+ # Project Documentation
+ ## Installation
+ Run `npm install` to install dependencies.
+ ## Usage
+ See [getting started](getting-started.md) guide.
+ ### Advanced Usage
+ More info at [docs](docs/advanced.md).
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx(code));
+ }
+
+ private static DetectorContext ctx(String content) {
+ return DetectorTestUtils.contextFor("markdown", content);
+ }
}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorsCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorsCoverageTest.java
new file mode 100644
index 00000000..978bc73f
--- /dev/null
+++ b/src/test/java/io/github/randomcodespace/iq/detector/frontend/FrontendDetectorsCoverageTest.java
@@ -0,0 +1,570 @@
+package io.github.randomcodespace.iq.detector.frontend;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Additional coverage tests for frontend detectors — branches not hit by
+ * existing tests.
+ */
+class FrontendDetectorsCoverageTest {
+
+ // =====================================================================
+ // ReactComponentDetector
+ // =====================================================================
+ @Nested
+ class ReactCoverage {
+ private final ReactComponentDetector d = new ReactComponentDetector();
+
+ @Test
+ void classExtendsReactComponentIsDetected() {
+ String code = """
+ class Dashboard extends React.Component {
+ render() { return
; }
+ }
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals(NodeKind.COMPONENT, r.nodes().get(0).getKind());
+ assertEquals("class", r.nodes().get(0).getProperties().get("component_type"));
+ }
+
+ @Test
+ void classExtendsComponentIsDetected() {
+ String code = """
+ class Login extends Component {
+ render() { return ; }
+ }
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("Login", r.nodes().get(0).getLabel());
+ }
+
+ @Test
+ void multipleHooksExportedAsConst() {
+ String code = """
+ export const useFetch = () => { return {}; };
+ export const useDebounce = () => { return {}; };
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(2, r.nodes().size());
+ assertTrue(r.nodes().stream().allMatch(n -> n.getKind() == NodeKind.HOOK));
+ }
+
+ @Test
+ void duplicateComponentNameIsDeduped() {
+ String code = """
+ export default function App() { return ; }
+ export default function App() { return ; }
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ // Should only appear once (deduplicated)
+ assertEquals(1, r.nodes().size());
+ }
+
+ @Test
+ void duplicateHookIsDeduped() {
+ String code = """
+ export function useData() { return {}; }
+ export function useData() { return {}; }
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("App.tsx", "typescript", null));
+ assertTrue(r.nodes().isEmpty());
+ assertTrue(r.edges().isEmpty());
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx("typescript", ""));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void exportConstFCPattern() {
+ String code = "export const Nav: React.FC = () => ;";
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("Nav", r.nodes().get(0).getLabel());
+ assertEquals("function", r.nodes().get(0).getProperties().get("component_type"));
+ }
+
+ @Test
+ void jsxTagsFromMultipleComponentsAreScopedCorrectly() {
+ // Each component should only get its own JSX children
+ String code = """
+ export const Alpha = () => ;
+ export const Gamma = () => ;
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(2, r.nodes().size());
+
+ List alphaRenders = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.RENDERS && e.getSourceId().contains("Alpha"))
+ .map(e -> e.getTarget().getId())
+ .toList();
+ List gammaRenders = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.RENDERS && e.getSourceId().contains("Gamma"))
+ .map(e -> e.getTarget().getId())
+ .toList();
+ assertTrue(alphaRenders.contains("Beta"));
+ assertFalse(alphaRenders.contains("Delta"));
+ assertTrue(gammaRenders.contains("Delta"));
+ assertFalse(gammaRenders.contains("Beta"));
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("typescript",
+ """
+ class Modal extends React.Component {
+ render() { return
; }
+ }
+ export function useModal() { return {}; }
+ """));
+ }
+ }
+
+ // =====================================================================
+ // VueComponentDetector
+ // =====================================================================
+ @Nested
+ class VueCoverage {
+ private final VueComponentDetector d = new VueComponentDetector();
+
+ @Test
+ void detectsOptionsApiStyle() {
+ String code = "export default { name: 'ProductList' }";
+ DetectorResult r = d.detect(ctx("javascript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("options", r.nodes().get(0).getProperties().get("api_style"));
+ }
+
+ @Test
+ void detectsDefineComponentCompositionStyle() {
+ String code = """
+ export default defineComponent({
+ name: 'ShoppingCart',
+ setup() { return {}; }
+ })
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("ShoppingCart", r.nodes().get(0).getLabel());
+ assertEquals("composition", r.nodes().get(0).getProperties().get("api_style"));
+ }
+
+ @Test
+ void detectsComposableFunctionWithRef() {
+ String code = """
+ export function useTheme() {
+ const theme = ref('light');
+ return { theme };
+ }
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals(NodeKind.HOOK, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void detectsComposableConstWithRef() {
+ String code = """
+ export const useLocale = () => {
+ const locale = ref('en');
+ return { locale };
+ }
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals(NodeKind.HOOK, r.nodes().get(0).getKind());
+ assertEquals("useLocale", r.nodes().get(0).getLabel());
+ }
+
+ @Test
+ void scriptSetupExtractsNameFromVueFile() {
+ String code = "\n{{ msg }}
";
+ DetectorResult r = d.detect(new DetectorContext("components/MyWidget.vue", "vue", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("MyWidget", r.nodes().get(0).getLabel());
+ assertEquals("script_setup", r.nodes().get(0).getProperties().get("api_style"));
+ }
+
+ @Test
+ void scriptSetupNonVueExtensionReturnsEmpty() {
+ // Non-.vue file path — extractScriptSetupName returns null
+ String code = "";
+ DetectorResult r = d.detect(new DetectorContext("app.ts", "typescript", code));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void duplicateComposableNameIsDeduped() {
+ String code = """
+ export function useStore() { return {}; }
+ export function useStore() { return {}; }
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("javascript",
+ "export default { name: 'App' }\nexport function useData() {}"));
+ }
+ }
+
+ // =====================================================================
+ // SvelteComponentDetector
+ // =====================================================================
+ @Nested
+ class SvelteCoverage {
+ private final SvelteComponentDetector d = new SvelteComponentDetector();
+
+ @Test
+ void detectsWithReactiveStatement() {
+ String code = """
+
+ $: doubled = count * 2;
+ {doubled}
+ """;
+ DetectorResult r = d.detect(new DetectorContext("Counter.svelte", "svelte", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("svelte", r.nodes().get(0).getProperties().get("framework"));
+ }
+
+ @Test
+ void detectsWithScriptAndHtmlTemplate() {
+ String code = """
+
+ Hello {name}!
+ """;
+ DetectorResult r = d.detect(new DetectorContext("Hello.svelte", "svelte", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("Hello", r.nodes().get(0).getLabel());
+ }
+
+ @Test
+ void detectsPropsAndStoresThemInProperties() {
+ String code = """
+
+ {title}: {count}
+ """;
+ DetectorResult r = d.detect(new DetectorContext("Card.svelte", "svelte", code));
+ assertEquals(1, r.nodes().size());
+ @SuppressWarnings("unchecked")
+ List props = (List) r.nodes().get(0).getProperties().get("props");
+ assertTrue(props.contains("title"));
+ assertTrue(props.contains("count"));
+ }
+
+ @Test
+ void multipleReactiveStatementsCountedCorrectly() {
+ String code = """
+ export let x;
+ $: doubled = x * 2;
+ $: tripled = x * 3;
+ """;
+ DetectorResult r = d.detect(new DetectorContext("Reactive.svelte", "svelte", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals(2, r.nodes().get(0).getProperties().get("reactive_statements"));
+ }
+
+ @Test
+ void noSvelteSignaturesReturnsEmpty() {
+ // Plain JS without Svelte patterns
+ String code = "const x = 1;\nfunction foo() { return 42; }";
+ DetectorResult r = d.detect(ctx("svelte", code));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("A.svelte", "svelte", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void fileNameWithoutExtensionHandled() {
+ String code = "export let value;";
+ DetectorResult r = d.detect(new DetectorContext("NoExt", "svelte", code));
+ assertEquals(1, r.nodes().size());
+ // Component name falls back to filename without extension
+ assertEquals("NoExt", r.nodes().get(0).getLabel());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d,
+ new DetectorContext("Widget.svelte", "svelte",
+ "export let a;\nexport let b;\n$: sum = a + b;"));
+ }
+ }
+
+ // =====================================================================
+ // AngularComponentDetector
+ // =====================================================================
+ @Nested
+ class AngularCoverage {
+ private final AngularComponentDetector d = new AngularComponentDetector();
+
+ @Test
+ void detectsComponentWithSelector() {
+ String code = """
+ @Component({
+ selector: 'app-sidebar',
+ templateUrl: './sidebar.component.html'
+ })
+ export class SidebarComponent {}
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("app-sidebar", r.nodes().get(0).getProperties().get("selector"));
+ assertEquals("Component", r.nodes().get(0).getProperties().get("decorator"));
+ }
+
+ @Test
+ void detectsInjectableWithPlatformScope() {
+ String code = """
+ @Injectable({
+ providedIn: 'platform'
+ })
+ export class PlatformService {}
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("platform", r.nodes().get(0).getProperties().get("provided_in"));
+ assertEquals(NodeKind.MIDDLEWARE, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void detectsDirectiveWithAttributeSelector() {
+ String code = """
+ @Directive({
+ selector: '[appTooltip]'
+ })
+ export class TooltipDirective {}
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("[appTooltip]", r.nodes().get(0).getProperties().get("selector"));
+ assertEquals("Directive", r.nodes().get(0).getProperties().get("decorator"));
+ }
+
+ @Test
+ void detectsPipeWithCustomName() {
+ String code = """
+ @Pipe({
+ name: 'truncate'
+ })
+ export class TruncatePipe {}
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("truncate", r.nodes().get(0).getProperties().get("pipe_name"));
+ assertEquals("Pipe", r.nodes().get(0).getProperties().get("decorator"));
+ }
+
+ @Test
+ void detectsNgModuleWithDeclarations() {
+ String code = """
+ @NgModule({
+ declarations: [HomeComponent, NavComponent],
+ bootstrap: [AppComponent]
+ })
+ export class CoreModule {}
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ assertEquals("NgModule", r.nodes().get(0).getProperties().get("decorator"));
+ }
+
+ @Test
+ void deduplicatesByClassName() {
+ // Same class appearing twice (e.g., decorator appearing on same class) — deduplicated
+ String code = """
+ @Component({ selector: 'app-dup' })
+ class DupComponent {}
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ }
+
+ @Test
+ void noAngularDecoratorsReturnsEmpty() {
+ String code = "export class PlainService { doSomething() {} }";
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void deterministic() {
+ DetectorTestUtils.assertDeterministic(d, ctx("typescript",
+ "@Component({ selector: 'app-root' })\nexport class AppComponent {}"));
+ }
+ }
+
+ // =====================================================================
+ // FrontendRouteDetector
+ // =====================================================================
+ @Nested
+ class FrontendRouteCoverage {
+ private final FrontendRouteDetector d = new FrontendRouteDetector();
+
+ @Test
+ void detectsReactRouteWithComponentProp() {
+ String code = """
+
+
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(2, r.nodes().size());
+ assertFalse(r.edges().isEmpty());
+ }
+
+ @Test
+ void nextjsAppRouterMatchesPageFile() {
+ // app/settings/page.tsx -> route /settings
+ DetectorResult r = d.detect(
+ new DetectorContext("app/settings/page.tsx", "typescript", "export default function SettingsPage() {}"));
+ assertEquals(1, r.nodes().size());
+ assertEquals("route /settings", r.nodes().get(0).getLabel());
+ }
+
+ @Test
+ void nextjsPagesIndexMapsToRoot() {
+ DetectorResult r = d.detect(
+ new DetectorContext("pages/index.tsx", "typescript", "export default function Home() {}"));
+ assertEquals(1, r.nodes().size());
+ assertEquals("route /", r.nodes().get(0).getLabel());
+ }
+
+ @Test
+ void nextjsNestedPageRoute() {
+ DetectorResult r = d.detect(
+ new DetectorContext("pages/blog/[slug].tsx", "typescript", "export default function Post() {}"));
+ assertEquals(1, r.nodes().size());
+ assertTrue(r.nodes().get(0).getLabel().startsWith("route /blog"));
+ }
+
+ @Test
+ void vueRouterWithComponentLinks() {
+ String code = """
+ const router = createRouter({
+ routes: [
+ { path: '/users', component: UsersPage },
+ { path: '/users/:id', component: UserDetailPage }
+ ]
+ });
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertTrue(r.nodes().size() >= 2);
+ // There should be RENDERS edges for components
+ assertFalse(r.edges().isEmpty());
+ }
+
+ @Test
+ void angularRoutesWithComponents() {
+ String code = """
+ RouterModule.forRoot([
+ { path: 'home', component: HomeComponent },
+ { path: 'about', component: AboutComponent },
+ { path: 'contact', component: ContactComponent }
+ ])
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertTrue(r.nodes().size() >= 3);
+ assertFalse(r.edges().isEmpty());
+ }
+
+ @Test
+ void angularChildRoutes() {
+ String code = """
+ RouterModule.forChild([
+ { path: 'dashboard', component: DashboardComponent }
+ ])
+ """;
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertEquals(1, r.nodes().size());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorResult r = d.detect(new DetectorContext("test.ts", "typescript", null));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void vueRoutesColonArrayPatternTriggersDetection() {
+ // VUE_ROUTES_ARRAY matches "routes: [" (colon, not equals)
+ String code = """
+ const router = {
+ routes: [
+ { path: '/home' }
+ ]
+ };
+ """;
+ // VUE_ROUTES_ARRAY matches "routes: [" — Vue detection triggered
+ DetectorResult r = d.detect(ctx("typescript", code));
+ assertTrue(r.nodes().size() >= 1);
+ }
+
+ @Test
+ void plainObjectWithPathButNoVuePatternNotDetectedAsVueRoute() {
+ // Only { path: ... } without createRouter or routes: keyword
+ String code = "const cfg = { path: '/api' };";
+ // No createRouter and no "routes:" keyword — Vue detection skipped entirely
+ DetectorResult r = d.detect(ctx("typescript", code));
+ // Angular and React also won't fire (no RouterModule, no } />\n} />"));
+ }
+ }
+
+ // =====================================================================
+ // Helpers
+ // =====================================================================
+
+ private static DetectorContext ctx(String language, String content) {
+ return DetectorTestUtils.contextFor(language, content);
+ }
+}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/go/GoOrmDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/go/GoOrmDetectorTest.java
index 0f9d0aad..9f245e56 100644
--- a/src/test/java/io/github/randomcodespace/iq/detector/go/GoOrmDetectorTest.java
+++ b/src/test/java/io/github/randomcodespace/iq/detector/go/GoOrmDetectorTest.java
@@ -1,11 +1,164 @@
package io.github.randomcodespace.iq.detector.go;
-import io.github.randomcodespace.iq.detector.*; import io.github.randomcodespace.iq.model.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
class GoOrmDetectorTest {
+
private final GoOrmDetector d = new GoOrmDetector();
- @Test void detectsGormModel() {
- DetectorResult r = d.detect(DetectorTestUtils.contextFor("go", "import \"gorm.io/gorm\"\ntype User struct {\n gorm.Model\n}"));
- assertTrue(r.nodes().size() >= 1); assertEquals(NodeKind.ENTITY, r.nodes().get(0).getKind());
+
+ @Test
+ void detectsGormModel() {
+ String code = "import \"gorm.io/gorm\"\ntype User struct {\n gorm.Model\n Name string\n}";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().size() >= 1);
+ assertEquals(NodeKind.ENTITY, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void gormModelHasFrameworkProperty() {
+ String code = "import \"gorm.io/gorm\"\ntype Product struct {\n gorm.Model\n}";
+ DetectorResult r = d.detect(ctx(code));
+ var entity = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENTITY).findFirst().orElseThrow();
+ assertEquals("gorm", entity.getProperties().get("framework"));
+ }
+
+ @Test
+ void detectsGormAutoMigrate() {
+ String code = "import \"gorm.io/gorm\"\ndb.AutoMigrate(&User{}, &Product{})";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MIGRATION));
+ }
+
+ @Test
+ void detectsGormQueryEdges() {
+ String code = """
+ import "gorm.io/gorm"
+ func getUsers(db *gorm.DB) {
+ db.Find(&users)
+ db.Where("active = ?", true).First(&user)
+ db.Create(&newUser)
+ db.Save(&user)
+ db.Delete(&user)
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.QUERIES).findAny().isEmpty());
+ }
+
+ @Test
+ void detectsSqlxConnection() {
+ String code = """
+ import "github.com/jmoiron/sqlx"
+ func connect() {
+ db := sqlx.Connect("postgres", dsn)
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.DATABASE_CONNECTION
+ && "sqlx".equals(n.getProperties().get("framework"))));
+ }
+
+ @Test
+ void detectsSqlxQueryEdges() {
+ String code = """
+ import "github.com/jmoiron/sqlx"
+ func query(db *sqlx.DB) {
+ db.Select(&users, "SELECT * FROM users")
+ db.Get(&user, "SELECT * FROM users WHERE id=$1", 1)
+ db.NamedExec("INSERT INTO users VALUES (:name)", user)
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.QUERIES).findAny().isEmpty());
+ }
+
+ @Test
+ void detectsDatabaseSqlConnection() {
+ String code = """
+ import "database/sql"
+ func connect() {
+ db, _ := sql.Open("mysql", dsn)
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.DATABASE_CONNECTION
+ && "database_sql".equals(n.getProperties().get("framework"))));
+ }
+
+ @Test
+ void detectsDatabaseSqlQueryEdges() {
+ String code = """
+ import "database/sql"
+ func query(db *sql.DB) {
+ rows, _ := db.Query("SELECT * FROM items")
+ row := db.QueryRow("SELECT * FROM items WHERE id = ?", 1)
+ db.Exec("DELETE FROM items WHERE id = ?", 1)
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertFalse(r.edges().stream().filter(e -> e.getKind() == EdgeKind.QUERIES).findAny().isEmpty());
+ }
+
+ @Test
+ void noMatchOnPlainGoCode() {
+ DetectorResult r = d.detect(ctx("package main\nfunc main() {}"));
+ assertEquals(0, r.nodes().size());
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx(""));
+ assertTrue(r.nodes().isEmpty());
+ assertTrue(r.edges().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorContext ctxNull = new DetectorContext("test.go", "go", null);
+ DetectorResult r = d.detect(ctxNull);
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void returnsCorrectName() {
+ assertEquals("go_orm", d.getName());
+ }
+
+ @Test
+ void supportedLanguagesContainsGo() {
+ assertTrue(d.getSupportedLanguages().contains("go"));
+ }
+
+ @Test
+ void deterministic() {
+ String code = """
+ import "gorm.io/gorm"
+ type User struct {
+ gorm.Model
+ Name string
+ }
+ type Order struct {
+ gorm.Model
+ Total float64
+ }
+ func setup(db *gorm.DB) {
+ db.AutoMigrate(&User{}, &Order{})
+ db.Create(&User{Name: "test"})
+ db.Find(&users)
+ db.Where("name = ?", "test").First(&user)
+ }
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx(code));
+ }
+
+ private static DetectorContext ctx(String content) {
+ return DetectorTestUtils.contextFor("go", content);
}
- @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("go", "package main")).nodes().size()); }
- @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("go", "import \"gorm.io/gorm\"\ntype User struct {\n gorm.Model\n}\ndb.AutoMigrate(&User{})")); }
}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java
index d614910d..1801ef50 100644
--- a/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java
+++ b/src/test/java/io/github/randomcodespace/iq/detector/go/GoStructuresDetectorTest.java
@@ -1,11 +1,183 @@
package io.github.randomcodespace.iq.detector.go;
-import io.github.randomcodespace.iq.detector.*; import io.github.randomcodespace.iq.model.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
class GoStructuresDetectorTest {
+
private final GoStructuresDetector d = new GoStructuresDetector();
- @Test void detectsStructAndInterface() {
- DetectorResult r = d.detect(DetectorTestUtils.contextFor("go", "package main\ntype User struct {\n}\ntype Reader interface {\n}"));
+
+ @Test
+ void detectsPackageNode() {
+ DetectorResult r = d.detect(ctx("package main\n"));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.MODULE));
+ }
+
+ @Test
+ void detectsStruct() {
+ String code = """
+ package models
+ type User struct {
+ ID int
+ Name string
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.CLASS && "User".equals(n.getLabel())));
+ }
+
+ @Test
+ void detectsExportedStructFlagTrue() {
+ String code = "package p\ntype PublicStruct struct {}\n";
+ DetectorResult r = d.detect(ctx(code));
+ var node = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).findFirst().orElseThrow();
+ assertEquals(true, node.getProperties().get("exported"));
+ }
+
+ @Test
+ void detectsUnexportedStructFlagFalse() {
+ String code = "package p\ntype privateStruct struct {}\n";
+ DetectorResult r = d.detect(ctx(code));
+ var node = r.nodes().stream().filter(n -> n.getKind() == NodeKind.CLASS).findFirst().orElseThrow();
+ assertEquals(false, node.getProperties().get("exported"));
+ }
+
+ @Test
+ void detectsInterface() {
+ String code = """
+ package repository
+ type UserRepository interface {
+ FindAll() []User
+ FindByID(id int) User
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.INTERFACE && "UserRepository".equals(n.getLabel())));
+ }
+
+ @Test
+ void detectsMethodOnReceiver() {
+ String code = """
+ package service
+ type UserService struct{}
+ func (s *UserService) GetUser(id int) User {
+ return User{}
+ }
+ func (s *UserService) CreateUser(u User) error {
+ return nil
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count());
+ // Methods have receiver_type property
+ assertTrue(r.nodes().stream()
+ .filter(n -> n.getKind() == NodeKind.METHOD)
+ .allMatch(n -> "UserService".equals(n.getProperties().get("receiver_type"))));
+ }
+
+ @Test
+ void detectsMethodProducesDefinesEdge() {
+ String code = """
+ package svc
+ type Svc struct{}
+ func (s *Svc) Do() {}
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.DEFINES));
+ }
+
+ @Test
+ void detectsTopLevelFunctions() {
+ String code = """
+ package main
+ func main() {}
+ func helper() int { return 42 }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.METHOD).count());
+ }
+
+ @Test
+ void detectsBlockImports() {
+ String code = """
+ package main
+ import (
+ "fmt"
+ "net/http"
+ "github.com/gorilla/mux"
+ )
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.IMPORTS));
+ assertEquals(3, r.edges().stream().filter(e -> e.getKind() == EdgeKind.IMPORTS).count());
+ }
+
+ @Test
+ void detectsSingleImport() {
+ String code = "package main\nimport \"os\"\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.IMPORTS
+ && "os".equals(e.getTarget().getLabel())));
+ }
+
+ @Test
+ void detectsStructAndInterface() {
+ DetectorResult r = d.detect(ctx("package main\ntype User struct {\n}\ntype Reader interface {\n}"));
assertTrue(r.nodes().size() >= 3);
}
- @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("go", "")).nodes().size()); }
- @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("go", "package main\ntype Foo struct {\n}\nfunc Bar() {}")); }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx(""));
+ assertTrue(r.nodes().isEmpty());
+ assertTrue(r.edges().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorContext ctxNull = new DetectorContext("test.go", "go", null);
+ DetectorResult r = d.detect(ctxNull);
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void returnsCorrectName() {
+ assertEquals("go_structures", d.getName());
+ }
+
+ @Test
+ void supportedLanguagesContainsGo() {
+ assertTrue(d.getSupportedLanguages().contains("go"));
+ }
+
+ @Test
+ void deterministic() {
+ String code = """
+ package main
+ import (
+ "fmt"
+ "os"
+ )
+ type Config struct {
+ Name string
+ Port int
+ }
+ type Configurable interface {
+ Configure() error
+ }
+ func (c *Config) Apply() error { return nil }
+ func run() { fmt.Println("start") }
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx(code));
+ }
+
+ private static DetectorContext ctx(String content) {
+ return DetectorTestUtils.contextFor("go", content);
+ }
}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java b/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java
index 9cdbf984..a4bac39c 100644
--- a/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java
+++ b/src/test/java/io/github/randomcodespace/iq/detector/go/GoWebDetectorTest.java
@@ -1,11 +1,158 @@
package io.github.randomcodespace.iq.detector.go;
-import io.github.randomcodespace.iq.detector.*; import io.github.randomcodespace.iq.model.*; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
class GoWebDetectorTest {
+
private final GoWebDetector d = new GoWebDetector();
- @Test void detectsGinRoute() {
- DetectorResult r = d.detect(DetectorTestUtils.contextFor("go", "r := gin.Default()\nr.GET(\"/users\", getUsers)"));
- assertTrue(r.nodes().size() >= 1); assertEquals(NodeKind.ENDPOINT, r.nodes().get(0).getKind());
+
+ @Test
+ void detectsGinGetRoute() {
+ String code = "r := gin.Default()\nr.GET(\"/users\", getUsers)\n";
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.ENDPOINT));
+ assertEquals(NodeKind.ENDPOINT, r.nodes().get(0).getKind());
+ }
+
+ @Test
+ void detectsGinFramework() {
+ String code = "r := gin.Default()\nr.GET(\"/ping\", pingHandler)\n";
+ DetectorResult r = d.detect(ctx(code));
+ var ep = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).findFirst().orElseThrow();
+ assertEquals("gin", ep.getProperties().get("framework"));
+ assertEquals("GET", ep.getProperties().get("http_method"));
+ assertEquals("/ping", ep.getProperties().get("path"));
+ }
+
+ @Test
+ void detectsAllGinMethods() {
+ String code = """
+ r := gin.New()
+ r.GET("/items", list)
+ r.POST("/items", create)
+ r.PUT("/items/:id", update)
+ r.DELETE("/items/:id", delete)
+ r.PATCH("/items/:id", patch)
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(5, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count());
+ }
+
+ @Test
+ void detectsEchoRoutes() {
+ String code = """
+ e := echo.New()
+ e.GET("/items", getItems)
+ e.POST("/items", createItem)
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count());
+ }
+
+ @Test
+ void detectsEchoFramework() {
+ String code = "e := echo.New()\ne.GET(\"/health\", healthCheck)\n";
+ DetectorResult r = d.detect(ctx(code));
+ var ep = r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).findFirst().orElseThrow();
+ assertEquals("echo", ep.getProperties().get("framework"));
+ }
+
+ @Test
+ void detectsChiLowercaseRoutes() {
+ String code = """
+ r := chi.NewRouter()
+ r.Get("/health", healthCheck)
+ r.Post("/webhook", handleWebhook)
+ r.Delete("/data/{id}", deleteData)
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(3, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count());
+ }
+
+ @Test
+ void detectsNetHttpHandleFunc() {
+ String code = """
+ func main() {
+ http.HandleFunc("/hello", helloHandler)
+ http.Handle("/static/", fs)
+ }
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count());
+ }
+
+ @Test
+ void detectsMuxHandleFuncWithMethods() {
+ String code = """
+ r := mux.NewRouter()
+ r.HandleFunc("/api/users", getUsers).Methods("GET")
+ r.HandleFunc("/api/users", createUser).Methods("POST")
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertTrue(r.nodes().stream().filter(n -> n.getKind() == NodeKind.ENDPOINT).count() >= 2);
+ }
+
+ @Test
+ void detectsMiddlewareUse() {
+ String code = """
+ r := gin.Default()
+ r.Use(cors)
+ r.Use(authMiddleware)
+ r.GET("/users", getUsers)
+ """;
+ DetectorResult r = d.detect(ctx(code));
+ assertEquals(2, r.nodes().stream().filter(n -> n.getKind() == NodeKind.MIDDLEWARE).count());
+ }
+
+ @Test
+ void noMatchOnPlainGoCode() {
+ DetectorResult r = d.detect(ctx("package main\nfunc main() {}"));
+ assertEquals(0, r.nodes().size());
+ }
+
+ @Test
+ void emptyContentReturnsEmpty() {
+ DetectorResult r = d.detect(ctx(""));
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void nullContentReturnsEmpty() {
+ DetectorContext ctxNull = new DetectorContext("test.go", "go", null);
+ DetectorResult r = d.detect(ctxNull);
+ assertTrue(r.nodes().isEmpty());
+ }
+
+ @Test
+ void returnsCorrectName() {
+ assertEquals("go_web", d.getName());
+ }
+
+ @Test
+ void supportedLanguagesContainsGo() {
+ assertTrue(d.getSupportedLanguages().contains("go"));
+ }
+
+ @Test
+ void deterministic() {
+ String code = """
+ r := gin.Default()
+ r.Use(cors)
+ r.GET("/a", a)
+ r.POST("/b", b)
+ r.PUT("/c/:id", c)
+ r.DELETE("/d/:id", d)
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx(code));
+ }
+
+ private static DetectorContext ctx(String content) {
+ return DetectorTestUtils.contextFor("go", content);
}
- @Test void noMatch() { assertEquals(0, d.detect(DetectorTestUtils.contextFor("go", "package main")).nodes().size()); }
- @Test void deterministic() { DetectorTestUtils.assertDeterministic(d, DetectorTestUtils.contextFor("go", "r := gin.Default()\nr.GET(\"/a\", a)\nr.POST(\"/b\", b)")); }
}
diff --git a/src/test/java/io/github/randomcodespace/iq/detector/java/JavaDetectorsCoverageTest.java b/src/test/java/io/github/randomcodespace/iq/detector/java/JavaDetectorsCoverageTest.java
new file mode 100644
index 00000000..e9ffa94b
--- /dev/null
+++ b/src/test/java/io/github/randomcodespace/iq/detector/java/JavaDetectorsCoverageTest.java
@@ -0,0 +1,1810 @@
+package io.github.randomcodespace.iq.detector.java;
+
+import io.github.randomcodespace.iq.detector.DetectorContext;
+import io.github.randomcodespace.iq.detector.DetectorResult;
+import io.github.randomcodespace.iq.detector.DetectorTestUtils;
+import io.github.randomcodespace.iq.model.EdgeKind;
+import io.github.randomcodespace.iq.model.NodeKind;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * Comprehensive coverage tests for Java detectors targeting branches not covered
+ * by JavaDetectorsTest and JavaDetectorsExtendedTest.
+ */
+class JavaDetectorsCoverageTest {
+
+ // ==================== KafkaDetector — deeper coverage ====================
+ @Nested
+ class KafkaDetectorCoverage {
+ private final KafkaDetector d = new KafkaDetector();
+
+ @Test
+ void returnsEmptyOnNullContent() {
+ var ctx = new DetectorContext("Test.java", "java", null);
+ assertTrue(d.detect(ctx).nodes().isEmpty());
+ }
+
+ @Test
+ void returnsEmptyOnEmptyContent() {
+ var ctx = new DetectorContext("Test.java", "java", "");
+ assertTrue(d.detect(ctx).nodes().isEmpty());
+ }
+
+ @Test
+ void returnsEmptyWhenNoKafkaKeywords() {
+ var ctx = ctx("java", "public class Foo { void bar() {} }");
+ assertTrue(d.detect(ctx).nodes().isEmpty());
+ }
+
+ @Test
+ void returnsEmptyWhenKafkaListenerButNoClass() {
+ // @KafkaListener present but no class declaration
+ var ctx = ctx("java", "@KafkaListener(topics = \"test\") void handle() {}");
+ assertTrue(d.detect(ctx).nodes().isEmpty());
+ }
+
+ @Test
+ void detectsKafkaListenerWithTopicsArray() {
+ String code = """
+ public class OrderConsumer {
+ @KafkaListener(topics = {"orders", "returns"})
+ public void onOrder(String msg) {}
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ // At minimum one CONSUMES edge should be found
+ assertFalse(r.edges().isEmpty());
+ }
+
+ @Test
+ void detectsKafkaListenerWithGroupId() {
+ String code = """
+ public class InventoryConsumer {
+ @KafkaListener(topics = "inventory", groupId = "inventory-group")
+ public void process(String msg) {}
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ assertFalse(r.edges().isEmpty());
+ var consumesEdge = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.CONSUMES)
+ .findFirst();
+ assertTrue(consumesEdge.isPresent());
+ assertEquals("inventory-group", consumesEdge.get().getProperties().get("group_id"));
+ }
+
+ @Test
+ void detectsMultipleTopicsBothConsumedAndProduced() {
+ String code = """
+ public class PaymentService {
+ @KafkaListener(topics = "payments")
+ public void onPayment(String msg) {}
+ @KafkaListener(topics = "refunds")
+ public void onRefund(String msg) {}
+ public void notify() { kafkaTemplate.send("notifications", "done"); }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertEquals(3, r.nodes().size(), "Should have 3 topic nodes");
+ long consumesCount = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.CONSUMES).count();
+ long producesCount = r.edges().stream()
+ .filter(e -> e.getKind() == EdgeKind.PRODUCES).count();
+ assertEquals(2, consumesCount);
+ assertEquals(1, producesCount);
+ }
+
+ @Test
+ void detectsKafkaListenerOnNextLineTopics() {
+ // Pattern where @KafkaListener is on one line, topics on next
+ String code = """
+ public class ShipmentConsumer {
+ @KafkaListener(
+ "shipments")
+ public void handle(String msg) {}
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ // May or may not detect — just ensure no exception
+ assertNotNull(r);
+ }
+
+ @Test
+ void isDeterministicWithMultipleTopics() {
+ String code = """
+ public class MultiConsumer {
+ @KafkaListener(topics = "topic1")
+ public void a(String m) {}
+ @KafkaListener(topics = "topic2")
+ public void b(String m) {}
+ public void produce() { kafkaTemplate.send("output", "x"); }
+ }
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx("java", code));
+ }
+
+ @Test
+ void getName() {
+ assertEquals("kafka", d.getName());
+ }
+
+ @Test
+ void getSupportedLanguages() {
+ assertThat(d.getSupportedLanguages()).contains("java", "kotlin");
+ }
+ }
+
+ // ==================== GrpcServiceDetector — deeper coverage ====================
+ @Nested
+ class GrpcDetectorCoverage {
+ private final GrpcServiceDetector d = new GrpcServiceDetector();
+
+ @Test
+ void returnsEmptyOnNullContent() {
+ assertTrue(d.detect(new DetectorContext("Test.java", "java", null)).nodes().isEmpty());
+ }
+
+ @Test
+ void returnsEmptyOnEmptyContent() {
+ assertTrue(d.detect(new DetectorContext("Test.java", "java", "")).nodes().isEmpty());
+ }
+
+ @Test
+ void detectsGrpcWithAnnotationOnly() {
+ String code = """
+ @GrpcService
+ public class MyServiceImpl extends MyServiceGrpc.MyServiceImplBase {
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ assertTrue(r.nodes().stream().anyMatch(n ->
+ n.getAnnotations().contains("@GrpcService")));
+ }
+
+ @Test
+ void detectsGrpcClientStub() {
+ String code = """
+ public class PaymentClient {
+ private final PaymentServiceGrpc.PaymentServiceBlockingStub stub;
+ public PaymentClient(ManagedChannel channel) {
+ stub = PaymentServiceGrpc.newBlockingStub(channel);
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.edges().isEmpty());
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CALLS));
+ }
+
+ @Test
+ void detectsMultipleRpcMethods() {
+ String code = """
+ @GrpcService
+ public class GreeterImpl extends GreeterGrpc.GreeterImplBase {
+ @Override
+ public void sayHello(HelloRequest request, StreamObserver responseObserver) {}
+ @Override
+ public void sayGoodbye(GoodbyeRequest request, StreamObserver responseObserver) {}
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ long rpcNodes = r.nodes().stream()
+ .filter(n -> n.getKind() == NodeKind.ENDPOINT).count();
+ assertTrue(rpcNodes >= 2, "Should detect at least 2 RPC method nodes");
+ }
+
+ @Test
+ void detectsBothImplAndStubInSameClass() {
+ String code = """
+ @GrpcService
+ public class GatewayImpl extends GatewayGrpc.GatewayImplBase {
+ @Override
+ public void route(RouteRequest req, StreamObserver obs) {
+ OrderServiceGrpc.newBlockingStub(channel).process(null);
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ boolean hasExposes = r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.EXPOSES);
+ boolean hasCalls = r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CALLS);
+ assertTrue(hasExposes, "Should have EXPOSES edge for service impl");
+ assertTrue(hasCalls, "Should have CALLS edge for client stub");
+ }
+
+ @Test
+ void detectsFutureStub() {
+ String code = """
+ public class AsyncClient {
+ void init() {
+ UserServiceGrpc.newFutureStub(channel);
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.CALLS));
+ }
+
+ @Test
+ void isDeterministic() {
+ String code = """
+ @GrpcService
+ public class OrderImpl extends OrderGrpc.OrderImplBase {
+ @Override
+ public void createOrder(OrderRequest req, StreamObserver obs) {}
+ }
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx("java", code));
+ }
+
+ @Test
+ void getName() {
+ assertEquals("grpc_service", d.getName());
+ }
+ }
+
+ // ==================== RabbitmqDetector — deeper coverage ====================
+ @Nested
+ class RabbitmqDetectorCoverage {
+ private final RabbitmqDetector d = new RabbitmqDetector();
+
+ @Test
+ void returnsEmptyOnNull() {
+ assertTrue(d.detect(new DetectorContext("Test.java", "java", null)).nodes().isEmpty());
+ }
+
+ @Test
+ void detectsRabbitListenerWithQueuesAttr() {
+ String code = """
+ public class OrderListener {
+ @RabbitListener(queues = "orders.queue")
+ public void listen(String msg) {}
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ assertTrue(r.nodes().stream().anyMatch(n -> n.getKind() == NodeKind.QUEUE));
+ }
+
+ @Test
+ void detectsDirectExchangeDeclaration() {
+ String code = """
+ public class RabbitConfig {
+ @Bean
+ public DirectExchange("orders-exchange") ordersExchange() {
+ return new DirectExchange("orders-exchange");
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsTopicExchangeDeclaration() {
+ String code = """
+ public class RabbitConfig {
+ @Bean
+ public TopicExchange topicExchange() {
+ return new TopicExchange("events-exchange");
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsFanoutExchangeDeclaration() {
+ String code = """
+ public class RabbitConfig {
+ @Bean
+ public FanoutExchange broadcastExchange() {
+ return new FanoutExchange("broadcast");
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsRabbitTemplateWithRoutingKey() {
+ String code = """
+ public class NotificationSender {
+ public void send(String msg) {
+ rabbitTemplate.convertAndSend("events-exchange", msg);
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ assertFalse(r.edges().isEmpty());
+ assertTrue(r.edges().stream().anyMatch(e -> e.getKind() == EdgeKind.PRODUCES));
+ }
+
+ @Test
+ void detectsMultipleQueuesAndExchanges() {
+ String code = """
+ public class MessageService {
+ @RabbitListener(queues = "orders")
+ public void onOrder(String m) {}
+ @RabbitListener(queues = "payments")
+ public void onPayment(String m) {}
+ public void publish() {
+ rabbitTemplate.convertAndSend("notifications", "done");
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ // Should have queue nodes for orders, payments, and exchange for notifications
+ assertTrue(r.nodes().size() >= 3);
+ }
+
+ @Test
+ void isDeterministic() {
+ String code = """
+ public class EventBus {
+ @RabbitListener(queues = "events")
+ public void consume(String msg) {}
+ public void emit() {
+ rabbitTemplate.convertAndSend("events-exchange", "data");
+ }
+ }
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx("java", code));
+ }
+
+ @Test
+ void getName() {
+ assertEquals("rabbitmq", d.getName());
+ }
+ }
+
+ // ==================== JdbcDetector — deeper coverage ====================
+ @Nested
+ class JdbcDetectorCoverage {
+ private final JdbcDetector d = new JdbcDetector();
+
+ @Test
+ void returnsEmptyOnNull() {
+ assertTrue(d.detect(new DetectorContext("Test.java", "java", null)).nodes().isEmpty());
+ }
+
+ @Test
+ void detectsPostgresJdbcUrl() {
+ String code = """
+ public class DbService {
+ void connect() {
+ DriverManager.getConnection("jdbc:postgresql://localhost:5432/mydb");
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ assertTrue(r.nodes().stream().anyMatch(n ->
+ "postgresql".equals(n.getProperties().get("db_type"))));
+ }
+
+ @Test
+ void detectsH2JdbcUrl() {
+ String code = """
+ public class TestDb {
+ void connect() {
+ DriverManager.getConnection("jdbc:h2:mem:testdb");
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsOracleJdbcUrl() {
+ String code = """
+ public class OracleService {
+ void connect() {
+ DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:orcl");
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsNamedParameterJdbcTemplate() {
+ String code = """
+ public class UserDao {
+ private final NamedParameterJdbcTemplate namedParameterJdbcTemplate;
+ public UserDao(NamedParameterJdbcTemplate template) {
+ this.namedParameterJdbcTemplate = template;
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsJdbcClientModern() {
+ String code = """
+ public class ModernDao {
+ private final JdbcClient jdbcClient;
+ public ModernDao(JdbcClient client) {
+ this.jdbcClient = client;
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsDataSourceBean() {
+ // DataSource @Bean alone doesn't create a DB node — it needs JdbcTemplate or DriverManager too
+ // but a class with @Bean and DataSource keyword is detected via DATASOURCE_BEAN_RE
+ String code = """
+ public class DataSourceConfig {
+ @Bean
+ public DataSource dataSource() {
+ return DataSourceBuilder.create().build();
+ }
+ private JdbcTemplate jdbcTemplate;
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void detectsStandaloneJdbcUrlString() {
+ String code = """
+ public class ConnectionFactory {
+ private static final String URL = "jdbc:mysql://db-server:3306/prod";
+ DataSource ds;
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ }
+
+ @Test
+ void isDeterministicWithMultipleSources() {
+ String code = """
+ public class MultiDbService {
+ private JdbcTemplate jdbcTemplate;
+ void connect() {
+ DriverManager.getConnection("jdbc:postgresql://host:5432/db");
+ }
+ }
+ """;
+ DetectorTestUtils.assertDeterministic(d, ctx("java", code));
+ }
+
+ @Test
+ void getName() {
+ assertEquals("jdbc", d.getName());
+ }
+ }
+
+ // ==================== RawSqlDetector — deeper coverage ====================
+ @Nested
+ class RawSqlDetectorCoverage {
+ private final RawSqlDetector d = new RawSqlDetector();
+
+ @Test
+ void returnsEmptyOnNull() {
+ assertTrue(d.detect(new DetectorContext("Test.java", "java", null)).nodes().isEmpty());
+ }
+
+ @Test
+ void detectsNativeQuery() {
+ String code = """
+ public interface ProductRepo extends JpaRepository {
+ @Query(value = "SELECT * FROM products WHERE category = ?1", nativeQuery = true)
+ List findByCategory(String cat);
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ assertTrue((Boolean) r.nodes().get(0).getProperties().get("native"));
+ }
+
+ @Test
+ void detectsJdbcTemplateQuery() {
+ String code = """
+ public class OrderDao {
+ JdbcTemplate jdbcTemplate;
+ public List findAll() {
+ return jdbcTemplate.query("SELECT id, name FROM orders WHERE active = 1",
+ rowMapper);
+ }
+ }
+ """;
+ var r = d.detect(ctx("java", code));
+ assertFalse(r.nodes().isEmpty());
+ assertEquals("jdbc_template", r.nodes().get(0).getProperties().get("source"));
+ }
+
+ @Test
+ void detectsEntityManagerNativeQuery() {
+ // createNativeQuery is on one line — the pattern requires it on the same line as the string
+ String code = """
+ public class ReportDao {
+ EntityManager entityManager;
+ public List