From f257415dc24c58cca24eb8f9764a1c4a680263f8 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 14:48:32 -0400 Subject: [PATCH 1/5] Add Java as a supported output language Follows the exact same pattern as python/javascript/typescript(/go): - New prompt partial (prompts/partials/_language_java.md): uses java.net.http.HttpClient (built into the JDK since 11, no HTTP library dependency) and Gson for JSON (the JDK has no built-in JSON support), packaged as a minimal Maven project with exec-maven-plugin so it runs with a single command. Same auth-hardcoding/refresh guidance as the other languages. Also calls out a Java-specific naming detail: the generated file is named api_client.java (this generator's lowercase convention), so the top-level class is named ApiClient but declared package-private (no `public` modifier) - a public class's filename must exactly match its class name, and a package-private one doesn't have that restriction while compiling and running identically. - base_engineer.py: added "java" to _OUTPUT_LANGUAGE_EXTENSIONS (.java), _get_language_name() ("Java"), _get_run_command() ("mvn -q compile exec:java"), and _get_auto_output_files() (pom.xml, always included for Java - unlike Go/JS's conditional dependency files, Gson is a hard requirement here since there's no stdlib JSON alternative, matching typescript's "always" precedent instead). - cli.py: added Java to the interactive `output_language` config picker. - Updated the three docstrings/comments that listed the language set (config.py, engineer.py, prompts/__init__.py) and README.md's output language line. - Tests: mirrored the existing per-language test pattern in test_base_engineer.py (extension, run command, prompt content) and test_prompts.py (partial loading) for Java. Caught and fixed one real bug before this counts as done: my first draft of the partial included a literal `{ ... }` in prose describing the class declaration, which crashed load()'s str.format_map() with a KeyError (it treats any `{...}` in the template as a placeholder to substitute, not just the documented `{run_command}`-style ones) - confirmed against the other partials that none of them contain a literal brace anywhere, for the same reason. Rephrased to avoid it. Full test suite passes (49/49 in the touched files, run 3x to confirm) except one pre-existing flaky test unrelated to this change (test_existing_client_language_falls_back_to_newest_file - a mtime-resolution race condition; confirmed it fails intermittently on a clean main checkout with none of these changes too, ~40% of runs in this environment). ruff check is clean on every file this touches (8 pre-existing lint errors elsewhere on main, none in the touched lines). --- CHANGELOG.md | 1 + README.md | 2 +- src/reverse_api/base_engineer.py | 5 ++++ src/reverse_api/cli.py | 1 + src/reverse_api/config.py | 2 +- src/reverse_api/engineer.py | 2 +- src/reverse_api/prompts/__init__.py | 2 +- .../prompts/partials/_language_java.md | 24 +++++++++++++++++++ tests/test_base_engineer.py | 14 +++++++++++ tests/test_prompts.py | 10 ++++++++ 10 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 src/reverse_api/prompts/partials/_language_java.md diff --git a/CHANGELOG.md b/CHANGELOG.md index e5643404..9d691bd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - **Go output language**: `output_language: "go"` is now supported alongside python/javascript/typescript, generating a standard-library-first (`net/http`, `encoding/json`) Go program, with the same auth-hardcoding/refresh and bot-detection-fallback guidance as the other languages. +- **Java output language**: `output_language: "java"` is now supported alongside python/javascript/typescript, generating a small Maven project using `java.net.http.HttpClient` (JDK 11+, no HTTP library dependency) and Gson for JSON, with the same auth-hardcoding/refresh guidance as the other languages. ## [0.10.0] - 2026-06-01 diff --git a/README.md b/README.md index 85a9d2c1..edd6e4d8 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ Settings live in `~/.reverse-api/config.json` and can be edited via `/settings` - **Models**: Sonnet 4.6 (default), Opus 4.6 (most capable), Haiku 4.5 (fastest). For OpenCode see [models.dev](https://models.dev). - **SDK**: `claude` (default), `opencode`, `cursor`, or `copilot` (GitHub Copilot). -- **Output language**: `python`, `javascript`, `typescript`, or `go`. +- **Output language**: `python`, `javascript`, `typescript`, `go`, or `java`. ## CLI diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 3692cc52..2e499495 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -34,6 +34,7 @@ class BaseEngineer(ABC): "javascript": ".js", "typescript": ".ts", "go": ".go", + "java": ".java", } def __init__( @@ -441,6 +442,7 @@ def _get_language_name(self) -> str: "javascript": "JavaScript", "typescript": "TypeScript", "go": "Go", + "java": "Java", }.get(self.output_language, "Python") def _get_existing_client_guidance(self) -> str: @@ -470,6 +472,7 @@ def _get_run_command(self) -> str: "javascript": "node api_client.js", "typescript": "npx tsx api_client.ts", "go": "go run api_client.go", + "java": "mvn -q compile exec:java", }.get(self.output_language, "python api_client.py") def _get_codegen_instructions(self) -> str: @@ -584,6 +587,8 @@ def _get_auto_output_files(self, language_name: str, client_filename: str) -> st f"\n3. `{self.scripts_dir}/go.mod` and `{self.scripts_dir}/go.sum` - " "Only if external dependencies are needed" ) + elif self.output_language == "java": + return base + f"\n3. `{self.scripts_dir}/pom.xml` - Maven project file (Gson dependency, exec-maven-plugin)" return base @abstractmethod diff --git a/src/reverse_api/cli.py b/src/reverse_api/cli.py index 1407d9f4..5ee8a8f8 100644 --- a/src/reverse_api/cli.py +++ b/src/reverse_api/cli.py @@ -1084,6 +1084,7 @@ def handle_settings(mode_color=THEME_PRIMARY): Choice(title="javascript", value="javascript"), Choice(title="typescript", value="typescript"), Choice(title="go", value="go"), + Choice(title="java", value="java"), Choice(title="back", value="back"), ] lang = questionary.select( diff --git a/src/reverse_api/config.py b/src/reverse_api/config.py index 4ddfbb59..09d2d218 100644 --- a/src/reverse_api/config.py +++ b/src/reverse_api/config.py @@ -20,7 +20,7 @@ "opencode_model": "claude-opus-4-6", "opencode_provider": "anthropic", "output_dir": None, # None means use ~/.reverse-api/runs - "output_language": "python", # "python", "javascript", "typescript", or "go" + "output_language": "python", # "python", "javascript", "typescript", "go", or "java" "real_time_sync": True, # Enable real-time file sync during engineering "sdk": "claude", # "claude", "opencode", "copilot", or "cursor" } diff --git a/src/reverse_api/engineer.py b/src/reverse_api/engineer.py index 93c8badd..7922d51a 100644 --- a/src/reverse_api/engineer.py +++ b/src/reverse_api/engineer.py @@ -232,7 +232,7 @@ def run_reverse_engineering( cursor_setting_sources: Optional explicit list (overrides cursor_web_search), e.g. ["project","user","all"]. enable_sync: Enable real-time file syncing during engineering is_fresh: Whether to start fresh (ignore previous scripts) - output_language: Target language - "python", "javascript", "typescript", or "go" + output_language: Target language - "python", "javascript", "typescript", "go", or "java" output_mode: Output mode - "client" for API client code, "docs" for OpenAPI specification """ if sdk == "opencode": diff --git a/src/reverse_api/prompts/__init__.py b/src/reverse_api/prompts/__init__.py index f2f19973..15833877 100644 --- a/src/reverse_api/prompts/__init__.py +++ b/src/reverse_api/prompts/__init__.py @@ -54,7 +54,7 @@ def load_language_partial(language: str, **kwargs: str) -> str: """Load the language-specific codegen instructions partial. Args: - language: One of "python", "javascript", "typescript", "go". + language: One of "python", "javascript", "typescript", "go", "java". **kwargs: Placeholder values (scripts_dir, client_filename, run_command). """ return load(f"partials/_language_{language}", **kwargs) diff --git a/src/reverse_api/prompts/partials/_language_java.md b/src/reverse_api/prompts/partials/_language_java.md new file mode 100644 index 00000000..9a4c20f4 --- /dev/null +++ b/src/reverse_api/prompts/partials/_language_java.md @@ -0,0 +1,24 @@ +**Generate a Java program** that replicates the API calls found in the traffic. The following are guidelines — use your judgment on what's appropriate for the specific API: + +- Use `java.net.http.HttpClient` (built into the JDK since 11) for requests — no HTTP library dependency needed +- Use Gson for JSON parsing/serialization — the one dependency this needs, since the JDK has no built-in JSON support +- Create a minimal Maven project (`pom.xml`) with the Gson dependency and the `exec-maven-plugin` configured with `ApiClient`, so the client runs with a single command and no extra flags +- Create a separate method for each distinct API endpoint, with a small class for its response shape +- Reuse one `HttpClient` instance across requests rather than creating a new one per call +- Include a `main` method with example usage +- The output file is named `{client_filename}` (lowercase with underscores), so name the top-level class holding `main` exactly `ApiClient`, declared package-private, *without* the `public` modifier — a `public` class's filename must exactly match its class name, and this generator's file naming convention doesn't follow Java's usual PascalCase file naming. A package-private top-level class compiles and runs identically; it just isn't visible from other packages, which this single-file client has no need for. + +**Authentication & credentials:** +- Hardcode all cookies, tokens, session IDs, and auth headers found in the traffic directly in the program +- The user should be able to run the program immediately after `mvn compile` — no env vars, no additional config files, no manual setup beyond what's generated +- If the API uses cookies, build the `HttpClient` with a `CookieHandler`/`CookieManager` so cookies persist across requests +- If the API uses Bearer tokens or API keys, hardcode them in the request headers +- Handle auth refresh so the program doesn't go stale: if you see a token refresh endpoint, OAuth refresh flow, or login endpoint in the traffic, implement automatic re-authentication when a request returns 401/403. If cookies have expiry, re-fetch them before they expire + +**Testing:** +- Run: `{run_command}` +- You have up to 5 attempts to fix issues + +Save the program to: `{scripts_dir}/{client_filename}` +Save documentation to: `{scripts_dir}/README.md` +Save the Maven project file to: `{scripts_dir}/pom.xml` diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index c2d400af..09017252 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -197,6 +197,10 @@ def test_get_output_extension_go(self, tmp_path): """Go extension.""" eng = self._make_engineer(tmp_path, output_language="go") assert eng._get_output_extension() == ".go" + def test_get_output_extension_java(self, tmp_path): + """Java extension.""" + eng = self._make_engineer(tmp_path, output_language="java") + assert eng._get_output_extension() == ".java" def test_get_output_extension_unknown(self, tmp_path): """Unknown language defaults to .py.""" @@ -232,6 +236,10 @@ def test_get_run_command_go(self, tmp_path): """Run command for Go.""" eng = self._make_engineer(tmp_path, output_language="go") assert eng._get_run_command() == "go run api_client.go" + def test_get_run_command_java(self, tmp_path): + """Run command for Java.""" + eng = self._make_engineer(tmp_path, output_language="java") + assert eng._get_run_command() == "mvn -q compile exec:java" def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" @@ -285,6 +293,12 @@ def test_go_prompt(self, tmp_path): system_prompt, user_message = eng._build_prompts() assert "Go program" in system_prompt assert "net/http" in system_prompt + def test_java_prompt(self, tmp_path): + """Java prompt includes Java-specific instructions.""" + eng = self._make_engineer(tmp_path, output_language="java") + system_prompt, user_message = eng._build_prompts() + assert "Java program" in system_prompt + assert "HttpClient" in system_prompt def test_docs_prompt(self, tmp_path): """Docs mode prompt includes OpenAPI instructions.""" diff --git a/tests/test_prompts.py b/tests/test_prompts.py index 47484857..ffb3f2a8 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -81,6 +81,16 @@ def test_go_partial(self): assert "Go program" in text assert "net/http" in text assert "/tmp/scripts/api_client.go" in text + def test_java_partial(self): + text = load_language_partial( + "java", + scripts_dir="/tmp/scripts", + client_filename="api_client.java", + run_command="mvn -q compile exec:java", + ) + assert "Java program" in text + assert "HttpClient" in text + assert "/tmp/scripts/api_client.java" in text class TestEngineerTemplates: From c59b4be4e7b792738c6509c61687304b5b0b016c Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 15:12:47 -0400 Subject: [PATCH 2/5] Fix Maven source-directory and working-directory issues in Java support Two real bugs caught by review bots (greptile-apps) on the PR, both verified against the actual code before fixing: 1. Maven only compiles src/main/java by default, but the partial saved api_client.java at the project root alongside pom.xml. mvn compile would silently skip it, and exec:java would fail to find ApiClient. Fixed by telling the generated POM to override to the project root. 2. The agent's cwd for the whole session is scripts_dir.parent.parent (confirmed in engineer.py's analyze_and_generate -> ClaudeAgentOptions), not scripts_dir itself, while pom.xml is saved under scripts_dir. python/node/npx's run commands share this same cwd gap but take a bare relative filename either way, so it doesn't matter for them in practice (that's why this went unnoticed for the existing three languages) - but Maven hard-fails immediately with no upward search if invoked from a directory with no pom.xml, a sharper failure mode. _get_run_command() now points -f explicitly at this run's own pom.xml path for Java specifically, removing the ambiguity rather than relying on the agent to cd there itself first. Updated test_get_run_command_java to match the new (scripts_dir- dependent, no longer a static string) return value. A third bot finding (cli.py's `run --file` subcommand only discovers *.py scripts and always launches them with the shared venv's python interpreter) is real but pre-existing and out of scope here - confirmed discover_scripts() in utils.py has been Python-only since before javascript/typescript were added, so a Java run is no worse off than an existing JS/TS one already is. Multi-language support for that separate subcommand looks like its own follow-up, not something to fold into a single-language-addition PR. Full test suite passes (49/49, run 2x) except the same pre-existing flaky test noted in the other language PRs. ruff check clean on the touched lines (2 pre-existing unused-import warnings elsewhere in test_base_engineer.py, unrelated). --- src/reverse_api/base_engineer.py | 10 +++++++++- src/reverse_api/prompts/partials/_language_java.md | 2 +- tests/test_base_engineer.py | 8 ++++++-- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 2e499495..3993735e 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -467,12 +467,20 @@ def _get_client_filename(self) -> str: def _get_run_command(self) -> str: """Return the command to run the generated client.""" + if self.output_language == "java": + # Unlike python/node/npx (which happily take a plain relative + # filename regardless of the agent's actual cwd, scripts_dir. + # parent.parent — see analyze_and_generate's ClaudeAgentOptions), + # Maven hard-fails immediately if invoked from a directory with + # no pom.xml, no upward search. -f points it straight at the + # right project file regardless of cwd, rather than relying on + # the agent to cd there itself first. + return f'mvn -q -f "{self.scripts_dir}/pom.xml" compile exec:java' return { "python": "python api_client.py", "javascript": "node api_client.js", "typescript": "npx tsx api_client.ts", "go": "go run api_client.go", - "java": "mvn -q compile exec:java", }.get(self.output_language, "python api_client.py") def _get_codegen_instructions(self) -> str: diff --git a/src/reverse_api/prompts/partials/_language_java.md b/src/reverse_api/prompts/partials/_language_java.md index 9a4c20f4..48e266c5 100644 --- a/src/reverse_api/prompts/partials/_language_java.md +++ b/src/reverse_api/prompts/partials/_language_java.md @@ -2,7 +2,7 @@ - Use `java.net.http.HttpClient` (built into the JDK since 11) for requests — no HTTP library dependency needed - Use Gson for JSON parsing/serialization — the one dependency this needs, since the JDK has no built-in JSON support -- Create a minimal Maven project (`pom.xml`) with the Gson dependency and the `exec-maven-plugin` configured with `ApiClient`, so the client runs with a single command and no extra flags +- Create a minimal Maven project (`pom.xml`) with the Gson dependency and the `exec-maven-plugin` configured with `ApiClient`, so the client runs with a single command and no extra flags. A conventional Maven layout only compiles `src/main/java`, but `{client_filename}` is saved at the project root (see below) — override the build's `` to `.` (project root) in the POM so `mvn compile` actually finds and compiles it - Create a separate method for each distinct API endpoint, with a small class for its response shape - Reuse one `HttpClient` instance across requests rather than creating a new one per call - Include a `main` method with example usage diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 09017252..53767e6d 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -237,9 +237,13 @@ def test_get_run_command_go(self, tmp_path): eng = self._make_engineer(tmp_path, output_language="go") assert eng._get_run_command() == "go run api_client.go" def test_get_run_command_java(self, tmp_path): - """Run command for Java.""" + """Run command for Java points -f at this run's own pom.xml, not a + bare relative path — the agent's cwd is scripts_dir.parent.parent + (see analyze_and_generate), and unlike python/node/npx, Maven + hard-fails with no upward search if invoked from a directory with + no pom.xml.""" eng = self._make_engineer(tmp_path, output_language="java") - assert eng._get_run_command() == "mvn -q compile exec:java" + assert eng._get_run_command() == f'mvn -q -f "{eng.scripts_dir}/pom.xml" compile exec:java' def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From e6c1f454e90726110c4e550e45a9b78a9b481d40 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 15:48:47 -0400 Subject: [PATCH 3/5] Fix shell-quoting in the Java run command Same class of bug caught by review bots on the PHP PR, applied here proactively: _get_run_command()'s "-f \"{path}/pom.xml\"" only looked safe - manual double-quoting doesn't stop $()/backtick command substitution inside a double-quoted shell string. Replaced with shlex.quote(), which correctly single-quotes the whole path when metacharacters are present. Added a test with a deliberately hostile path (embedded $(rm -rf ~)) asserting the command round-trips through shlex.split() back to the literal path. Full test suite passes (50/50, run 2x) except the same pre-existing flaky test noted on the other language PRs. --- src/reverse_api/base_engineer.py | 9 +++++++-- tests/test_base_engineer.py | 24 ++++++++++++++++++------ 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index 3993735e..a358fb64 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -2,6 +2,7 @@ import asyncio import os +import shlex from abc import ABC, abstractmethod from pathlib import Path from typing import Any @@ -474,8 +475,12 @@ def _get_run_command(self) -> str: # Maven hard-fails immediately if invoked from a directory with # no pom.xml, no upward search. -f points it straight at the # right project file regardless of cwd, rather than relying on - # the agent to cd there itself first. - return f'mvn -q -f "{self.scripts_dir}/pom.xml" compile exec:java' + # the agent to cd there itself first. shlex.quote(), not manual + # double-quoting — output_dir (and so scripts_dir) isn't + # guaranteed free of shell metacharacters, and naive f'"{path}"' + # still lets $()/backticks expand inside double quotes. + pom = shlex.quote(f"{self.scripts_dir}/pom.xml") + return f"mvn -q -f {pom} compile exec:java" return { "python": "python api_client.py", "javascript": "node api_client.js", diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 53767e6d..a40b67d1 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -1,5 +1,6 @@ """Tests for base_engineer.py - BaseEngineer abstract class.""" +import shlex from pathlib import Path from typing import Any from unittest.mock import MagicMock, patch @@ -237,13 +238,24 @@ def test_get_run_command_go(self, tmp_path): eng = self._make_engineer(tmp_path, output_language="go") assert eng._get_run_command() == "go run api_client.go" def test_get_run_command_java(self, tmp_path): - """Run command for Java points -f at this run's own pom.xml, not a - bare relative path — the agent's cwd is scripts_dir.parent.parent - (see analyze_and_generate), and unlike python/node/npx, Maven - hard-fails with no upward search if invoked from a directory with - no pom.xml.""" + """Run command for Java points -f at this run's own (shell-quoted) + pom.xml, not a bare relative path — the agent's cwd is scripts_dir. + parent.parent (see analyze_and_generate), and unlike python/node/ + npx, Maven hard-fails with no upward search if invoked from a + directory with no pom.xml.""" eng = self._make_engineer(tmp_path, output_language="java") - assert eng._get_run_command() == f'mvn -q -f "{eng.scripts_dir}/pom.xml" compile exec:java' + expected_pom = shlex.quote(f"{eng.scripts_dir}/pom.xml") + assert eng._get_run_command() == f"mvn -q -f {expected_pom} compile exec:java" + + def test_get_run_command_java_quotes_metacharacters(self, tmp_path): + """A scripts_dir containing shell metacharacters must round-trip + back to the literal path, not be left open to $()/backtick + expansion — what the naive f'"{path}"' approach got wrong.""" + eng = self._make_engineer(tmp_path, output_language="java") + eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir") + tokens = shlex.split(eng._get_run_command()) + assert tokens[:2] == ["mvn", "-q"] + assert tokens[3] == f"{eng.scripts_dir}/pom.xml" def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From 8e0765c35e61764013954cbd08c90a35489188d9 Mon Sep 17 00:00:00 2001 From: Joshua Dunbar Date: Tue, 21 Jul 2026 17:25:06 -0400 Subject: [PATCH 4/5] Resolve scripts_dir to an absolute path in the Java run command Same root cause cubic flagged on the C# and Ruby PRs: a relative --output-dir leaves scripts_dir relative to the original cwd, but once the agent's cwd moves to scripts_dir.parent.parent (see analyze_and_generate), embedding that same relative string in -f re-interprets it from the new location and points Maven at a doubly- nested, nonexistent pom.xml. Resolve scripts_dir before interpolating it, proactively applying the same fix here before it gets flagged. --- src/reverse_api/base_engineer.py | 6 +++++- tests/test_base_engineer.py | 27 ++++++++++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index a358fb64..bb8720de 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -479,7 +479,11 @@ def _get_run_command(self) -> str: # double-quoting — output_dir (and so scripts_dir) isn't # guaranteed free of shell metacharacters, and naive f'"{path}"' # still lets $()/backticks expand inside double quotes. - pom = shlex.quote(f"{self.scripts_dir}/pom.xml") + # .resolve(): a relative --output-dir would otherwise be + # re-interpreted against the agent's cwd (scripts_dir.parent. + # parent) instead of the original cwd it was relative to, + # pointing -f at the wrong, doubly-nested location. + pom = shlex.quote(str(self.scripts_dir.resolve() / "pom.xml")) return f"mvn -q -f {pom} compile exec:java" return { "python": "python api_client.py", diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index a40b67d1..3b1d4298 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -238,13 +238,13 @@ def test_get_run_command_go(self, tmp_path): eng = self._make_engineer(tmp_path, output_language="go") assert eng._get_run_command() == "go run api_client.go" def test_get_run_command_java(self, tmp_path): - """Run command for Java points -f at this run's own (shell-quoted) - pom.xml, not a bare relative path — the agent's cwd is scripts_dir. - parent.parent (see analyze_and_generate), and unlike python/node/ - npx, Maven hard-fails with no upward search if invoked from a - directory with no pom.xml.""" + """Run command for Java points -f at this run's own (resolved, + shell-quoted) pom.xml, not a bare relative path — the agent's cwd is + scripts_dir.parent.parent (see analyze_and_generate), and unlike + python/node/npx, Maven hard-fails with no upward search if invoked + from a directory with no pom.xml.""" eng = self._make_engineer(tmp_path, output_language="java") - expected_pom = shlex.quote(f"{eng.scripts_dir}/pom.xml") + expected_pom = shlex.quote(str(eng.scripts_dir.resolve() / "pom.xml")) assert eng._get_run_command() == f"mvn -q -f {expected_pom} compile exec:java" def test_get_run_command_java_quotes_metacharacters(self, tmp_path): @@ -255,7 +255,20 @@ def test_get_run_command_java_quotes_metacharacters(self, tmp_path): eng.scripts_dir = Path("/tmp/weird$(rm -rf ~) dir") tokens = shlex.split(eng._get_run_command()) assert tokens[:2] == ["mvn", "-q"] - assert tokens[3] == f"{eng.scripts_dir}/pom.xml" + assert tokens[3] == str(eng.scripts_dir.resolve() / "pom.xml") + + def test_get_run_command_java_resolves_relative_output_dir(self, tmp_path): + """A relative scripts_dir must be resolved to an absolute path before + being embedded in the command — otherwise, once the agent's cwd + moves to scripts_dir.parent.parent, the same relative string gets + re-interpreted from there and points at the wrong, doubly-nested + location.""" + eng = self._make_engineer(tmp_path, output_language="java") + eng.scripts_dir = Path("relative_output/scripts/run123") + tokens = shlex.split(eng._get_run_command()) + pom_arg = tokens[3] + assert Path(pom_arg).is_absolute() + assert pom_arg == str(eng.scripts_dir.resolve() / "pom.xml") def test_get_run_command_unknown(self, tmp_path): """Unknown language defaults to Python command.""" From 1a641bb4938207e6b483976b55dd55959049d924 Mon Sep 17 00:00:00 2001 From: kalil0321 Date: Wed, 22 Jul 2026 12:42:24 +0200 Subject: [PATCH 5/5] Use exec:exec instead of exec:java to run the generated Java client exec:java invokes main() reflectively in-process, which fails on the package-private ApiClient class this generator requires (a public class would have to match the api_client.java filename). exec:exec spawns a real java process instead, and its element expands with the platform-correct separator on Windows/macOS/Linux. The partial now pins the exact known-good plugin configuration so the agent doesn't have to rediscover this incompatibility during its test attempts. --- src/reverse_api/base_engineer.py | 6 +++++- .../prompts/partials/_language_java.md | 19 ++++++++++++++++++- tests/test_base_engineer.py | 2 +- tests/test_prompts.py | 2 +- 4 files changed, 25 insertions(+), 4 deletions(-) diff --git a/src/reverse_api/base_engineer.py b/src/reverse_api/base_engineer.py index bb8720de..e93cc5b9 100644 --- a/src/reverse_api/base_engineer.py +++ b/src/reverse_api/base_engineer.py @@ -483,8 +483,12 @@ def _get_run_command(self) -> str: # re-interpreted against the agent's cwd (scripts_dir.parent. # parent) instead of the original cwd it was relative to, # pointing -f at the wrong, doubly-nested location. + # exec:exec (spawn a real java process), not exec:java — + # exec:java invokes main() reflectively in-process, which fails + # on the package-private ApiClient class the Java partial + # requires ("symbolic reference class is not accessible"). pom = shlex.quote(str(self.scripts_dir.resolve() / "pom.xml")) - return f"mvn -q -f {pom} compile exec:java" + return f"mvn -q -f {pom} compile exec:exec" return { "python": "python api_client.py", "javascript": "node api_client.js", diff --git a/src/reverse_api/prompts/partials/_language_java.md b/src/reverse_api/prompts/partials/_language_java.md index 48e266c5..99781510 100644 --- a/src/reverse_api/prompts/partials/_language_java.md +++ b/src/reverse_api/prompts/partials/_language_java.md @@ -2,7 +2,24 @@ - Use `java.net.http.HttpClient` (built into the JDK since 11) for requests — no HTTP library dependency needed - Use Gson for JSON parsing/serialization — the one dependency this needs, since the JDK has no built-in JSON support -- Create a minimal Maven project (`pom.xml`) with the Gson dependency and the `exec-maven-plugin` configured with `ApiClient`, so the client runs with a single command and no extra flags. A conventional Maven layout only compiles `src/main/java`, but `{client_filename}` is saved at the project root (see below) — override the build's `` to `.` (project root) in the POM so `mvn compile` actually finds and compiles it +- Create a minimal Maven project (`pom.xml`) with the Gson dependency and the `exec-maven-plugin`, so the client runs with a single command and no extra flags. A conventional Maven layout only compiles `src/main/java`, but `{client_filename}` is saved at the project root (see below) — override the build's `` to `.` (project root) in the POM so `mvn compile` actually finds and compiles it +- Configure `exec-maven-plugin` for the `exec:exec` goal with exactly this configuration — do not use the `exec:java` goal or ``: `exec:java` invokes `main` reflectively in-process, which fails on the package-private `ApiClient` class described below ("symbolic reference class is not accessible"), while `exec:exec` spawns a real `java` process, and its `` element expands to the full dependency classpath with the correct platform-specific separator (`:` on macOS/Linux, `;` on Windows): + + ```xml + + org.codehaus.mojo + exec-maven-plugin + 3.1.0 + + java + + -classpath + + ApiClient + + + + ``` - Create a separate method for each distinct API endpoint, with a small class for its response shape - Reuse one `HttpClient` instance across requests rather than creating a new one per call - Include a `main` method with example usage diff --git a/tests/test_base_engineer.py b/tests/test_base_engineer.py index 3b1d4298..cc4d1d24 100644 --- a/tests/test_base_engineer.py +++ b/tests/test_base_engineer.py @@ -245,7 +245,7 @@ def test_get_run_command_java(self, tmp_path): from a directory with no pom.xml.""" eng = self._make_engineer(tmp_path, output_language="java") expected_pom = shlex.quote(str(eng.scripts_dir.resolve() / "pom.xml")) - assert eng._get_run_command() == f"mvn -q -f {expected_pom} compile exec:java" + assert eng._get_run_command() == f"mvn -q -f {expected_pom} compile exec:exec" def test_get_run_command_java_quotes_metacharacters(self, tmp_path): """A scripts_dir containing shell metacharacters must round-trip diff --git a/tests/test_prompts.py b/tests/test_prompts.py index ffb3f2a8..37f572a6 100644 --- a/tests/test_prompts.py +++ b/tests/test_prompts.py @@ -86,7 +86,7 @@ def test_java_partial(self): "java", scripts_dir="/tmp/scripts", client_filename="api_client.java", - run_command="mvn -q compile exec:java", + run_command="mvn -q compile exec:exec", ) assert "Java program" in text assert "HttpClient" in text