Skip to content

Commit 712b4b8

Browse files
LarsEckartclaude
andcommitted
Add RunTestsTool for executing Gradle tests
- Implement RunTestsTool for Anthropic provider with 1-minute timeout - Add runTests() method to GeminiTools with same functionality - Register test execution tool in both AI providers - Remove console appender from logback.xml configuration - Update documentation to reflect new test execution capability - Simple zero-parameter design for ease of use 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 315c59f commit 712b4b8

File tree

7 files changed

+256
-31
lines changed

7 files changed

+256
-31
lines changed

CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ This is a very simple AI agent, following this tutorial: https://ampcode.com/how
88
It's implemented in java though.
99

1010
## AI Provider Support
11-
- **Anthropic Claude**: Full support including tool calling (file operations)
12-
- **Google Gemini**: Full support including tool calling (file operations: list files, read file, edit file)
11+
- **Anthropic Claude**: Full support including tool calling (file operations, test execution)
12+
- **Google Gemini**: Full support including tool calling (file operations, test execution: list files, read file, edit file, run tests)
1313

1414

1515
## Project Structure

README.md

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,35 +7,35 @@ This application demonstrates clean architecture principles and provides both CL
77

88
### Project Statistics
99

10-
- **Total Commits**: 60
11-
- **AI-Assisted Commits**: 41 (68.33%)
12-
- **Total Lines Added**: 8622
13-
- **AI-Assisted Lines Added**: 6952 (80.63%)
14-
- **Total Lines Changed**: 12883
15-
- **AI-Assisted Lines Changed**: 10480 (81.35%)
10+
- **Total Commits**: 61
11+
- **AI-Assisted Commits**: 42 (68.85%)
12+
- **Total Lines Added**: 8925
13+
- **AI-Assisted Lines Added**: 7255 (81.29%)
14+
- **Total Lines Changed**: 13304
15+
- **AI-Assisted Lines Changed**: 10901 (81.94%)
1616

1717
### Breakdown by AI Assistant
1818

1919
#### Claude Code
2020

21-
- **Commits**: 32 (53.33%)
22-
- **Lines Added**: 5623
23-
- **Lines Deleted**: 2645
24-
- **Lines Changed**: 8268 (64.18%)
21+
- **Commits**: 33 (54.10%)
22+
- **Lines Added**: 5926
23+
- **Lines Deleted**: 2763
24+
- **Lines Changed**: 8689 (65.31%)
2525

2626
#### Amp
2727

28-
- **Commits**: 7 (11.67%)
28+
- **Commits**: 7 (11.48%)
2929
- **Lines Added**: 1258
3030
- **Lines Deleted**: 809
31-
- **Lines Changed**: 2067 (16.04%)
31+
- **Lines Changed**: 2067 (15.54%)
3232

3333
#### GitHub Copilot
3434

35-
- **Commits**: 2 (3.33%)
35+
- **Commits**: 2 (3.28%)
3636
- **Lines Added**: 71
3737
- **Lines Deleted**: 74
38-
- **Lines Changed**: 145 (1.13%)
38+
- **Lines Changed**: 145 (1.09%)
3939

4040

4141
*Statistics are automatically updated on each commit.*
@@ -45,6 +45,7 @@ This application demonstrates clean architecture principles and provides both CL
4545
- **Multi-Provider Support**: Choose between Anthropic Claude and Google Gemini
4646
- **Dual Interface Support**: Run as a command-line application or web server
4747
- **File Operations**: Built-in tools for reading, editing, and listing files (supports both Claude and Gemini)
48+
- **Test Execution**: Run all Gradle tests with detailed output (1-minute timeout)
4849
- **Clean Architecture**: Hexagonal architecture with clear separation of concerns
4950
- **Hot Reloading**: Development server with automatic restart and live reload
5051
- **Separate Logging**: Mode-specific log files (CLI: `logs/application-cli.log`, Web: `logs/application-web.log`)
@@ -175,13 +176,17 @@ This application follows hexagonal architecture principles:
175176

176177
### Built-in Tools
177178

178-
The agent comes with file operation tools:
179+
The agent comes with these built-in tools:
179180

