Skip to content

Commit 5ba95f6

Browse files
committed
Fix Windows CLI path resolution and extract shared TestUtil
modified: src/test/java/com/github/copilot/sdk/CopilotClientTest.java - Removed duplicate `getCliPath()` and `findCopilotInPath()` methods; delegates to `TestUtil.findCliPath()` - Removed unused imports (`BufferedReader`, `InputStreamReader`, `Path`, `Paths`) - Replaced 5 skip-guard blocks (`if (cliPath == null) return`) with `assertNotNull(cliPath, ...)` so tests fail instead of silently skipping modified: src/test/java/com/github/copilot/sdk/MetadataApiTest.java - Removed duplicate `getCliPath()` and `findCopilotInPath()` methods; delegates to `TestUtil.findCliPath()` - Removed unused imports (`BufferedReader`, `InputStreamReader`, `Path`, `Paths`) - Replaced 3 skip-guard blocks with `assertNotNull(cliPath, ...)` so tests fail instead of silently skipping new file: src/test/java/com/github/copilot/sdk/TestUtil.java - New package-private utility class with shared `findCliPath()` and `findCopilotInPath()` - `findCopilotInPath()` iterates all `where.exe` results on Windows and tries launching each candidate, fixing CreateProcess error 193 caused by a Linux ELF binary appearing first in PATH - Resolution order: PATH search, then COPILOT_CLI_PATH env var, then parent-directory walk for nodejs module Signed-off-by: Ed Burns <edburns@microsoft.com>
1 parent cc3e09a commit 5ba95f6

File tree

3 files changed

+112
-133
lines changed

3 files changed

+112
-133
lines changed

src/test/java/com/github/copilot/sdk/CopilotClientTest.java

Lines changed: 6 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,7 @@
1414
import com.github.copilot.sdk.json.SessionLifecycleEvent;
1515
import com.github.copilot.sdk.json.SessionLifecycleEventTypes;
1616

17-
import java.io.BufferedReader;
18-
import java.io.InputStreamReader;
1917
import java.lang.reflect.Field;
20-
import java.nio.file.Path;
21-
import java.nio.file.Paths;
2218
import java.util.ArrayList;
2319
import java.util.concurrent.CompletableFuture;
2420
import java.util.concurrent.ExecutionException;
@@ -38,53 +34,7 @@ public class CopilotClientTest {
3834

3935
@BeforeAll
4036
static void setup() {
41-
cliPath = getCliPath();
42-
}
43-
44-
private static String getCliPath() {
45-
// First, try to find 'copilot' in PATH
46-
String copilotInPath = findCopilotInPath();
47-
if (copilotInPath != null) {
48-
return copilotInPath;
49-
}
50-
51-
// Fall back to COPILOT_CLI_PATH environment variable
52-
String envPath = System.getenv("COPILOT_CLI_PATH");
53-
if (envPath != null && !envPath.isEmpty()) {
54-
return envPath;
55-
}
56-
57-
// Search for the CLI in the parent directories (nodejs module)
58-
Path current = Paths.get(System.getProperty("user.dir"));
59-
while (current != null) {
60-
Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/index.js");
61-
if (cliPath.toFile().exists()) {
62-
return cliPath.toString();
63-
}
64-
current = current.getParent();
65-
}
66-
67-
return null;
68-
}
69-
70-
private static String findCopilotInPath() {
71-
try {
72-
// Use 'where' on Windows, 'which' on Unix-like systems
73-
String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which";
74-
var pb = new ProcessBuilder(command, "copilot");
75-
pb.redirectErrorStream(true);
76-
Process process = pb.start();
77-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
78-
String line = reader.readLine();
79-
int exitCode = process.waitFor();
80-
if (exitCode == 0 && line != null && !line.isEmpty()) {
81-
return line.trim();
82-
}
83-
}
84-
} catch (Exception e) {
85-
// Ignore - copilot not found in PATH
86-
}
87-
return null;
37+
cliPath = TestUtil.findCliPath();
8838
}
8939

