Skip to content

Commit 4550721

Browse files
rojiCopilot
andcommitted
Stabilize Java E2E fixture handling
Parse folded YAML prompt scalars correctly and share replay proxy HTTP resources across the monolithic test JVM. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 9e70dd5 commit 4550721

4 files changed

Lines changed: 166 additions & 23 deletions

File tree

‎java/sdk/src/test/java/com/github/copilot/CapiProxy.java‎

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,15 @@ public class CapiProxy implements AutoCloseable {
5757

5858
private static final ObjectMapper MAPPER = new ObjectMapper();
5959
private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)(?:\\s+(\\{.*\\}))?$");
60+
private static final HttpClient HTTP_CLIENT = HttpClient.newHttpClient();
6061

6162
private Process process;
6263
private String proxyUrl;
6364
private String connectProxyUrl;
6465
private String caFilePath;
65-
private final HttpClient httpClient;
6666
private BufferedReader stdoutReader;
6767

6868
public CapiProxy() {
69-
this.httpClient = HttpClient.newHttpClient();
7069
}
7170

7271
/**
@@ -212,7 +211,7 @@ public void configure(String filePath, String workDir, TestInfo testInfo) throws
212211
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/config"))
213212
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build();
214213

215-
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
214+
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
216215
if (response.statusCode() != 200) {
217216
throw new IOException("Proxy config failed with status " + response.statusCode() + ": " + response.body());
218217
}
@@ -234,7 +233,7 @@ public List<Map<String, Object>> getExchanges() throws IOException, InterruptedE
234233

235234
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/exchanges")).GET().build();
236235

237-
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
236+
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
238237
if (response.statusCode() != 200) {
239238
throw new IOException("Failed to get exchanges: " + response.statusCode());
240239
}
@@ -284,7 +283,7 @@ public void setCopilotUserByToken(String token, String login, String copilotPlan
284283
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/copilot-user-config"))
285284
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build();
286285

287-
HttpResponse<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
286+
HttpResponse<String> response = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
288287
if (response.statusCode() != 200) {
289288
throw new IOException(
290289
"Failed to set copilot user config: " + response.statusCode() + ": " + response.body());
@@ -331,7 +330,7 @@ public void setCopilotUserByToken(String token, Map<String, Object> response)
331330
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(proxyUrl + "/copilot-user-config"))
332331
.header("Content-Type", "application/json").POST(HttpRequest.BodyPublishers.ofString(body)).build();
333332

334-
HttpResponse<String> response2 = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
333+
HttpResponse<String> response2 = HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
335334
if (response2.statusCode() != 200) {
336335
throw new IOException(
337336
"Failed to set copilot user config: " + response2.statusCode() + ": " + response2.body());
@@ -376,7 +375,7 @@ public void stop(boolean skipWritingCache) throws IOException, InterruptedExcept
376375
HttpRequest request = HttpRequest.newBuilder().uri(URI.create(stopUrl))
377376
.POST(HttpRequest.BodyPublishers.noBody()).build();
378377

379-
httpClient.send(request, HttpResponse.BodyHandlers.ofString());
378+
HTTP_CLIENT.send(request, HttpResponse.BodyHandlers.ofString());
380379
} catch (Exception e) {
381380
// Best effort - ignore errors
382381
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot;
6+
7+
import static org.junit.jupiter.api.Assertions.assertTrue;
8+
9+
import java.lang.reflect.Modifier;
10+
11+
import org.junit.jupiter.api.Test;
12+
13+
class CapiProxyTest {
14+
15+
@Test
16+
void proxyInstancesShareHttpClientResources() throws Exception {
17+
var field = CapiProxy.class.getDeclaredField("HTTP_CLIENT");
18+
assertTrue(Modifier.isStatic(field.getModifiers()),
19+
"E2E contexts must not retain one HttpClient selector manager per test class");
20+
}
21+
}

‎java/sdk/src/test/java/com/github/copilot/E2ETestContext.java‎

Lines changed: 100 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,8 +65,8 @@ public class E2ETestContext implements AutoCloseable {
6565
*/
6666
private static final String DEFAULT_GITHUB_TOKEN = "fake-token-for-e2e-tests";
6767
private static final Pattern SNAKE_CASE = Pattern.compile("[^a-zA-Z0-9]");
68-
private static final Pattern USER_CONTENT_PATTERN = Pattern
69-
.compile("^\\s+-\\s+role:\\s+user\\s*$\\s+content:\\s*(.+?)$", Pattern.MULTILINE);
68+
private static final Pattern USER_ROLE_PATTERN = Pattern.compile("^(\\s*)-\\s+role:\\s+user\\s*$");
69+
private static final Pattern CONTENT_PATTERN = Pattern.compile("^(\\s*)content:\\s*(.*)$");
7070

