Skip to content

Commit 44bc685

Browse files
authored
Merge pull request #5 from drompincen/feature/claude-companion-features
Raise code coverage from 48.6% to 68.0%
2 parents 90000d8 + 271d231 commit 44bc685

13 files changed

Lines changed: 2950 additions & 6 deletions

drom-plans/coverage-65.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
---
2+
title: Code Coverage to 65%
3+
status: completed
4+
created: 2026-03-28
5+
updated: 2026-03-28
6+
current_chapter: 1
7+
loop: true
8+
loop_target: 65.0
9+
loop_metric: instruction_coverage_percent
10+
loop_max_iterations: 5
11+
---
12+
13+
# Plan: Code Coverage to 65%
14+
15+
Raise JaCoCo instruction coverage from **48.6%** to **≥65%** using a closed-loop approach: write tests, measure, repeat until target is met.
16+
17+
**Baseline (2026-03-28):** 10,627 / 21,869 instructions covered (48.6%), 186 tests passing.
18+
19+
**Strategy:** Target the classes with the highest uncovered instruction count first (biggest bang per test). Skip CLI classes (`JavaDuckerClient`, `InteractiveCli`, `CommandDispatcher`, etc.) — they are thin wrappers over REST calls and hard to unit-test without major infra. Focus on service/ingestion/REST classes.
20+
21+
## Priority targets (by uncovered instructions, descending)
22+
23+
| Class | Covered | Total | Gap | Instructions to cover |
24+
|-------|---------|-------|-----|----------------------|
25+
| ReladomoQueryService | 684 | 1507 | 45% | 823 |
26+
| HnswIndex | 0 | 853 | 0% | 853 |
27+
| JavaDuckerRestController | 633 | 1191 | 53% | 558 |
28+
| TextExtractor | 668 | 1235 | 54% | 567 |
29+
| ExplainService | 52 | 499 | 10% | 447 |
30+
| IngestionWorker | 701 | 1249 | 56% | 548 |
31+
| StalenessService | 43 | 307 | 14% | 264 |
32+
| CoChangeService | 245 | 530 | 46% | 285 |
33+
| ProjectMapService | 0 | 273 | 0% | 273 |
34+
| FileWatcher | 13 | 282 | 5% | 269 |
35+
| DependencyService | 0 | 127 | 0% | 127 |
36+
| ReladomoConfigParser | 7 | 244 | 3% | 237 |
37+
| GitBlameService | 262 | 457 | 57% | 195 |
38+
| ImportParser | 91 | 175 | 52% | 84 |
39+
40+
**Need:** ~3,550 more instructions covered to reach 65% (14,215 / 21,869).
41+
42+
## Chapter 1: High-Impact Service Tests (target: ~55%)
43+
**Status:** completed
44+
**Depends on:** none
45+
46+
Write tests for the services with highest uncovered instruction count that can be tested with DuckDB in-memory:
47+
48+
- [ ] `ExplainServiceTest` — expand: test `explain()` and `explainByPath()` with a real DuckDB + seeded artifacts (not just static helpers). Cover classification, tags, salient_points, related_artifacts sections. Target: 10% → 70%
49+
- [ ] `StalenessServiceTest` — expand: test `checkStaleness()` and `checkAll()` with real DuckDB + temp files on disk. Cover file-exists, file-missing, file-modified paths. Target: 14% → 70%
50+
- [ ] `DependencyServiceTest` — new: test `getDependencies()` and `getDependents()` with seeded `artifact_imports` data. Target: 0% → 80%
51+
- [ ] `CoChangeServiceTest` — expand: test `buildCoChangeIndex()` and `getRelatedFiles()` with real DuckDB. Target: 46% → 70%
52+
- [ ] `ProjectMapServiceTest` — new: test `getProjectMap()` with seeded artifacts. Target: 0% → 60%
53+
54+
**Measure:** Run `mvn verify -B`, parse `jacoco.csv`, check if ≥55%.
55+
56+
## Chapter 2: Ingestion & Parser Tests (target: ~60%)
57+
**Status:** completed
58+
**Depends on:** Chapter 1
59+
60+
- [ ] `HnswIndexTest` — new: test `add()`, `search()`, `isEmpty()`, `size()`, `buildIndex()` with synthetic embeddings. HnswIndex is 853 uncovered instructions — biggest single-class gap. Target: 0% → 60%
61+
- [ ] `ImportParserTest` — new/expand: test Java import parsing, XML namespace extraction, edge cases. Target: 52% → 80%
62+
- [ ] `ReladomoConfigParserTest` — new: test parsing of Reladomo runtime config XML. Target: 3% → 60%
63+
- [ ] `FileWatcherTest` — new: test start/stop/status with temp directory. Target: 5% → 40%
64+
- [ ] `TextExtractorTest` — expand: test more file types (HTML, XML, plain text). Target: 54% → 70%
65+
66+
**Measure:** Run `mvn verify -B`, parse `jacoco.csv`, check if ≥60%.
67+
68+
## Chapter 3: REST Controller + Integration (target: ~65%)
69+
**Status:** completed
70+
**Depends on:** Chapter 2
71+
72+
- [ ] `JavaDuckerRestControllerTest` — expand: cover newly added endpoints (stale/summary, related, blame, explain) and uncovered existing endpoints (map, watch/start, watch/stop, dependencies, dependents, content intelligence write endpoints)
73+
- [ ] `IngestionWorkerTest` — new: test the ingestion pipeline with a real file through upload → parse → chunk → embed → index. Target: 56% → 70%
74+
- [ ] `ReladomoQueryServiceTest` — expand: cover uncovered query methods (getGraph, getPath, getSchema, getObjectFiles, getFinderPatterns, getDeepFetchProfiles, getTemporalInfo, getConfig). Target: 45% → 65%
75+
76+
**Measure:** Run `mvn verify -B`, parse `jacoco.csv`, check if ≥65%. If not, identify remaining gaps and add targeted tests.
77+
78+
---
79+
80+
## Closed-Loop Protocol
81+
82+
After each chapter:
83+
84+
1. Run `mvn verify -B`
85+
2. Parse `target/site/jacoco/jacoco.csv` → compute instruction coverage %
86+
3. If coverage ≥ 65%: **STOP** — mark plan completed
87+
4. If coverage < 65% and chapters remain: proceed to next chapter
88+
5. If coverage < 65% and all chapters done: identify the top 5 uncovered classes, write targeted tests, re-measure (max 2 extra iterations)
89+
6. Log each iteration: coverage %, delta, tests added
90+
91+
## Exclusions
92+
93+
Do NOT write tests for these CLI/UI classes (low ROI, hard to test):
94+
- `JavaDuckerClient` and all nested `*Cmd` classes
95+
- `InteractiveCli`, `CommandDispatcher`, `SearchCommand`, `CatCommand`, `IndexCommand`, `StatsCommand`, `StatusCommand`
96+
- `ResultsFormatter`, `ProgressBar`, `StatsPanel`, `Theme`
97+
- `ApiClient`
98+
99+
---
Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
package com.javaducker.server.ingestion;
2+
3+
import com.javaducker.server.config.AppConfig;
4+
import com.javaducker.server.db.DuckDBDataSource;
5+
import com.javaducker.server.db.SchemaBootstrap;
6+
import com.javaducker.server.service.ArtifactService;
7+
import com.javaducker.server.service.ReladomoService;
8+
import com.javaducker.server.service.SearchService;
9+
import com.javaducker.server.service.UploadService;
10+
import org.junit.jupiter.api.*;
11+
import org.junit.jupiter.api.io.TempDir;
12+
13+
import java.io.IOException;
14+
import java.nio.file.Files;
15+
import java.nio.file.Path;
16+
import java.sql.PreparedStatement;
17+
import java.sql.ResultSet;
18+
import java.util.Set;
19+
20+
import static org.junit.jupiter.api.Assertions.*;
21+
22+
class FileWatcherTest {
23+
24+
@TempDir
25+
Path tempDir;
26+
27+
private DuckDBDataSource dataSource;
28+
private UploadService uploadService;
29+
private FileWatcher fileWatcher;
30+
31+
@BeforeEach
32+
void setUp() throws Exception {
33+
AppConfig config = new AppConfig();
34+
config.setDbPath(tempDir.resolve("test.duckdb").toString());
35+
config.setIntakeDir(tempDir.resolve("intake").toString());
36+
config.setChunkSize(200);
37+
config.setChunkOverlap(50);
38+
config.setEmbeddingDim(64);
39+
config.setIngestionWorkerThreads(1);
40+
41+
dataSource = new DuckDBDataSource(config);
42+
ArtifactService artifactService = new ArtifactService(dataSource);
43+
uploadService = new UploadService(dataSource, config, artifactService);
44+
SearchService searchService = new SearchService(dataSource, new EmbeddingService(config), config);
45+
IngestionWorker worker = new IngestionWorker(dataSource, artifactService,
46+
new TextExtractor(), new TextNormalizer(), new Chunker(),
47+
new EmbeddingService(config), new FileSummarizer(), new ImportParser(),
48+
new ReladomoXmlParser(), new ReladomoService(dataSource),
49+
new ReladomoFinderParser(), new ReladomoConfigParser(),
50+
searchService, config);
51+
52+
SchemaBootstrap bootstrap = new SchemaBootstrap(dataSource, config, worker);
53+
bootstrap.bootstrap();
54+
55+
fileWatcher = new FileWatcher(uploadService);
56+
}
57+
58+
@AfterEach
59+
void tearDown() {
60+
fileWatcher.stopWatching();
61+
dataSource.close();
62+
}
63+
64+
@Test
65+
void isWatchingInitiallyFalse() {
66+
assertFalse(fileWatcher.isWatching());
67+
}
68+
69+
@Test
70+
void getWatchedDirectoryInitiallyNull() {
71+
assertNull(fileWatcher.getWatchedDirectory());
72+
}
73+
74+
@Test
75+
void startAndStop() throws IOException {
76+
Path watchDir = tempDir.resolve("watched");
77+
Files.createDirectory(watchDir);
78+
79+
fileWatcher.startWatching(watchDir, Set.of());
80+
81+
assertTrue(fileWatcher.isWatching(), "Should be watching after startWatching");
82+
assertEquals(watchDir, fileWatcher.getWatchedDirectory());
83+
84+
fileWatcher.stopWatching();
85+
86+
assertFalse(fileWatcher.isWatching(), "Should not be watching after stopWatching");
87+
assertNull(fileWatcher.getWatchedDirectory(), "Watched directory should be null after stop");
88+
}
89+
90+
@Test
91+
void startWatchingSetsDirectory() throws IOException {
92+
Path watchDir = tempDir.resolve("watched2");
93+
Files.createDirectory(watchDir);
94+
95+
fileWatcher.startWatching(watchDir, Set.of(".java", ".txt"));
96+
97+
assertEquals(watchDir, fileWatcher.getWatchedDirectory());
98+
}
99+
100+
@Test
101+
void stopWhenNotWatching() {
102+
assertDoesNotThrow(() -> fileWatcher.stopWatching());
103+
assertFalse(fileWatcher.isWatching());
104+
}
105+
106+
@Test
107+
void startWatchingTwiceRestartsCleanly() throws IOException {
108+
Path dir1 = tempDir.resolve("dir1");
109+
Path dir2 = tempDir.resolve("dir2");
110+
Files.createDirectory(dir1);
111+
Files.createDirectory(dir2);
112+
113+
fileWatcher.startWatching(dir1, Set.of());
114+
assertTrue(fileWatcher.isWatching());
115+
assertEquals(dir1, fileWatcher.getWatchedDirectory());
116+
117+
fileWatcher.startWatching(dir2, Set.of(".java"));
118+
assertTrue(fileWatcher.isWatching());
119+
assertEquals(dir2, fileWatcher.getWatchedDirectory());
120+
}
121+
122+
@Test
123+
void startWatchingWithEmptyExtensions() throws IOException {
124+
Path watchDir = tempDir.resolve("watched3");
125+
Files.createDirectory(watchDir);
126+
127+
fileWatcher.startWatching(watchDir, Set.of());
128+
assertTrue(fileWatcher.isWatching());
129+
}
130+
131+
@Test
132+
void stopWatchingClearsState() throws IOException {
133+
Path watchDir = tempDir.resolve("watched4");
134+
Files.createDirectory(watchDir);
135+
136+
fileWatcher.startWatching(watchDir, Set.of());
137+
assertTrue(fileWatcher.isWatching());
138+
assertNotNull(fileWatcher.getWatchedDirectory());
139+
140+
fileWatcher.stopWatching();
141+
assertFalse(fileWatcher.isWatching());
142+
assertNull(fileWatcher.getWatchedDirectory());
143+
}
144+
145+
@Test
146+
void watcherDetectsNewFile() throws Exception {
147+
Path watchDir = tempDir.resolve("detect");
148+
Files.createDirectory(watchDir);
149+
150+
fileWatcher.startWatching(watchDir, Set.of());
151+
152+
// Create a file in the watched directory
153+
Path testFile = watchDir.resolve("test.txt");
154+
Files.writeString(testFile, "Hello, FileWatcher!");
155+
156+
// Give the watcher thread time to pick up the event and process it
157+
Thread.sleep(3000);
158+
159+
// Verify an artifact was created in the database
160+
long count = dataSource.withConnection(conn -> {
161+
try (PreparedStatement ps = conn.prepareStatement(
162+
"SELECT COUNT(*) FROM artifacts WHERE file_name = 'test.txt'")) {
163+
try (ResultSet rs = ps.executeQuery()) {
164+
rs.next();
165+
return rs.getLong(1);
166+
}
167+
}
168+
});
169+
assertTrue(count >= 1, "FileWatcher should have uploaded the detected file, artifact count: " + count);
170+
}
171+
172+
@Test
173+
void watcherSkipsExcludedDirs() throws Exception {
174+
Path watchDir = tempDir.resolve("excluded");
175+
Files.createDirectory(watchDir);
176+
Path gitDir = watchDir.resolve(".git");
177+
Files.createDirectories(gitDir);
178+
179+
fileWatcher.startWatching(watchDir, Set.of());
180+
181+
// Create a file inside an excluded directory
182+
Files.writeString(gitDir.resolve("config"), "excluded content");
183+
184+
Thread.sleep(2000);
185+
186+
// File inside .git should not trigger upload
187+
long count = dataSource.withConnection(conn -> {
188+
try (PreparedStatement ps = conn.prepareStatement(
189+
"SELECT COUNT(*) FROM artifacts WHERE file_name = 'config'")) {
190+
try (ResultSet rs = ps.executeQuery()) {
191+
rs.next();
192+
return rs.getLong(1);
193+
}
194+
}
195+
});
196+
assertEquals(0, count, "File inside .git should not be uploaded");
197+
}
198+
199+
@Test
200+
void watcherRespectsExtensionFilter() throws Exception {
201+
Path watchDir = tempDir.resolve("filter");
202+
Files.createDirectory(watchDir);
203+
204+
fileWatcher.startWatching(watchDir, Set.of(".java"));
205+
206+
Files.writeString(watchDir.resolve("ignored.txt"), "should be ignored");
207+
Files.writeString(watchDir.resolve("Main.java"), "public class Main {}");
208+
209+
Thread.sleep(3000);
210+
211+
long txtCount = dataSource.withConnection(conn -> {
212+
try (PreparedStatement ps = conn.prepareStatement(
213+
"SELECT COUNT(*) FROM artifacts WHERE file_name = 'ignored.txt'")) {
214+
try (ResultSet rs = ps.executeQuery()) {
215+
rs.next();
216+
return rs.getLong(1);
217+
}
218+
}
219+
});
220+
221+
long javaCount = dataSource.withConnection(conn -> {
222+
try (PreparedStatement ps = conn.prepareStatement(
223+
"SELECT COUNT(*) FROM artifacts WHERE file_name = 'Main.java'")) {
224+
try (ResultSet rs = ps.executeQuery()) {
225+
rs.next();
226+
return rs.getLong(1);
227+
}
228+
}
229+
});
230+
231+
assertEquals(0, txtCount, ".txt file should be filtered out when only .java is allowed");
232+
assertTrue(javaCount >= 1, ".java file should be uploaded when .java extension is allowed");
233+
}
234+
}

0 commit comments

Comments
 (0)