Skip to content

Commit 160deb4

Browse files
committed
bugfix: filter duplicate detection to open issues only
1 parent b9a2337 commit 160deb4

5 files changed

Lines changed: 199 additions & 16 deletions

File tree

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
{
2-
"status": "open",
2+
"status": "closed",
3+
"resolution": "implemented",
4+
"target_branch": "v2.1",
35
"dependencies": [],
46
"blocks": []
57
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"status" : "open"
3+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# Plan
2+
3+
## Goal
4+
5+
Remove unnecessary @SuppressWarnings("try") annotations from test methods that don't explicitly call scope.close() inside try-with-resources blocks. The suppression is only needed when close() is called explicitly inside a try-with-resources block (to suppress the "already auto-closed" compiler warning). Methods that simply use the scope normally in try-with-resources don't generate this warning and don't need the annotation.
6+
7+
## Type
8+
9+
refactor
10+
11+
## Pre-conditions
12+
13+
- [ ] All dependent issues are closed
14+
15+
## Post-conditions
16+
17+
- [ ] User-visible behavior unchanged
18+
- [ ] Tests passing — all tests compile and pass after annotation removal
19+
- [ ] Code quality improved — unnecessary @SuppressWarnings("try") annotations removed
20+
- [ ] E2E verification — full test suite passes (mvn verify) after changes

client/src/main/java/io/github/cowwoc/cat/hooks/skills/GetAddOutput.java

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import java.io.IOException;
2020
import java.io.PrintStream;
2121

22-
import static io.github.cowwoc.cat.hooks.Strings.block;
22+
import java.nio.file.DirectoryStream;
2323
import java.nio.file.Files;
2424
import java.nio.file.Path;
2525
import java.util.ArrayList;
@@ -28,6 +28,7 @@
2828
import java.util.Objects;
2929
import java.util.stream.Stream;
3030

31+
import static io.github.cowwoc.cat.hooks.Strings.block;
3132
import static io.github.cowwoc.requirements13.java.DefaultJavaValidators.requireThat;
3233

3334
/**
@@ -215,15 +216,42 @@ private VersionData readVersionData(Path versionDir) throws IOException
215216
summary = parseGoalSummary(planContent);
216217
}
217218

218-
try (Stream<Path> entries = Files.list(versionDir))
219+
List<String> existingIssues = new ArrayList<>();
220+
List<Path> issueDirs = new ArrayList<>();
221+
try (DirectoryStream<Path> entries = Files.newDirectoryStream(versionDir))
219222
{
220-
List<String> existingIssues = entries.
221-
filter(Files::isDirectory).
222-
map(p -> p.getFileName().toString()).
223-
sorted().
224-
toList();
225-
return new VersionData(version, status, summary, existingIssues);
223+
for (Path entry : entries)
224+
{
225+
if (Files.isDirectory(entry))
226+
issueDirs.add(entry);
227+
}
228+
}
229+
for (Path issueDir : issueDirs)
230+
{
231+
if (isOpenIssue(issueDir))
232+
existingIssues.add(issueDir.getFileName().toString());
226233
}
234+
return new VersionData(version, status, summary, existingIssues);
235+
}
236+
237+
/**
238+
* Returns true if the issue directory has an {@code index.json} with {@code "status": "open"}.
239+
* Directories without an {@code index.json}, or with any other status, are not open issues.
240+
*
241+
* @param issueDir the candidate issue directory
242+
* @return true if the issue is open
243+
* @throws IOException if an I/O error occurs reading the index.json
244+
*/
245+
private boolean isOpenIssue(Path issueDir) throws IOException
246+
{
247+
Path indexJson = issueDir.resolve("index.json");
248+
if (Files.notExists(indexJson))
249+
return false;
250+
JsonNode indexNode = scope.getJsonMapper().readTree(indexJson.toFile());
251+
JsonNode statusNode = indexNode.get("status");
252+
if (statusNode == null || !statusNode.isString())
253+
return false;
254+
return statusNode.asString().equals("open");
227255
}
228256

229257
/**

client/src/test/java/io/github/cowwoc/cat/hooks/test/GetAddOutputPlanningDataTest.java

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import java.io.IOException;
1515
import java.nio.file.Files;
1616
import java.nio.file.Path;
17+
import java.util.List;
1718

1819
import static io.github.cowwoc.requirements13.java.DefaultJavaValidators.requireThat;
1920

@@ -100,7 +101,10 @@ public void inProgressVersionIsIncluded() throws IOException
100101
{"status":"in-progress"}""");
101102
Files.writeString(versionDir.resolve("plan.md"),
102103
"# Plan\n\n## Goal\n\nTest version goal summary.\n");
103-
Files.createDirectories(versionDir.resolve("my-issue"));
104+
Path myIssueDir = versionDir.resolve("my-issue");
105+
Files.createDirectories(myIssueDir);
106+
Files.writeString(myIssueDir.resolve("index.json"), """
107+
{"status":"open"}""");
104108

105109
GetAddOutput handler = new GetAddOutput(scope);
106110
String result = handler.getOutput(new String[0]);
@@ -323,7 +327,10 @@ public void regularFilesNotListedAsIssues() throws IOException
323327
Files.writeString(versionDir.resolve("plan.md"), "# Plan\n\n## Goal\n\nGoal.\n");
324328
Files.writeString(versionDir.resolve("CHANGELOG.md"), "# Changelog\n");
325329
Files.writeString(versionDir.resolve("notes.txt"), "some notes");
326-
Files.createDirectories(versionDir.resolve("real-issue"));
330+
Path realIssueDir = versionDir.resolve("real-issue");
331+
Files.createDirectories(realIssueDir);
332+
Files.writeString(realIssueDir.resolve("index.json"), """
333+
{"status":"open"}""");
327334

328335
GetAddOutput handler = new GetAddOutput(scope);
329336
String result = handler.getOutput(new String[0]);
@@ -354,8 +361,14 @@ public void issueNamesWithSpecialCharactersListed() throws IOException
354361
Files.createDirectories(versionDir);
355362
Files.writeString(versionDir.resolve("index.json"), """
356363
{"status":"open"}""");
357-
Files.createDirectories(versionDir.resolve("fix-bug-123"));
358-
Files.createDirectories(versionDir.resolve("add-feature-abc"));
364+
Path fixBugDir = versionDir.resolve("fix-bug-123");
365+
Files.createDirectories(fixBugDir);
366+
Files.writeString(fixBugDir.resolve("index.json"), """
367+
{"status":"open"}""");
368+
Path addFeatureDir = versionDir.resolve("add-feature-abc");
369+
Files.createDirectories(addFeatureDir);
370+
Files.writeString(addFeatureDir.resolve("index.json"), """
371+
{"status":"open"}""");
359372

360373
GetAddOutput handler = new GetAddOutput(scope);
361374
String result = handler.getOutput(new String[0]);
@@ -388,9 +401,13 @@ public void issueCountEqualsExistingIssuesSize() throws IOException
388401
Files.createDirectories(versionDir);
389402
Files.writeString(versionDir.resolve("index.json"), """
390403
{"status":"open"}""");
391-
Files.createDirectories(versionDir.resolve("issue-a"));
392-
Files.createDirectories(versionDir.resolve("issue-b"));
393-
Files.createDirectories(versionDir.resolve("issue-c"));
404+
for (String issueName : List.of("issue-a", "issue-b", "issue-c"))
405+
{
406+
Path issueDir = versionDir.resolve(issueName);
407+
Files.createDirectories(issueDir);
408+
Files.writeString(issueDir.resolve("index.json"), """
409+
{"status":"open"}""");
410+
}
394411

395412
GetAddOutput handler = new GetAddOutput(scope);
396413
String result = handler.getOutput(new String[0]);
@@ -886,4 +903,117 @@ public void nullArgsThrowsNullPointerException() throws IOException
886903
handler.getOutput(null);
887904
}
888905
}
906+
907+
// ==================== open-only issue filtering ====================
908+
909+
/**
910+
* Verifies that closed issues are excluded from the existing_issues list, allowing new issues with the
911+
* same name to be created without a false duplicate error.
912+
*
913+
* @throws IOException if an I/O error occurs
914+
*/
915+
@Test
916+
@SuppressWarnings("try")
917+
public void closedIssueExcludedFromExistingIssues() throws IOException
918+
{
919+
try (TestClaudeTool scope = new TestClaudeTool())
920+
{
921+
Path projectPath = scope.getProjectPath();
922+
Path issuesDir = projectPath.resolve(".cat/issues");
923+
Path versionDir = issuesDir.resolve("v2/v2.1");
924+
Files.createDirectories(versionDir);
925+
Files.writeString(versionDir.resolve("index.json"), """
926+
{"status":"open"}""");
927+
928+
Path closedIssueDir = versionDir.resolve("fix-bug");
929+
Files.createDirectories(closedIssueDir);
930+
Files.writeString(closedIssueDir.resolve("index.json"), """
931+
{"status":"closed","resolution":"implemented"}""");
932+
933+
GetAddOutput handler = new GetAddOutput(scope);
934+
String result = handler.getOutput(new String[0]);
935+
936+
JsonMapper mapper = scope.getJsonMapper();
937+
JsonNode version = mapper.readTree(result).get("versions").get(0);
938+
JsonNode existingIssues = version.get("existing_issues");
939+
940+
requireThat(existingIssues.size(), "existing_issues.size").isEqualTo(0);
941+
requireThat(version.get("issue_count").asInt(), "issue_count").isEqualTo(0);
942+
}
943+
}
944+
945+
/**
946+
* Verifies that open issues are included and closed issues are excluded from existing_issues.
947+
*
948+
* @throws IOException if an I/O error occurs
949+
*/
950+
@Test
951+
@SuppressWarnings("try")
952+
public void openIssueIncludedClosedIssueExcluded() throws IOException
953+
{
954+
try (TestClaudeTool scope = new TestClaudeTool())
955+
{
956+
Path projectPath = scope.getProjectPath();
957+
Path issuesDir = projectPath.resolve(".cat/issues");
958+
Path versionDir = issuesDir.resolve("v2/v2.1");
959+
Files.createDirectories(versionDir);
960+
Files.writeString(versionDir.resolve("index.json"), """
961+
{"status":"open"}""");
962+
963+
Path openIssueDir = versionDir.resolve("new-feature");
964+
Files.createDirectories(openIssueDir);
965+
Files.writeString(openIssueDir.resolve("index.json"), """
966+
{"status":"open"}""");
967+
968+
Path closedIssueDir = versionDir.resolve("fix-bug");
969+
Files.createDirectories(closedIssueDir);
970+
Files.writeString(closedIssueDir.resolve("index.json"), """
971+
{"status":"closed","resolution":"implemented"}""");
972+
973+
GetAddOutput handler = new GetAddOutput(scope);
974+
String result = handler.getOutput(new String[0]);
975+
976+
JsonMapper mapper = scope.getJsonMapper();
977+
JsonNode version = mapper.readTree(result).get("versions").get(0);
978+
JsonNode existingIssues = version.get("existing_issues");
979+
980+
requireThat(existingIssues.size(), "existing_issues.size").isEqualTo(1);
981+
requireThat(existingIssues.get(0).asString(), "existing_issues[0]").isEqualTo("new-feature");
982+
requireThat(version.get("issue_count").asInt(), "issue_count").isEqualTo(1);
983+
}
984+
}
985+
986+
/**
987+
* Verifies that an issue directory with no index.json is excluded from existing_issues, since its status
988+
* cannot be determined as open.
989+
*
990+
* @throws IOException if an I/O error occurs
991+
*/
992+
@Test
993+
@SuppressWarnings("try")
994+
public void issueWithMissingIndexJsonExcluded() throws IOException
995+
{
996+
try (TestClaudeTool scope = new TestClaudeTool())
997+
{
998+
Path projectPath = scope.getProjectPath();
999+
Path issuesDir = projectPath.resolve(".cat/issues");
1000+
Path versionDir = issuesDir.resolve("v2/v2.1");
1001+
Files.createDirectories(versionDir);
1002+
Files.writeString(versionDir.resolve("index.json"), """
1003+
{"status":"open"}""");
1004+
1005+
Path noIndexIssueDir = versionDir.resolve("mystery-issue");
1006+
Files.createDirectories(noIndexIssueDir);
1007+
1008+
GetAddOutput handler = new GetAddOutput(scope);
1009+
String result = handler.getOutput(new String[0]);
1010+
1011+
JsonMapper mapper = scope.getJsonMapper();
1012+
JsonNode version = mapper.readTree(result).get("versions").get(0);
1013+
JsonNode existingIssues = version.get("existing_issues");
1014+
1015+
requireThat(existingIssues.size(), "existing_issues.size").isEqualTo(0);
1016+
requireThat(version.get("issue_count").asInt(), "issue_count").isEqualTo(0);
1017+
}
1018+
}
8891019
}

0 commit comments

Comments
 (0)