9040
@Test
@@ -133,10 +83,7 @@ void testCliUrlMutualExclusionWithCliPath() {
13383

13484
@Test
13585
void testStartAndConnectUsingStdio() throws Exception {
136-
if (cliPath == null) {
137-
System.out.println("Skipping test: CLI not found");
138-
return;
139-
}
86+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
14087

14188
try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) {
14289
client.start().get();
@@ -153,10 +100,7 @@ void testStartAndConnectUsingStdio() throws Exception {
153100

154101
@Test
155102
void testShouldReportErrorWithStderrWhenCliFailsToStart() throws Exception {
156-
if (cliPath == null) {
157-
System.out.println("Skipping test: CLI not found");
158-
return;
159-
}
103+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
160104

161105
var options = new CopilotClientOptions().setCliPath(cliPath)
162106
.setCliArgs(new String[]{"--nonexistent-flag-for-testing"}).setUseStdio(true);
@@ -173,10 +117,7 @@ void testShouldReportErrorWithStderrWhenCliFailsToStart() throws Exception {
173117

174118
@Test
175119
void testStartAndConnectUsingTcp() throws Exception {
176-
if (cliPath == null) {
177-
System.out.println("Skipping test: CLI not found");
178-
return;
179-
}
120+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
180121

181122
try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(false))) {
182123
client.start().get();
@@ -191,10 +132,7 @@ void testStartAndConnectUsingTcp() throws Exception {
191132

192133
@Test
193134
void testForceStopWithoutCleanup() throws Exception {
194-
if (cliPath == null) {
195-
System.out.println("Skipping test: CLI not found");
196-
return;
197-
}
135+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
198136

199137
try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) {
200138
client.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
@@ -448,10 +386,7 @@ void testForceStopWithNoConnectionCompletes() throws Exception {
448386

449387
@Test
450388
void testCloseSessionAfterStoppingClientDoesNotThrow() throws Exception {
451-
if (cliPath == null) {
452-
System.out.println("Skipping test: CLI not found");
453-
return;
454-
}
389+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
455390

456391
try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath))) {
457392
var session = client

src/test/java/com/github/copilot/sdk/MetadataApiTest.java

Lines changed: 4 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,6 @@
1111
import org.junit.jupiter.api.BeforeAll;
1212
import org.junit.jupiter.api.Test;
1313

14-
import java.io.BufferedReader;
15-
import java.io.InputStreamReader;
16-
import java.nio.file.Path;
17-
import java.nio.file.Paths;
1814
import java.util.List;
1915

2016
import static org.junit.jupiter.api.Assertions.*;
@@ -30,52 +26,7 @@ public class MetadataApiTest {
3026

3127
@BeforeAll
3228
static void setup() {
33-
cliPath = getCliPath();
34-
}
35-
36-
private static String getCliPath() {
37-
// First, try to find 'copilot' in PATH
38-
String copilotInPath = findCopilotInPath();
39-
if (copilotInPath != null) {
40-
return copilotInPath;
41-
}
42-
43-
// Fall back to COPILOT_CLI_PATH environment variable
44-
String envPath = System.getenv("COPILOT_CLI_PATH");
45-
if (envPath != null && !envPath.isEmpty()) {
46-
return envPath;
47-
}
48-
49-
// Search for the CLI in the parent directories (nodejs module)
50-
Path current = Paths.get(System.getProperty("user.dir"));
51-
while (current != null) {
52-
Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/index.js");
53-
if (cliPath.toFile().exists()) {
54-
return cliPath.toString();
55-
}
56-
current = current.getParent();
57-
}
58-
59-
return null;
60-
}
61-
62-
private static String findCopilotInPath() {
63-
try {
64-
String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which";
65-
var pb = new ProcessBuilder(command, "copilot");
66-
pb.redirectErrorStream(true);
67-
Process process = pb.start();
68-
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
69-
String line = reader.readLine();
70-
int exitCode = process.waitFor();
71-
if (exitCode == 0 && line != null && !line.isEmpty()) {
72-
return line.trim();
73-
}
74-
}
75-
} catch (Exception e) {
76-
// Ignore - copilot not found in PATH
77-
}
78-
return null;
29+
cliPath = TestUtil.findCliPath();
7930
}
8031

8132
// ===== ToolExecutionProgressEvent Tests =====
@@ -262,10 +213,7 @@ void testGetModelsResponseDeserialization() throws Exception {
262213

263214
@Test
264215
void testGetStatus() throws Exception {
265-
if (cliPath == null) {
266-
System.out.println("Skipping test: CLI not found");
267-
return;
268-
}
216+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
269217

270218
try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) {
271219
client.start().get();
@@ -281,10 +229,7 @@ void testGetStatus() throws Exception {
281229

282230
@Test
283231
void testGetAuthStatus() throws Exception {
284-
if (cliPath == null) {
285-
System.out.println("Skipping test: CLI not found");
286-
return;
287-
}
232+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
288233

289234
try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) {
290235
client.start().get();
@@ -299,10 +244,7 @@ void testGetAuthStatus() throws Exception {
299244

300245
@Test
301246
void testListModels() throws Exception {
302-
if (cliPath == null) {
303-
System.out.println("Skipping test: CLI not found");
304-
return;
305-
}
247+
assertNotNull(cliPath, "Copilot CLI not found in PATH or COPILOT_CLI_PATH");
306248

307249
try (var client = new CopilotClient(new CopilotClientOptions().setCliPath(cliPath).setUseStdio(true))) {
308250
client.start().get();
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk;
6+
7+
import java.io.BufferedReader;
8+
import java.io.InputStreamReader;
9+
import java.nio.file.Path;
10+
import java.nio.file.Paths;
11+
12+
/**
13+
* Shared test utilities for locating the Copilot CLI binary.
14+
*/
15+
final class TestUtil {
16+
17+
private TestUtil() {
18+
}
19+
20+
/**
21+
* Locates a launchable Copilot CLI executable.
22+
* <p>
23+
* Resolution order:
24+
* <ol>
25+
* <li>Search the system PATH using {@code where.exe} (Windows) or
26+
* {@code which} (Linux/macOS).</li>
27+
* <li>Fall back to the {@code COPILOT_CLI_PATH} environment variable.</li>
28+
* <li>Walk parent directories looking for
29+
* {@code nodejs/node_modules/@github/copilot/index.js}.</li>
30+
* </ol>
31+
*
32+
* <p>
33+
* <b>Why iterate all PATH results?</b> On Windows, {@code where.exe copilot}
34+
* can return multiple candidates. The first hit is often a Linux ELF binary
35+
* bundled inside the VS Code Insiders extension directory — it exists on disk
36+
* but cannot be executed by {@link ProcessBuilder} (CreateProcess error 193).
37+
* This method tries each candidate with {@code --version} and returns the first
38+
* one that actually launches, skipping non-executable entries.
39+
*
40+
* @return the absolute path to a launchable {@code copilot} binary, or
41+
* {@code null} if none was found
42+
*/
43+
static String findCliPath() {
44+
String copilotInPath = findCopilotInPath();
45+
if (copilotInPath != null) {
46+
return copilotInPath;
47+
}
48+
49+
String envPath = System.getenv("COPILOT_CLI_PATH");
50+
if (envPath != null && !envPath.isEmpty()) {
51+
return envPath;
52+
}
53+
54+
Path current = Paths.get(System.getProperty("user.dir"));
55+
while (current != null) {
56+
Path cliPath = current.resolve("nodejs/node_modules/@github/copilot/index.js");
57+
if (cliPath.toFile().exists()) {
58+
return cliPath.toString();
59+
}
60+
current = current.getParent();
61+
}
62+
63+
return null;
64+
}
65+
66+
/**
67+
* Searches the system PATH for a launchable {@code copilot} executable.
68+
* <p>
69+
* Uses {@code where.exe} on Windows and {@code which} on Unix-like systems.
70+
* On Windows, {@code where.exe} may return multiple results (e.g. a Linux ELF
71+
* binary, a {@code .bat} wrapper, a {@code .cmd} wrapper). This method iterates
72+
* all results and returns the first one that {@link ProcessBuilder} can actually
73+
* start.
74+
*/
75+
private static String findCopilotInPath() {
76+
try {
77+
String command = System.getProperty("os.name").toLowerCase().contains("win") ? "where" : "which";
78+
var pb = new ProcessBuilder(command, "copilot");
79+
pb.redirectErrorStream(true);
80+
Process process = pb.start();
81+
try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
82+
int exitCode = process.waitFor();
83+
if (exitCode != 0) {
84+
return null;
85+
}
86+
var lines = reader.lines().map(String::trim).filter(l -> !l.isEmpty()).toList();
87+
for (String candidate : lines) {
88+
try {
89+
new ProcessBuilder(candidate, "--version")
90+
.redirectErrorStream(true).start().destroyForcibly();
91+
return candidate;
92+
} catch (Exception launchFailed) {
93+
// Not launchable on this platform — try next candidate
94+
}
95+
}
96+
}
97+
} catch (Exception e) {
98+
// Ignore - copilot not found in PATH
99+
}
100+
return null;
101+
}
102+
}

0 commit comments

Comments
 (0)