/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
package com.github.copilot.sdk;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
/**
* Manages a replaying proxy server for E2E tests.
*
*
* This spawns the shared test harness server from test/harness/server.ts which
* acts as a replaying proxy to AI endpoints. It captures and stores
* request/response pairs in YAML snapshot files and replays stored responses on
* subsequent runs for deterministic testing.
*
*
*
* Usage example:
*
*
*
* {@code
* CapiProxy proxy = new CapiProxy();
* String proxyUrl = proxy.start();
*
* // Configure for a specific test
* proxy.configure("test/snapshots/tools/my_test.yaml", workDir);
*
* // ... run tests with proxyUrl ...
*
* // Get captured exchanges
* List
*/
public class CapiProxy implements AutoCloseable {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final Pattern LISTENING_PATTERN = Pattern.compile("Listening: (http://[^\\s]+)(?:\\s+(\\{.*\\}))?$");
private Process process;
private String proxyUrl;
private String connectProxyUrl;
private String caFilePath;
private final HttpClient httpClient;
private BufferedReader stdoutReader;
public CapiProxy() {
this.httpClient = HttpClient.newHttpClient();
}
/**
* Starts the proxy server and returns its URL.
*
* @return the proxy URL (e.g., "http://localhost:12345")
* @throws IOException
* if the server fails to start
* @throws InterruptedException
* if the startup is interrupted
*/
public String start() throws IOException, InterruptedException {
if (proxyUrl != null) {
return proxyUrl;
}
// Find the repo root by looking for the test/harness directory
Path harnessDir = findHarnessDirectory();
if (harnessDir == null) {
throw new IOException("Could not find test/harness directory. "
+ "Make sure you are running from within the copilot-sdk repository.");
}
// Start the harness server using npx tsx
// On Windows, npx is installed as npx.cmd which requires cmd /c to launch
boolean isWindows = System.getProperty("os.name").toLowerCase().contains("win");
var pb = isWindows
? new ProcessBuilder("cmd", "/c", "npx", "tsx", "server.ts")
: new ProcessBuilder("npx", "tsx", "server.ts");
pb.directory(harnessDir.toFile());
pb.redirectErrorStream(false);
process = pb.start();
// Read stdout to get the listening URL
// Note: We keep the reader open to avoid closing the process input stream
stdoutReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
// Also consume stderr in a background thread to prevent blocking
Thread stderrThread = new Thread(() -> {
try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String errLine;
while ((errLine = errReader.readLine()) != null) {
System.err.println("[CapiProxy stderr] " + errLine);
}
} catch (IOException e) {
// Ignore
}
});
stderrThread.setDaemon(true);
stderrThread.start();
String line = stdoutReader.readLine();
if (line == null) {
// Try to get error info
StringBuilder errInfo = new StringBuilder();
try (BufferedReader errReader = new BufferedReader(new InputStreamReader(process.getErrorStream()))) {
String errLine;
while ((errLine = errReader.readLine()) != null) {
errInfo.append(errLine).append("\n");
}
}
process.destroyForcibly();
throw new IOException("Failed to read proxy URL - server may have crashed. Stderr: " + errInfo);
}
Matcher matcher = LISTENING_PATTERN.matcher(line);
if (!matcher.find()) {
process.destroyForcibly();
throw new IOException("Unexpected proxy output: " + line);
}
String url = matcher.group(1);
// Parse optional metadata (CONNECT proxy details)
String metadata = matcher.group(2);
if (metadata != null && !metadata.isEmpty()) {
try {
Map meta = MAPPER.readValue(metadata, new TypeReference