180-
- **ReadFileTool**: Read file contents
181-
- **EditFileTool**: Modify existing files
182-
- **ListFilesTool**: Browse directory contents
181+
#### File Operations
182+
- **ReadFileTool**: Read file contents with encoding support
183+
- **EditFileTool**: Modify existing files with automatic backup creation
184+
- **ListFilesTool**: Browse directory contents with hidden file options
183185

184-
> **Note**: Tool support is available for both Anthropic Claude and Google Gemini providers.
186+
#### Development Tools
187+
- **RunTestsTool**: Execute all Gradle tests (1-minute timeout)
188+
189+
> **Note**: All tools are available for both Anthropic Claude and Google Gemini providers.
185190
186191
> **Security Note**: These file tools are designed exclusively for CLI mode where they operate on the user's local file system. In web mode, these tools would pose serious security risks by allowing web users to access the server's file system. A proper web implementation would require either disabling file tools entirely or implementing sandboxed alternatives.
187192

app/src/main/java/com/larseckart/adapters/ai/AnthropicProvider.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import com.larseckart.core.tools.EditFileTool;
2424
import com.larseckart.core.tools.ListFilesTool;
2525
import com.larseckart.core.tools.ReadFileTool;
26+
import com.larseckart.core.tools.RunTestsTool;
2627
import java.util.ArrayList;
2728
import java.util.List;
2829
import java.util.Map;
@@ -45,6 +46,7 @@ public AnthropicProvider(ApiKey apiKey) {
4546
this.toolRegistry.registerTool(new ReadFileTool());
4647
this.toolRegistry.registerTool(new ListFilesTool());
4748
this.toolRegistry.registerTool(new EditFileTool());
49+
this.toolRegistry.registerTool(new RunTestsTool());
4850

4951
log.debug("AnthropicProvider initialized with {} tools", toolRegistry.getAllTools().size());
5052
}

