Skip to content

Commit ae8ad83

Browse files
Copilotedburns
authored andcommitted
[WIP] Achieve 100% InProcess and out-of-process test parity (#2272)
Author: Ed Burns <edburns@microsoft.com> Date: Fri Aug 7 17:29:06 2026 +0000 Fix Java in-process test lifecycle and parity Prevent the Java in-process test profile from corrupting Surefire control streams or poisoning later tests through the runtime's process-global LLM provider registration. Preserve explicit subprocess and TCP transport choices, sanitize process-only options before constructing in-process clients, and run request-handler tests over their required isolated stdio runtime. Fix the remaining test-contract issues by making fake socket RPC handler registration atomic with reader startup, honoring the configured CLI entrypoint when runtime.node is in a prebuilds directory, and isolating the streaming model-cache scenario. Remove in-process skip annotations from tests that already exercise an explicit subprocess transport. The complete `mvn clean verify -Pinprocess` run now finishes successfully without hangs, transport timeouts, provider-ownership failures, or Surefire stream corruption. File-by-file manifest: - `java/sdk/pom.xml`: use Surefire's TCP fork channel for unit and integration tests so native runtime output cannot corrupt Maven's process-pipe protocol. - `java/sdk/src/main/java/com/github/copilot/CopilotClient.java`: preserve explicitly selected TCP options when the default connection environment requests in-process transport. - `java/sdk/src/main/java/com/github/copilot/JsonRpcClient.java`: add a socket construction hook that registers handlers before the reader thread starts. - `java/sdk/src/main/java/com/github/copilot/ffi/NativeRuntimeLoader.java`: resolve the configured Copilot executable separately from runtime.node when the native library uses the package's prebuilds layout. - `java/sdk/src/main/java/com/github/copilot/rpc/CopilotClientOptions.java`: allow `setCwd(null)` to clear a previously configured working directory. - `java/sdk/src/test/java/com/github/copilot/ClientOptionsE2ETest.java`: run explicit fake-stdio option forwarding tests under the in-process profile. - `java/sdk/src/test/java/com/github/copilot/ConfigCloneTest.java`: cover clearing a configured working directory. - `java/sdk/src/test/java/com/github/copilot/CopilotClientTest.java`: remove obsolete in-process skips from explicit subprocess and TCP lifecycle tests. - `java/sdk/src/test/java/com/github/copilot/CopilotClientTransportTest.java`: test explicit transport precedence, in-process option sanitization, and TCP token selection under the profile default. - `java/sdk/src/test/java/com/github/copilot/CopilotRequestTestSupport.java`: explicitly select stdio for request-handler tests that register the process-global LLM inference provider. - `java/sdk/src/test/java/com/github/copilot/E2ETestContext.java`: honor explicit transports, route request-handler clients to subprocess isolation, and clear environment, cwd, and CLI arguments before in-process client construction. - `java/sdk/src/test/java/com/github/copilot/GitHubTelemetryTest.java`: register fake runtime RPC handlers before socket message processing begins. - `java/sdk/src/test/java/com/github/copilot/MetadataApiTest.java`: run explicit stdio metadata tests instead of skipping them under the profile. - `java/sdk/src/test/java/com/github/copilot/PerSessionAuthTest.java`: run the explicit subprocess unauthenticated case under the profile. - `java/sdk/src/test/java/com/github/copilot/RpcServerMiscE2ETest.java`: run the explicit subprocess account lifecycle case under the profile. - `java/sdk/src/test/java/com/github/copilot/StreamingFidelityTest.java`: give the gpt-5.4 reasoning/streaming scenario an isolated proxy and runtime model cache. - `java/sdk/src/test/java/com/github/copilot/ffi/FfiRuntimeHostTest.java`: cover failed connection-open cleanup followed by successful sequential startup. - `java/sdk/src/test/java/com/github/copilot/ffi/NativeRuntimeLoaderTest.java`: cover resolving a configured CLI beside a prebuilds runtime. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: e03c4e94-97b0-41ad-9f4e-c01633dc0bf7
1 parent 181da0a commit ae8ad83

28 files changed

Lines changed: 1409 additions & 99 deletions

1917-java-embed-rust-cli-runtime-remove-before-merge/1917-embed-cli-runtime-ignorance-reduction-plan.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,9 @@ The .NET PR uses MSBuild targets to copy `runtime.node` from `runtimes/<rid>/nat
135135

136136
The `package.json`-as-dependency-manifest approach was ruled out by experiment: `npm install` returns `EBADPLATFORM` for cross-platform packages, and `npm install --force` disables all npm safety checks. `npm pack` downloads the tarball without any platform check and does not require `--force`.
137137

138-
Long-term target shape: the `copilot-native` module's `generate-resources` phase runs `npm pack @github/copilot-<platform>@${project.version}` for each supported platform. This produces `.tgz` tarballs, which are then extracted with `tar` to stage the `runtime.node` binary at `target/native-staging/<classifier>/native/<classifier>/runtime.node`. The version comes from `${project.version}` — the SDK and npm package versions are identical, so no separate version property is needed.
138+
Long-term target shape: the `copilot-native` module's `generate-resources` phase runs `npm pack @github/copilot-<platform>@${project.version}` for each supported platform. This produces `.tgz` tarballs, which are then extracted with `tar` to stage **both** the `runtime.node` shared library and the `copilot` CLI executable at `target/native-staging/<classifier>/native/<classifier>/`. The version comes from `${project.version}` — the SDK and npm package versions are identical, so no separate version property is needed.
139+
140+
**Necessary-and-sufficient runtime artifact invariant:** The classifier JAR must contain both `native/<classifier>/runtime.node` (the cdylib loaded via JNA) **and** `native/<classifier>/copilot` (the CLI executable passed as `argv[0]` to `copilot_runtime_host_start`). The Rust `embedded_host.rs` spawns the CLI as a child process to service TypeScript method bodies not yet ported to Rust. Without the CLI executable, `host_start` fails — the classifier JAR is not self-sufficient. Both artifacts ship together in the same `@github/copilot-<platform>` npm package; both must be extracted and bundled. This matches the .NET SDK, which bundles the CLI binary and cdylib together under `runtimes/<rid>/native/`. When the TypeScript migration completes and `embedded_host.rs` no longer spawns a child process, the CLI executable can be removed from the classifier JAR.
139141

140142
Temporary invariant (`linux-x64` only for now): perform this only for `linux-x64` on Ubuntu 24.04 in this phase; all other platform packaging is deferred to a later phase.
141143

Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
# Prompt: make the Java InProcess test run clean
2+
3+
You are working in `/home/edburns/workareas/copilot-sdk-01`, branch
4+
`edburns/review-copilot-pr-2272`. Read these files first:
5+
6+
- `1917-java-embed-rust-cli-runtime-remove-before-merge/post-agentic-01-test-parity-fix-remaining-tests.md`
7+
- `java/20260807-0145-job-logs.txt`
8+
- the current git diff and the Java test/runtime/harness sources
9+
10+
The target command is:
11+
12+
```bash
13+
cd java
14+
mvn clean verify -Pinprocess
15+
```
16+
17+
Make the implementation and test changes necessary for a genuinely clean,
18+
non-hanging run. Do not solve this by broadly skipping tests, increasing
19+
timeouts, weakening assertions, or hiding errors. Preserve the negative-test
20+
assertions; expected negative cases may be logged, but they must not be
21+
reported as test errors.
22+
23+
## What the interrupted log establishes
24+
25+
The run was interrupted after more than an hour; it has no `BUILD SUCCESS`.
26+
There are 88 errors in 20 suites. The failures are highly clustered:
27+
28+
- `std/in stream corrupted` appears during `AskUserTest`.
29+
- `ByokBearerTokenProviderE2ETest` has the expected fake 404 in one negative
30+
case, but the other two tests fail because
31+
`llmInference.setProvider` says “Another client is already the LLM inference
32+
provider.”
33+
- The same provider-ownership error breaks
34+
`CopilotRequestCancelErrorE2ETest`, `CopilotRequestHandlerE2ETest`,
35+
`SessionConfigE2ETest`, and other provider/handler tests.
36+
- `CompactionTest`, `CopilotSessionTest`, `ErrorHandlingTest`,
37+
`EventFidelityTest`, `ExecutorWiringTest`, `HooksTest`, `McpAndAgentsTest`,
38+
`ModeHandlersTest`, `MultiProviderRegistryE2ETest`, `PermissionsTest`,
39+
`PreMcpToolCallHookTest`, `RpcSessionStateExtrasE2ETest`,
40+
`SessionConfigE2ETest`, and `SessionEventsE2ETest` contain repeated
41+
approximately 60-second `sendAndWait`/future timeouts.
42+
- `GitHubTelemetryTest` fails immediately because an InProcess connection
43+
receives `Method not found: connect` and `Method not found: ping`; determine
44+
whether this test must explicitly use the subprocess/socket transport or
45+
whether the InProcess endpoint is missing required handlers.
46+
- `RpcServerE2ETest` has a 30-second RPC timeout and
47+
`RpcSessionStateExtrasE2ETest` has a 60-second timeout.
48+
- `PerSessionAuthTest` has one skipped test and a negative 401 “Bad
49+
credentials” trace. The test itself is not an error.
50+
- `ClientOptionsE2ETest` skips all three tests. Other suites also report
51+
intentional-looking skips: `CopilotClientTest` (14),
52+
`CopilotClientTransportTest` (4), `MetadataApiTest` (3),
53+
`RpcServerMiscE2ETest` (1), and `CompactionTest` (1).
54+
- Many stack traces in `CreateSessionReKeyEntryTest`, `JsonRpcClientTest`,
55+
`LifecycleEventManagerTest`, `RpcHandlerDispatcherTest`, and
56+
`SessionHandlerTest` are deliberately generated negative-test traces and
57+
are followed by passing summaries. Do not misclassify them as failures.
58+
59+
## Priority 1: stop stream corruption and fix InProcess ownership/lifecycle
60+
61+
Investigate `std/in stream corrupted` first. Trace every process and stream
62+
created by the InProcess FFI path, `host_start`, the bundled `copilot`
63+
entrypoint, `NativeRuntimeLoader`, `InProcessRuntimeConnection`, `CapiProxy`,
64+
and Surefire. Identify which native/child process is writing bytes to the
65+
Surefire-controlled stdout/stdin protocol. Ensure child stdout/stderr are
66+
consumed or redirected in the same way as the supported transport and that
67+
the FFI receive/send streams are not closed or reused by another client.
68+
Do not merely suppress Surefire output.
69+
70+
Then fix the “Another client is already the LLM inference provider” root
71+
cause. Determine whether clients, native hosts, provider registrations, or
72+
`InProcessEnvGuard` instances survive test teardown. Verify the close path on
73+
both successful and failed `start()`, failed `createSession()`, and failed
74+
requests. Ensure a failed startup cannot leave a provider registered and that
75+
each test context closes its client/proxy/runtime deterministically. If the
76+
InProcess runtime is process-global, serialize or otherwise coordinate provider
77+
ownership rather than allowing overlapping providers. Add focused regression
78+
coverage for failed-start cleanup and sequential client startup.
79+
80+
The earlier context notes that `E2ETestContext.applyContextOptions()` must
81+
clear InProcess-incompatible `cwd` and `cliArgs` in addition to `environment`.
82+
Implement that carefully, and verify the actual setter semantics:
83+
`setEnvironment(null)` clears to an empty map, while `setCwd(null)` and
84+
`setCliArgs(null)` must be checked rather than assumed. Add or update tests so
85+
the options are truly absent according to constructor validation.
86+
87+
## Priority 2: isolate and repair the common timeout
88+
89+
After Priority 1, run small, serial selectors, not the full suite:
90+
91+
```bash
92+
cd java
93+
COPILOT_SDK_DEFAULT_CONNECTION=inprocess mvn test -pl sdk \
94+
-Dtest="AskUserTest,ByokBearerTokenProviderE2ETest,CopilotSessionTest" \
95+
-DfailIfNoTests=false
96+
```
97+
98+
Use a bounded shell timeout while debugging so a regression cannot consume an
99+
hour. For any remaining timeout, capture a thread dump and inspect the
100+
corresponding Surefire report plus replay-proxy output. Follow one request
101+
from Java JSON-RPC send, through the FFI callback/`QueueInputStream`, into the
102+
replay proxy, and back to the Java reader. Confirm that:
103+
104+
1. `host_start` returns a valid handle and the child `copilot` entrypoint is
105+
reachable.
106+
2. The request reaches the proxy with the expected snapshot.
107+
3. Every response/event is framed correctly and enqueued to the receive
108+
stream.
109+
4. stream completion/EOF and client close wake blocked readers.
110+
5. callbacks do not depend on a thread or executor that has already shut down.
111+
112+
Use `StreamingFidelityTest.testShouldEmitStreamingDeltasWithReasoningEffortConfigured`
113+
as the minimal streaming reproducer, but also test one ordinary
114+
`CopilotSessionTest` request. Do not patch each timed-out suite individually;
115+
the repeated 60-second failures indicate a shared transport or lifecycle
116+
defect. Once the common path works, rerun representative handler, hook,
117+
permission, event, session-config, MCP, and RPC-server selectors and only
118+
then the complete profile.
119+
120+
`GitHubTelemetryTest` is a separate transport-contract issue: inspect its
121+
test setup and the supported connection mode. If it intentionally uses a
122+
minimal fake RPC peer that only supports telemetry, make it explicitly select
123+
that transport so the global InProcess profile cannot route it to a runtime
124+
without `connect`/`ping`. If InProcess is intended, implement the missing
125+
protocol surface and add focused coverage.
126+
127+
## Priority 3: remove unjustified skips
128+
129+
Audit every skipped test in the log and the associated assumptions. For each:
130+
131+
- make it run under InProcess when the behavior is transport-independent;
132+
- explicitly force subprocess/socket transport when the test is specifically
133+
validating subprocess-only options or protocol behavior; or
134+
- change the test setup so the same public behavior is exercised through
135+
InProcess.
136+
137+
Do not add a profile-wide exclusion and do not convert skipped tests to
138+
passing assertions. In particular, investigate all three
139+
`ClientOptionsE2ETest` skips, the `PerSessionAuthTest` skip, and the skips in
140+
`CopilotClientTest`, `CopilotClientTransportTest`, `MetadataApiTest`,
141+
`RpcServerMiscE2ETest`, and `CompactionTest`. The final profile run should
142+
have zero skips unless a test is demonstrably impossible on the platform and
143+
the repository’s existing policy explicitly permits it; document any
144+
remaining exception in the test source.
145+
146+
## Priority 4: make expected negative output intentional
147+
148+
Do not alter assertions for negative tests. After all tests pass, reduce noisy
149+
expected stack-trace logging only where the repository’s logging conventions
150+
support it: distinguish expected test-triggered failures from unexpected
151+
transport failures, and avoid logging full stack traces at warning/error for
152+
the expected path if that can be done without hiding real failures. The
153+
`fake byok endpoint`, `401 Bad credentials`, `session.resume` not-found,
154+
handler exceptions, malformed JSON, socket-close, and re-key traces must
155+
remain asserted and diagnosable.
156+
157+
## Validation and completion criteria
158+
159+
Use the repository’s normal Java bootstrap and Maven logging conventions.
160+
Format Java changes with `mvn spotless:apply` from `java`. Run focused tests
161+
after each root-cause fix, then:
162+
163+
```bash
164+
cd java
165+
mvn clean verify -Pinprocess
166+
```
167+
168+
The task is complete only when this command terminates normally with
169+
`BUILD SUCCESS`, all test suites report zero failures and zero errors, no
170+
test hangs or 60-second transport timeouts occur, no Surefire stream
171+
corruption occurs, and the skip count is zero or each explicitly justified
172+
platform exception is documented and approved by the existing test policy.
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
# Fix remaining InProcess test parity failures
2+
3+
## Context
4+
5+
Branch: `edburns/review-copilot-pr-2272` (local worktree at `copilot-sdk-01`)
6+
Push target: `git push upstream HEAD:copilot/edburns1917-java-embed-rust-cli-runtime-post-agent`
7+
8+
The `-Pinprocess` Maven profile sets `COPILOT_SDK_DEFAULT_CONNECTION=inprocess`, which forces all E2E tests to use the InProcess FFI transport instead of subprocess. Most tests now pass. 24 tests still fail in two categories.
9+
10+
## Category 1: Tests that set `cwd` or `cliArgs` on options
11+
12+
These tests go through `ctx.createClient(options)``E2ETestContext.applyContextOptions()`. The InProcess branch absorbs `environment` into `InProcessEnvGuard` and nulls it, but does NOT do the same for `cwd` or `cliArgs`. The `CopilotClient` constructor then calls `validateEnvironmentOptions()` which rejects non-null `cwd`/`cliArgs` for InProcess.
13+
14+
**Fix:** In `E2ETestContext.applyContextOptions()`, when InProcess mode is detected, also null out `cwd` and `cliArgs` before constructing the client. For `cwd`, it's meaningless in InProcess (host process cwd is already set). For `cliArgs`, they're subprocess-specific flags.
15+
16+
Location: `java/sdk/src/test/java/com/github/copilot/E2ETestContext.java` lines 354-376
17+
18+
Current InProcess branch in `applyContextOptions`:
19+
```java
20+
if (isInProcessMode(options)) {
21+
InProcessEnvGuard guard = new InProcessEnvGuard(buildInProcessEnvironment(options));
22+
inProcessEnvGuards.add(guard);
23+
try {
24+
options.setEnvironment(null);
25+
return new CopilotClient(options, guard::close);
26+
} catch (RuntimeException e) {
27+
guard.close();
28+
throw e;
29+
}
30+
}
31+
```
32+
33+
Needs to also null `cwd` and `cliArgs`:
34+
```java
35+
options.setEnvironment(null);
36+
options.setCwd(null);
37+
options.setCliArgs(null);
38+
```
39+
40+
Affected tests: `PerSessionAuthTest` (sets cwd+environment), possibly others.
41+
42+
## Category 2: StreamingFidelityTest hang
43+
44+
`StreamingFidelityTest.testShouldEmitStreamingDeltasWithReasoningEffortConfigured` hangs indefinitely in InProcess mode. The main thread is blocked on `CompletableFuture.get()` at line 258. The JSON-RPC reader thread is reading from `QueueInputStream` (the InProcess FFI receive stream) but never receives the expected response.
45+
46+
This is a functional issue, not a validation issue. The replay proxy is running (CapiProxy thread is active), but the InProcess transport isn't completing the streaming interaction.
47+
48+
Diagnosis approach:
49+
1. Check if the test's replay snapshot exists and is correct for streaming
50+
2. Check if `host_start` succeeds for this test (serverHandle != 0)
51+
3. jstack showed the reader thread blocked in `QueueInputStream.read()` — no data arriving via the FFI callback
52+
4. Possible causes: the replay proxy response format doesn't match what the InProcess runtime expects for streaming, or the connection isn't routing correctly through the replay proxy
53+
54+
## Key architectural facts
55+
56+
- `runtime.node` is loaded via JNA. `copilot` CLI binary is spawned as child by `host_start` via `argv[0]`.
57+
- Both are now bundled in the classifier JAR at `native/<classifier>/runtime.node` and `native/<classifier>/copilot`.
58+
- `NativeRuntimeLoader.resolve()` extracts both to `~/.copilot/runtime-cache/<version>/<classifier>/`.
59+
- `NativeRuntimeLoader.resolveEntrypoint()` finds `copilot` alongside `runtime.node`.
60+
- `CopilotClient.resolveInProcessEntrypoint()` simply calls `NativeRuntimeLoader.resolveEntrypoint().toString()`.
61+
- `InProcessEnvGuard` uses JNA `libc.setenv()` to mutate the native process env (not visible to `System.getenv()`).
62+
- The replay proxy (CapiProxy) runs as a Node.js subprocess serving YAML snapshot responses.
63+
64+
## CopilotClientOptions.setEnvironment(null) quirk
65+
66+
`setEnvironment(null)` does NOT set the field to null — it calls `this.environment.clear()`, leaving an empty HashMap. `getEnvironment()` then returns a non-null empty map. The validation now checks `!isEmpty()` too (already fixed).
67+
68+
Similarly, check if `setCwd(null)` / `setCliArgs(null)` have similar behavior. If `setCwd(null)` doesn't actually null the field, the validation might still fire.
69+
70+
## Validation in CopilotClient constructor
71+
72+
```java
73+
private static void validateEnvironmentOptions(CopilotClientOptions options, RuntimeConnection connection) {
74+
if (!(connection instanceof InProcessRuntimeConnection)) return;
75+
rejectInProcessOption("Environment", options.getEnvironment() != null && !options.getEnvironment().isEmpty(), ...);
76+
rejectInProcessOption("Telemetry", options.getTelemetry() != null, ...);
77+
rejectInProcessOption("Cwd", options.getCwd() != null, ...);
78+
rejectInProcessOption("CliArgs", options.getCliArgs() != null && options.getCliArgs().length > 0, ...);
79+
}
80+
```
81+
82+
## resolveDefaultConnection precedence (already fixed)
83+
84+
When `COPILOT_SDK_DEFAULT_CONNECTION=inprocess` but `cliUrl`/`cliPath`/`port` are explicitly set, the explicit options win and subprocess transport is used. Tests like `McpAuthInterestRegistrationTest` that create `new CopilotClient(options.setCliUrl(...))` directly now correctly bypass InProcess.
85+
86+
## Full list of 24 failing test methods
87+
88+
```
89+
ByokBearerTokenProviderE2ETest (3 methods)
90+
CopilotRequestCancelErrorE2ETest (2)
91+
CopilotRequestHandlerE2ETest (2)
92+
CopilotRequestSessionIdE2ETest (1)
93+
GitHubTelemetryTest (2)
94+
McpAuthInterestRegistrationTest (3)
95+
ModeHandlersTest (2)
96+
PerSessionAuthTest (3)
97+
ProviderEndpointE2ETest (2)
98+
RpcServerE2ETest (1 - testShouldAddSecretFilterValues — NOW PASSES)
99+
SessionConfigE2ETest (2)
100+
StreamingFidelityTest (1 - hangs)
101+
SubagentHooksE2ETest (1)
102+
```
103+
104+
## Commands
105+
106+
```bash
107+
# Run all tests with InProcess
108+
cd java && mvn clean verify -Pinprocess
109+
110+
# Run specific failing tests
111+
COPILOT_SDK_DEFAULT_CONNECTION=inprocess mvn test -pl sdk -Dtest="PerSessionAuthTest,StreamingFidelityTest" -DfailIfNoTests=false
112+
113+
# Format before commit
114+
mvn spotless:apply
115+
116+
# Push
117+
git push upstream HEAD:copilot/edburns1917-java-embed-rust-cli-runtime-post-agent
118+
```
119+
120+
## Java env bootstrap (required before any mvn/java command)
121+
```bash
122+
export JAVA_HOME="/usr/lib/jvm/msopenjdk-25-amd64"
123+
export M2_HOME="${HOME}/Downloads/apache-maven-3.9.8"
124+
export PATH="${M2_HOME}/bin:${JAVA_HOME}/bin:${PATH}"
125+
```

0 commit comments

Comments
 (0)