Skip to content

Commit c855019

Browse files
test(intelligence): Phase 1-3 test execution — edge cases, cross-language, RepositoryIdentity (RAN-159)
- RepositoryIdentityTest (8 tests): non-git dir graceful null, commit SHA on git repo, detached HEAD branch normalised to null, record equality/null safety - ProvenanceEdgeCasesTest (6 tests): empty dir, single-file, unsupported-language-only, mixed-language (Java/TS/Python/Go), no-git-history null provenance fields, mixed-language determinism - LexicalCrossLanguageTest (11 tests): TypeScript/JavaScript block comments, Python triple-quoted docstrings (single-line and multiline), Go line comments, cross-language determinism, DocCommentExtractor direct calls All 1616 tests pass (0 failures, 0 errors, 31 skipped). Co-Authored-By: Paperclip <noreply@paperclip.ing>
1 parent cc08698 commit c855019

3 files changed

Lines changed: 621 additions & 0 deletions

File tree

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
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+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
package io.github.randomcodespace.iq.intelligence;
2+
3+
import org.junit.jupiter.api.Test;
4+
import org.junit.jupiter.api.io.TempDir;
5+
6+
import java.nio.file.Path;
7+
8+
import static org.assertj.core.api.Assertions.assertThat;
9+
import static org.assertj.core.api.Assertions.assertThatCode;
10+
11+
/**
12+
* Unit tests for {@link RepositoryIdentity}.
13+
* Validates graceful degradation when git metadata is unavailable.
14+
*/
15+
class RepositoryIdentityTest {
16+
17+
// ------------------------------------------------------------------
18+
// Non-git directory — all git fields null, no exception
19+
// ------------------------------------------------------------------
20+
21+
@Test
22+
void resolve_nonGitDirectory_allNullNoException(@TempDir Path dir) {
23+
assertThatCode(() -> RepositoryIdentity.resolve(dir)).doesNotThrowAnyException();
24+
25+
RepositoryIdentity id = RepositoryIdentity.resolve(dir);
26+
assertThat(id.repoUrl()).isNull();
27+
assertThat(id.commitSha()).isNull();
28+
assertThat(id.branch()).isNull();
29+
}
30+
31+
@Test
32+
void resolve_nonGitDirectory_buildTimestampAlwaysPresent(@TempDir Path dir) {
33+
RepositoryIdentity id = RepositoryIdentity.resolve(dir);
34+
assertThat(id.buildTimestamp()).isNotNull();
35+
}
36+
37+
// ------------------------------------------------------------------
38+
// Git repo with no remote — commitSha present, repoUrl null
39+
// ------------------------------------------------------------------
40+
41+
@Test
42+
void resolve_gitRepoNoRemote_commitShaPopulated(@TempDir Path dir) throws Exception {
43+
initGitRepo(dir);
44+
45+
RepositoryIdentity id = RepositoryIdentity.resolve(dir);
46+
// commit SHA may be null if no commits yet, but should not throw
47+
assertThat(id.repoUrl()).isNull();
48+
assertThat(id.buildTimestamp()).isNotNull();
49+
}
50+
51+
@Test
52+
void resolve_gitRepoWithCommit_commitShaPresent(@TempDir Path dir) throws Exception {
53+
initGitRepo(dir);
54+
makeInitialCommit(dir);
55+
56+
RepositoryIdentity id = RepositoryIdentity.resolve(dir);
57+
assertThat(id.commitSha()).isNotNull().isNotBlank();
58+
assertThat(id.repoUrl()).isNull();
59+
}
60+
61+
// ------------------------------------------------------------------
62+
// Detached HEAD — branch normalised to null
63+
// ------------------------------------------------------------------
64+
65+
@Test
66+
void resolve_detachedHead_branchIsNull(@TempDir Path dir) throws Exception {
67+
initGitRepo(dir);
68+
makeInitialCommit(dir);
69+
70+
// Detach HEAD by checking out the commit SHA directly
71+
String sha = runGit(dir, "rev-parse", "HEAD");
72+
runGit(dir, "checkout", "--detach", sha);
73+
74+
RepositoryIdentity id = RepositoryIdentity.resolve(dir);
75+
assertThat(id.branch()).isNull();
76+
assertThat(id.commitSha()).isNotNull();
77+
}
78+
79+
// ------------------------------------------------------------------
80+
// Record semantics
81+
// ------------------------------------------------------------------
82+
83+
@Test
84+
void record_constructorAndAccessors() {
85+
var ts = java.time.Instant.now();
86+
var id = new RepositoryIdentity("https://github.com/x/y", "abc123", "main", ts);
87+
88+
assertThat(id.repoUrl()).isEqualTo("https://github.com/x/y");
89+
assertThat(id.commitSha()).isEqualTo("abc123");
90+
assertThat(id.branch()).isEqualTo("main");
91+
assertThat(id.buildTimestamp()).isEqualTo(ts);
92+
}
93+
94+
@Test
95+
void record_equalityOnSameValues() {
96+
var ts = java.time.Instant.parse("2026-01-01T00:00:00Z");
97+
var id1 = new RepositoryIdentity("url", "sha", "main", ts);
98+
var id2 = new RepositoryIdentity("url", "sha", "main", ts);
99+
assertThat(id1).isEqualTo(id2);
100+
}
101+
102+
@Test
103+
void record_nullFieldsAllowed() {
104+
assertThatCode(() -> new RepositoryIdentity(null, null, null, java.time.Instant.now()))
105+
.doesNotThrowAnyException();
106+
}
107+
108+
// ------------------------------------------------------------------
109+
// Helpers
110+
// ------------------------------------------------------------------
111+
112+
private static void initGitRepo(Path dir) throws Exception {
113+
run(dir, "git", "init");
114+
run(dir, "git", "config", "user.email", "test@test.com");
115+
run(dir, "git", "config", "user.name", "Test");
116+
}
117+
118+
private static void makeInitialCommit(Path dir) throws Exception {
119+
Path readme = dir.resolve("README.md");
120+
java.nio.file.Files.writeString(readme, "# Test");
121+
run(dir, "git", "add", ".");
122+
run(dir, "git", "commit", "-m", "init");
123+
}
124+
125+
private static String runGit(Path dir, String... args) throws Exception {
126+
var cmd = new java.util.ArrayList<String>();
127+
cmd.add("git");
128+
cmd.addAll(java.util.Arrays.asList(args));
129+
var proc = new ProcessBuilder(cmd).directory(dir.toFile()).start();
130+
String out = new String(proc.getInputStream().readAllBytes()).trim();
131+
proc.waitFor();
132+
return out;
133+
}
134+
135+
private static void run(Path dir, String... cmd) throws Exception {
136+
new ProcessBuilder(cmd).directory(dir.toFile())
137+
.redirectErrorStream(true).start().waitFor();
138+
}
139+
}

0 commit comments

Comments
 (0)