app/src/main/java/com/larseckart/adapters/ai/GeminiProvider.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,9 +62,12 @@ public AIResponse sendMessage(AIRequest request) {
6262
Method editFileMethod =
6363
GeminiTools.class.getDeclaredMethod(
6464
"editFile", String.class, String.class, String.class);
65+
Method runTestsMethod = GeminiTools.class.getDeclaredMethod("runTests");
6566
configBuilder.tools(
66-
Tool.builder().functions(listFilesMethod, readFileMethod, editFileMethod));
67-
log.info("Successfully registered listFiles, readFile, and editFile tools for Gemini");
67+
Tool.builder()
68+
.functions(listFilesMethod, readFileMethod, editFileMethod, runTestsMethod));
69+
log.info(
70+
"Successfully registered listFiles, readFile, editFile, and runTests tools for Gemini");
6871
} catch (NoSuchMethodException e) {
6972
log.error("Failed to register Gemini tools", e);
7073
}
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
package com.larseckart.core.tools;
2+
3+
import static org.slf4j.LoggerFactory.getLogger;
4+
5+
import com.fasterxml.jackson.databind.JsonNode;
6+
import com.larseckart.core.domain.Tool;
7+
import java.io.BufferedReader;
8+
import java.io.IOException;
9+
import java.io.InputStreamReader;
10+
import java.nio.file.Files;
11+
import java.nio.file.Path;
12+
import java.nio.file.Paths;
13+
import java.util.concurrent.TimeUnit;
14+
import org.slf4j.Logger;
15+
16+
/**
17+
* A tool that runs all Gradle tests using gradlew with a 1-minute timeout. Provides detailed output
18+
* including test results, failures, and execution summary.
19+
*/
20+
public class RunTestsTool implements Tool {
21+
22+
private static final Logger log = getLogger(RunTestsTool.class);
23+
private static final int TIMEOUT_MINUTES = 1;
24+
private static final int MAX_OUTPUT_LENGTH = 10000;
25+
26+
@Override
27+
public String getName() {
28+
return "run_tests";
29+
}
30+
31+
@Override
32+
public String getDescription() {
33+
return "Runs all Gradle tests using gradlew. "
34+
+ "Provides detailed output including test results, failures, and execution summary.";
35+
}
36+
37+
@Override
38+
public String getParameterSchema() {
39+
return """
40+
{
41+
"$schema": "https://json-schema.org/draft/2020-12/schema",
42+
"type": "object",
43+
"properties": {},
44+
"additionalProperties": false
45+
}""";
46+
}
47+
48+
@Override
49+
public String execute(JsonNode parameters) {
50+
log.info("Executing RunTestsTool with parameters: {}", parameters);
51+
52+
try {
53+
54+
// Ensure we're in a Gradle project
55+
Path currentDir = Paths.get(System.getProperty("user.dir"));
56+
Path gradlewScript = currentDir.resolve("gradlew");
57+
Path gradlewBat = currentDir.resolve("gradlew.bat");
58+
59+
if (!Files.exists(gradlewScript) && !Files.exists(gradlewBat)) {
60+
return "Error: No gradlew script found in current directory. This tool requires a Gradle project with gradlew.";
61+
}
62+
63+
// Build the command
64+
String gradlewCommand = Files.exists(gradlewScript) ? "./gradlew" : "gradlew.bat";
65+
ProcessBuilder processBuilder = new ProcessBuilder(gradlewCommand, "test");
66+
log.info("Running all tests");
67+
68+
processBuilder.directory(currentDir.toFile());
69+
processBuilder.redirectErrorStream(true);
70+
71+
// Start the process
72+
Process process = processBuilder.start();
73+
StringBuilder output = new StringBuilder();
74+
75+
// Read output
76+
try (BufferedReader reader =
77+
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
78+
String line;
79+
while ((line = reader.readLine()) != null) {
80+
output.append(line).append("\n");
81+
// Prevent excessive output
82+
if (output.length() > MAX_OUTPUT_LENGTH) {
83+
output.append("\n[Output truncated - too long]\n");
84+
break;
85+
}
86+
}
87+
}
88+
89+
// Wait for completion with timeout
90+
boolean finished = process.waitFor(TIMEOUT_MINUTES, TimeUnit.MINUTES);
91+
92+
if (!finished) {
93+
process.destroyForcibly();
94+
return "Error: Test execution timed out after "
95+
+ TIMEOUT_MINUTES
96+
+ " minute.\n\nPartial output:\n"
97+
+ output.toString();
98+
}
99+
100+
int exitCode = process.exitValue();
101+
String result = output.toString();
102+
103+
// Format the response
104+
StringBuilder response = new StringBuilder();
105+
response.append("Test execution completed with exit code: ").append(exitCode).append("\n\n");
106+
107+
if (exitCode == 0) {
108+
response.append("✅ All tests passed!\n\n");
109+
} else {
110+
response.append("❌ Some tests failed or there were errors.\n\n");
111+
}
112+
113+
response.append("Output:\n");
114+
response.append(result);
115+
116+
return response.toString();
117+
118+
} catch (InterruptedException e) {
119+
Thread.currentThread().interrupt();
120+
return "Error: Test execution was interrupted: " + e.getMessage();
121+
} catch (IOException e) {
122+
return "Error: Failed to execute gradlew test: " + e.getMessage();
123+
} catch (Exception e) {
124+
return "Error: Unexpected error during test execution: " + e.getMessage();
125+
}
126+
}
127+
128+
@Override
129+
public void validate(JsonNode parameters) {
130+
// No parameters to validate
131+
}
132+
}

app/src/main/java/com/larseckart/tools/gemini/GeminiTools.java

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,17 @@
22

33
import static org.slf4j.LoggerFactory.getLogger;
44

5+
import java.io.BufferedReader;
56
import java.io.IOException;
7+
import java.io.InputStreamReader;
68
import java.nio.charset.Charset;
79
import java.nio.file.Files;
810
import java.nio.file.NoSuchFileException;
911
import java.nio.file.Path;
1012
import java.nio.file.Paths;
1113
import java.nio.file.StandardCopyOption;
1214
import java.util.Comparator;
15+
import java.util.concurrent.TimeUnit;
1316
import java.util.stream.Stream;
1417
import org.slf4j.Logger;
1518

