|
| 1 | +package io.github.randomcodespace.iq.intelligence; |
| 2 | + |
| 3 | +import io.github.randomcodespace.iq.analyzer.Analyzer; |
| 4 | +import io.github.randomcodespace.iq.analyzer.AnalysisResult; |
| 5 | +import io.github.randomcodespace.iq.model.CodeNode; |
| 6 | +import org.junit.jupiter.api.Test; |
| 7 | +import org.junit.jupiter.api.io.TempDir; |
| 8 | +import org.springframework.beans.factory.annotation.Autowired; |
| 9 | +import org.springframework.boot.test.context.SpringBootTest; |
| 10 | +import org.springframework.test.context.ActiveProfiles; |
| 11 | + |
| 12 | +import java.nio.file.Files; |
| 13 | +import java.nio.file.Path; |
| 14 | +import java.util.List; |
| 15 | + |
| 16 | +import static org.assertj.core.api.Assertions.assertThat; |
| 17 | +import static org.assertj.core.api.Assertions.assertThatCode; |
| 18 | + |
| 19 | +/** |
| 20 | + * Edge-case tests for provenance pipeline. |
| 21 | + * Covers: empty repos, single-file repos, unsupported-language-only repos, |
| 22 | + * mixed-language repos, and repos with no git history. |
| 23 | + */ |
| 24 | +@SpringBootTest |
| 25 | +@ActiveProfiles("indexing") |
| 26 | +class ProvenanceEdgeCasesTest { |
| 27 | + |
| 28 | + @Autowired |
| 29 | + Analyzer analyzer; |
| 30 | + |
| 31 | + // ------------------------------------------------------------------ |
| 32 | + // Empty repo — no source files, pipeline should not throw |
| 33 | + // ------------------------------------------------------------------ |
| 34 | + |
| 35 | + @Test |
| 36 | + void emptyDirectory_producesNoNodes_noException(@TempDir Path dir) { |
| 37 | + assertThatCode(() -> { |
| 38 | + AnalysisResult result = analyzer.run(dir, msg -> {}); |
| 39 | + assertThat(result).isNotNull(); |
| 40 | + // Empty dir may produce zero nodes — that is acceptable |
| 41 | + }).doesNotThrowAnyException(); |
| 42 | + } |
| 43 | + |
| 44 | + // ------------------------------------------------------------------ |
| 45 | + // Single-file repo — exactly one Java file |
| 46 | + // ------------------------------------------------------------------ |
| 47 | + |
| 48 | + @Test |
| 49 | + void singleJavaFile_allNodesHaveProvenance(@TempDir Path dir) throws Exception { |
| 50 | + Path src = dir.resolve("Greeter.java"); |
| 51 | + Files.writeString(src, """ |
| 52 | + public class Greeter { |
| 53 | + public String greet(String name) { |
| 54 | + return "Hello, " + name; |
| 55 | + } |
| 56 | + } |
| 57 | + """); |
| 58 | + |
| 59 | + AnalysisResult result = analyzer.run(dir, msg -> {}); |
| 60 | + assertThat(result.nodes()).isNotEmpty(); |
| 61 | + |
| 62 | + for (CodeNode node : result.nodes()) { |
| 63 | + assertThat(node.getProvenance()) |
| 64 | + .as("Node %s must carry provenance", node.getId()) |
| 65 | + .isNotNull(); |
| 66 | + assertThat(node.getProvenance().extractorVersion()).isNotBlank(); |
| 67 | + assertThat(node.getProvenance().confidence()).isNotNull(); |
| 68 | + } |
| 69 | + } |
| 70 | + |
| 71 | + // ------------------------------------------------------------------ |
| 72 | + // Unsupported-language-only repo — e.g., only .rb files |
| 73 | + // Pipeline should complete gracefully; any nodes produced must have provenance. |
| 74 | + // ------------------------------------------------------------------ |
| 75 | + |
| 76 | + @Test |
| 77 | + void unsupportedLanguageOnly_completesGracefully(@TempDir Path dir) throws Exception { |
| 78 | + Files.writeString(dir.resolve("script.rb"), "puts 'hello world'"); |
| 79 | + Files.writeString(dir.resolve("helper.brainfuck"), "++++++++[>++++["); |
| 80 | + |
| 81 | + assertThatCode(() -> { |
| 82 | + AnalysisResult result = analyzer.run(dir, msg -> {}); |
| 83 | + assertThat(result).isNotNull(); |
| 84 | + // Any nodes emitted for unsupported languages must still carry provenance |
| 85 | + for (CodeNode node : result.nodes()) { |
| 86 | + assertThat(node.getProvenance()) |
| 87 | + .as("Node %s from unsupported-language file must have provenance", node.getId()) |
| 88 | + .isNotNull(); |
| 89 | + } |
| 90 | + }).doesNotThrowAnyException(); |
| 91 | + } |
| 92 | + |
| 93 | + // ------------------------------------------------------------------ |
| 94 | + // Mixed-language repo — Java + TypeScript + Python + Go |
| 95 | + // Every node must carry provenance regardless of language. |
| 96 | + // ------------------------------------------------------------------ |
| 97 | + |
| 98 | + @Test |
| 99 | + void mixedLanguageRepo_allNodesHaveProvenance(@TempDir Path dir) throws Exception { |
| 100 | + // Java |
| 101 | + Path javaDir = dir.resolve("src/main/java/com/example"); |
| 102 | + Files.createDirectories(javaDir); |
| 103 | + Files.writeString(javaDir.resolve("UserService.java"), """ |
| 104 | + package com.example; |
| 105 | + public class UserService { |
| 106 | + public String findUser(String id) { return id; } |
| 107 | + } |
| 108 | + """); |
| 109 | + |
| 110 | + // TypeScript |
| 111 | + Path tsDir = dir.resolve("frontend/src"); |
| 112 | + Files.createDirectories(tsDir); |
| 113 | + Files.writeString(tsDir.resolve("api.ts"), """ |
| 114 | + export interface User { id: string; name: string; } |
| 115 | + export function fetchUser(id: string): Promise<User> { |
| 116 | + return fetch(`/api/users/${id}`).then(r => r.json()); |
| 117 | + } |
| 118 | + """); |
| 119 | + |
| 120 | + // Python |
| 121 | + Path pyDir = dir.resolve("scripts"); |
| 122 | + Files.createDirectories(pyDir); |
| 123 | + Files.writeString(pyDir.resolve("process.py"), """ |
| 124 | + def process_data(items: list) -> list: |
| 125 | + return [item for item in items if item] |
| 126 | + """); |
| 127 | + |
| 128 | + // Go |
| 129 | + Path goDir = dir.resolve("cmd"); |
| 130 | + Files.createDirectories(goDir); |
| 131 | + Files.writeString(goDir.resolve("main.go"), """ |
| 132 | + package main |
| 133 | + import "fmt" |
| 134 | + func main() { |
| 135 | + fmt.Println("hello") |
| 136 | + } |
| 137 | + """); |
| 138 | + |
| 139 | + AnalysisResult result = analyzer.run(dir, msg -> {}); |
| 140 | + List<CodeNode> nodes = result.nodes(); |
| 141 | + assertThat(nodes).isNotEmpty(); |
| 142 | + |
| 143 | + for (CodeNode node : nodes) { |
| 144 | + assertThat(node.getProvenance()) |
| 145 | + .as("Node %s (%s) must carry provenance", node.getId(), node.getFilePath()) |
| 146 | + .isNotNull(); |
| 147 | + assertThat(node.getProvenance().schemaVersion()) |
| 148 | + .as("Node %s must have schemaVersion >= 1", node.getId()) |
| 149 | + .isGreaterThanOrEqualTo(1); |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + // ------------------------------------------------------------------ |
| 154 | + // No git history — plain directory, not initialised as a git repo. |
| 155 | + // commitSha and repoUrl in provenance should be null, not throw. |
| 156 | + // ------------------------------------------------------------------ |
| 157 | + |
| 158 | + @Test |
| 159 | + void noGitHistory_provenanceHasNullGitFields(@TempDir Path dir) throws Exception { |
| 160 | + Path src = dir.resolve("App.java"); |
| 161 | + Files.writeString(src, """ |
| 162 | + public class App { |
| 163 | + public static void main(String[] args) {} |
| 164 | + } |
| 165 | + """); |
| 166 | + |
| 167 | + AnalysisResult result = analyzer.run(dir, msg -> {}); |
| 168 | + assertThat(result.nodes()).isNotEmpty(); |
| 169 | + |
| 170 | + for (CodeNode node : result.nodes()) { |
| 171 | + Provenance prov = node.getProvenance(); |
| 172 | + assertThat(prov).isNotNull(); |
| 173 | + // No git repo → commitSha and repoUrl must be null |
| 174 | + assertThat(prov.commitSha()) |
| 175 | + .as("No git repo — commitSha should be null for node %s", node.getId()) |
| 176 | + .isNull(); |
| 177 | + assertThat(prov.repositoryUrl()) |
| 178 | + .as("No git repo — repositoryUrl should be null for node %s", node.getId()) |
| 179 | + .isNull(); |
| 180 | + // extractorVersion and schemaVersion must still be populated |
| 181 | + assertThat(prov.extractorVersion()).isNotBlank(); |
| 182 | + assertThat(prov.schemaVersion()).isGreaterThanOrEqualTo(1); |
| 183 | + } |
| 184 | + } |
| 185 | + |
| 186 | + // ------------------------------------------------------------------ |
| 187 | + // Mixed-language determinism — same mixed-language repo analysed twice |
| 188 | + // ------------------------------------------------------------------ |
| 189 | + |
| 190 | + @Test |
| 191 | + void mixedLanguageRepo_deterministicProvenance(@TempDir Path dir) throws Exception { |
| 192 | + Files.writeString(dir.resolve("Service.java"), "public class Service {}"); |
| 193 | + Files.writeString(dir.resolve("index.ts"), "export const x = 1;"); |
| 194 | + Files.writeString(dir.resolve("util.py"), "def helper(): pass"); |
| 195 | + |
| 196 | + AnalysisResult r1 = analyzer.run(dir, msg -> {}); |
| 197 | + AnalysisResult r2 = analyzer.run(dir, msg -> {}); |
| 198 | + |
| 199 | + assertThat(r1.nodes()).hasSameSizeAs(r2.nodes()); |
| 200 | + for (int i = 0; i < r1.nodes().size(); i++) { |
| 201 | + Provenance p1 = r1.nodes().get(i).getProvenance(); |
| 202 | + Provenance p2 = r2.nodes().get(i).getProvenance(); |
| 203 | + assertThat(p1).isNotNull(); |
| 204 | + assertThat(p2).isNotNull(); |
| 205 | + assertThat(p1.extractorVersion()).isEqualTo(p2.extractorVersion()); |
| 206 | + assertThat(p1.schemaVersion()).isEqualTo(p2.schemaVersion()); |
| 207 | + assertThat(p1.confidence()).isEqualTo(p2.confidence()); |
| 208 | + } |
| 209 | + } |
| 210 | +} |
0 commit comments