7171
private final String cliPath;
7272
private final Path homeDir;
@@ -227,25 +227,109 @@ public List<String> getExpectedUserPrompts() {
227227
return List.of();
228228
}
229229
try {
230-
String content = Files.readString(currentSnapshotFile);
231-
List<String> prompts = new ArrayList<>();
232-
Matcher matcher = USER_CONTENT_PATTERN.matcher(content);
233-
while (matcher.find()) {
234-
String prompt = matcher.group(1).trim();
235-
// Remove quotes if present
236-
if ((prompt.startsWith("\"") && prompt.endsWith("\""))
237-
|| (prompt.startsWith("'") && prompt.endsWith("'"))) {
238-
prompt = prompt.substring(1, prompt.length() - 1);
230+
return parseExpectedUserPrompts(Files.readString(currentSnapshotFile));
231+
} catch (IOException e) {
232+
LOG.warning("Failed to read snapshot file: " + e.getMessage());
233+
return List.of();
234+
}
235+
}
236+
237+
static List<String> parseExpectedUserPrompts(String yaml) {
238+
String[] lines = yaml.split("\\R", -1);
239+
List<String> prompts = new ArrayList<>();
240+
241+
for (int i = 0; i < lines.length; i++) {
242+
Matcher roleMatcher = USER_ROLE_PATTERN.matcher(lines[i]);
243+
if (!roleMatcher.matches()) {
244+
continue;
245+
}
246+
247+
int roleIndent = roleMatcher.group(1).length();
248+
for (i++; i < lines.length; i++) {
249+
String line = lines[i];
250+
if (!line.isBlank() && leadingWhitespace(line) <= roleIndent) {
251+
i--;
252+
break;
239253
}
240-
if (!prompts.contains(prompt)) {
254+
255+
Matcher contentMatcher = CONTENT_PATTERN.matcher(line);
256+
if (!contentMatcher.matches()) {
257+
continue;
258+
}
259+
260+
int contentIndent = contentMatcher.group(1).length();
261+
String value = contentMatcher.group(2).trim();
262+
List<String> continuation = new ArrayList<>();
263+
while (i + 1 < lines.length
264+
&& (lines[i + 1].isBlank() || leadingWhitespace(lines[i + 1]) > contentIndent)) {
265+
continuation.add(lines[++i]);
266+
}
267+
268+
String prompt = isBlockScalar(value)
269+
? parseBlockScalar(value.charAt(0), continuation)
270+
: parsePlainScalar(value, continuation);
271+
if (!prompt.isEmpty() && !prompts.contains(prompt)) {
241272
prompts.add(prompt);
242273
}
274+
break;
243275
}
244-
return prompts;
245-
} catch (IOException e) {
246-
LOG.warning("Failed to read snapshot file: " + e.getMessage());
247-
return List.of();
248276
}
277+
278+
return prompts;
279+
}
280+
281+
private static String parsePlainScalar(String firstLine, List<String> continuation) {
282+
StringBuilder value = new StringBuilder(unquote(firstLine));
283+
for (String line : continuation) {
284+
if (!line.isBlank()) {
285+
if (!value.isEmpty()) {
286+
value.append(' ');
287+
}
288+
value.append(line.trim());
289+
}
290+
}
291+
return value.toString();
292+
}
293+
294+
private static String parseBlockScalar(char style, List<String> lines) {
295+
int contentIndent = lines.stream().filter(line -> !line.isBlank()).mapToInt(E2ETestContext::leadingWhitespace)
296+
.min().orElse(0);
297+
StringBuilder value = new StringBuilder();
298+
boolean previousWasContent = false;
299+
for (String line : lines) {
300+
String text = line.isBlank() ? "" : line.substring(Math.min(contentIndent, line.length()));
301+
if (text.isEmpty()) {
302+
value.append('\n');
303+
previousWasContent = false;
304+
} else {
305+
if (previousWasContent) {
306+
value.append(style == '>' ? ' ' : '\n');
307+
}
308+
value.append(text);
309+
previousWasContent = true;
310+
}
311+
}
312+
return value.toString().stripTrailing();
313+
}
314+
315+
private static boolean isBlockScalar(String value) {
316+
return value.matches("[>|][+-]?");
317+
}
318+
319+
private static String unquote(String value) {
320+
if (value.length() >= 2 && ((value.startsWith("\"") && value.endsWith("\""))
321+
|| (value.startsWith("'") && value.endsWith("'")))) {
322+
return value.substring(1, value.length() - 1);
323+
}
324+
return value;
325+
}
326+
327+
private static int leadingWhitespace(String value) {
328+
int index = 0;
329+
while (index < value.length() && Character.isWhitespace(value.charAt(index))) {
330+
index++;
331+
}
332+
return index;
249333
}
250334

251335
/**
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
9+
import java.util.List;
10+
11+
import org.junit.jupiter.api.Test;
12+
13+
class E2ETestContextTest {
14+
15+
@Test
16+
void expectedUserPromptsParseFoldedBlockScalars() {
17+
String snapshot = """
18+
conversations:
19+
- messages:
20+
- role: user
21+
content: First prompt
22+
continued here.
23+
- role: assistant
24+
content: response
25+
- role: user
26+
content: >-
27+
<system_notification>
28+
29+
Agent completed successfully.
30+
31+
</system_notification>
32+
""";
33+
34+
assertEquals(
35+
List.of("First prompt continued here.",
36+
"<system_notification>\nAgent completed successfully.\n</system_notification>"),
37+
E2ETestContext.parseExpectedUserPrompts(snapshot));
38+
}
39+
}

0 commit comments

Comments
 (0)