@@ -18,6 +21,8 @@ public class GeminiTools {
1821
private static final Logger log = getLogger(GeminiTools.class);
1922
private static final long MAX_FILE_SIZE = 1024 * 1024; // 1MB limit
2023
private static final String DEFAULT_ENCODING = "UTF-8";
24+
private static final int TIMEOUT_MINUTES = 1;
25+
private static final int MAX_OUTPUT_LENGTH = 10000;
2126

2227
/**
2328
* Lists the contents of a directory, including files and subdirectories
@@ -251,6 +256,91 @@ public static String editFile(String path, String searchText, String replaceText
251256
}
252257
}
253258

259+
/**
260+
* Runs all Gradle tests using gradlew with a 1-minute timeout. Provides detailed output including
261+
* test results, failures, and execution summary.
262+
*
263+
* @return Test execution results or error message
264+
*/
265+
public static String runTests() {
266+
log.info("Executing runTests");
267+
268+
try {
269+
270+
// Ensure we're in a Gradle project
271+
Path currentDir = Paths.get(System.getProperty("user.dir"));
272+
Path gradlewScript = currentDir.resolve("gradlew");
273+
Path gradlewBat = currentDir.resolve("gradlew.bat");
274+
275+
if (!Files.exists(gradlewScript) && !Files.exists(gradlewBat)) {
276+
return "Error: No gradlew script found in current directory. This tool requires a Gradle project with gradlew.";
277+
}
278+
279+
// Build the command
280+
String gradlewCommand = Files.exists(gradlewScript) ? "./gradlew" : "gradlew.bat";
281+
ProcessBuilder processBuilder = new ProcessBuilder(gradlewCommand, "test");
282+
log.info("Running all tests");
283+
284+
processBuilder.directory(currentDir.toFile());
285+
processBuilder.redirectErrorStream(true);
286+
287+
// Start the process
288+
Process process = processBuilder.start();
289+
StringBuilder output = new StringBuilder();
290+
291+
// Read output
292+
try (BufferedReader reader =
293+
new BufferedReader(new InputStreamReader(process.getInputStream()))) {
294+
String line;
295+
while ((line = reader.readLine()) != null) {
296+
output.append(line).append("\n");
297+
// Prevent excessive output
298+
if (output.length() > MAX_OUTPUT_LENGTH) {
299+
output.append("\n[Output truncated - too long]\n");
300+
break;
301+
}
302+
}
303+
}
304+
305+
// Wait for completion with timeout
306+
boolean finished = process.waitFor(TIMEOUT_MINUTES, TimeUnit.MINUTES);
307+
308+
if (!finished) {
309+
process.destroyForcibly();
310+
return "Error: Test execution timed out after "
311+
+ TIMEOUT_MINUTES
312+
+ " minute.\n\nPartial output:\n"
313+
+ output.toString();
314+
}
315+
316+
int exitCode = process.exitValue();
317+
String result = output.toString();
318+
319+
// Format the response
320+
StringBuilder response = new StringBuilder();
321+
response.append("Test execution completed with exit code: ").append(exitCode).append("\n\n");
322+
323+
if (exitCode == 0) {
324+
response.append("✅ All tests passed!\n\n");
325+
} else {
326+
response.append("❌ Some tests failed or there were errors.\n\n");
327+
}
328+
329+
response.append("Output:\n");
330+
response.append(result);
331+
332+
return response.toString();
333+
334+
} catch (InterruptedException e) {
335+
Thread.currentThread().interrupt();
336+
return "Error: Test execution was interrupted: " + e.getMessage();
337+
} catch (IOException e) {
338+
return "Error: Failed to execute gradlew test: " + e.getMessage();
339+
} catch (Exception e) {
340+
return "Error: Unexpected error during test execution: " + e.getMessage();
341+
}
342+
}
343+
254344
/**
255345
* Resolves the file path, handling both absolute and relative paths. Relative paths are resolved
256346
* against the current working directory.

app/src/main/resources/logback.xml

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,7 @@
88
<property name="APP_MODE" value="${app.mode:-cli}"/>
99
<property name="LOG_FILE" value="logs/application-${APP_MODE}.log"/>
1010
<property name="LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"/>
11-
12-
<!-- Console appender for CLI mode -->
13-
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
14-
<encoder>
15-
<pattern>${LOG_PATTERN}</pattern>
16-
</encoder>
17-
</appender>
18-
11+
1912
<!-- Rolling file appender to preserve log history -->
2013
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
2114
<file>${LOG_FILE}</file>

0 commit comments

Comments
 (0)