Skip to content

Commit 52bab0f

Browse files
committed
Add E2E tests for error handling scenarios
1 parent d23f461 commit 52bab0f

1 file changed

Lines changed: 216 additions & 0 deletions

File tree

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.sdk;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
import java.util.ArrayList;
10+
import java.util.List;
11+
import java.util.concurrent.CompletableFuture;
12+
import java.util.concurrent.TimeUnit;
13+
14+
import org.junit.jupiter.api.AfterAll;
15+
import org.junit.jupiter.api.BeforeAll;
16+
import org.junit.jupiter.api.Test;
17+
18+
import com.github.copilot.sdk.events.AbstractSessionEvent;
19+
import com.github.copilot.sdk.events.AssistantMessageEvent;
20+
import com.github.copilot.sdk.events.SessionErrorEvent;
21+
import com.github.copilot.sdk.json.MessageOptions;
22+
import com.github.copilot.sdk.json.SessionConfig;
23+
import com.github.copilot.sdk.json.ToolDefinition;
24+
25+
import java.util.Map;
26+
27+
/**
28+
* E2E tests for error handling scenarios.
29+
* <p>
30+
* These tests verify that the SDK properly handles errors in various scenarios
31+
* including tool errors, permission handler errors, and session errors.
32+
* </p>
33+
*/
34+
public class ErrorHandlingTest {
35+
36+
private static E2ETestContext ctx;
37+
38+
@BeforeAll
39+
static void setup() throws Exception {
40+
ctx = E2ETestContext.create();
41+
}
42+
43+
@AfterAll
44+
static void teardown() throws Exception {
45+
if (ctx != null) {
46+
ctx.close();
47+
}
48+
}
49+
50+
/**
51+
* Tests that tool errors are handled gracefully and don't crash the session.
52+
*/
53+
@Test
54+
void testToolErrorDoesNotCrashSession() throws Exception {
55+
ctx.configureForTest("tools", "handles_tool_calling_errors");
56+
57+
List<AbstractSessionEvent> allEvents = new ArrayList<>();
58+
59+
ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location",
60+
Map.of("type", "object", "properties", Map.of()), (invocation) -> {
61+
CompletableFuture<Object> future = new CompletableFuture<>();
62+
future.completeExceptionally(new RuntimeException("Location service unavailable"));
63+
return future;
64+
});
65+
66+
try (CopilotClient client = ctx.createClient()) {
67+
CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get();
68+
69+
session.on(event -> allEvents.add(event));
70+
71+
AssistantMessageEvent response = session
72+
.sendAndWait(new MessageOptions()
73+
.setPrompt("What is my location? If you can't find out, just say 'unknown'."))
74+
.get(60, TimeUnit.SECONDS);
75+
76+
// Session should complete without crashing
77+
assertNotNull(response, "Should receive a response even when tool fails");
78+
79+
// Should have received session.idle (indicating successful completion)
80+
assertTrue(allEvents.stream().anyMatch(e -> e instanceof com.github.copilot.sdk.events.SessionIdleEvent),
81+
"Session should reach idle state after handling tool error");
82+
83+
session.close();
84+
}
85+
}
86+
87+
/**
88+
* Tests that returning a failure result from a tool is handled properly.
89+
*/
90+
@Test
91+
void testToolReturnsFailureResult() throws Exception {
92+
ctx.configureForTest("tools", "handles_tool_calling_errors");
93+
94+
ToolDefinition failTool = ToolDefinition.create("get_user_location", "Gets the user's location",
95+
Map.of("type", "object", "properties", Map.of()), (invocation) -> {
96+
// Return a structured failure result via exception (matching the snapshot
97+
// behavior)
98+
return CompletableFuture.failedFuture(new RuntimeException("Location unavailable"));
99+
});
100+
101+
try (CopilotClient client = ctx.createClient()) {
102+
CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(failTool))).get();
103+
104+
AssistantMessageEvent response = session
105+
.sendAndWait(new MessageOptions()
106+
.setPrompt("What is my location? If you can't find out, just say 'unknown'."))
107+
.get(60, TimeUnit.SECONDS);
108+
109+
assertNotNull(response, "Should receive a response with failure result");
110+
111+
session.close();
112+
}
113+
}
114+
115+
/**
116+
* Tests that permission handler errors result in denied permission.
117+
*/
118+
@Test
119+
void testPermissionHandlerErrorDeniesPermission() throws Exception {
120+
ctx.configureForTest("permissions", "should_handle_permission_handler_errors_gracefully");
121+
122+
List<SessionErrorEvent> errorEvents = new ArrayList<>();
123+
124+
SessionConfig config = new SessionConfig().setOnPermissionRequest((request, invocation) -> {
125+
throw new RuntimeException("Permission handler crashed");
126+
});
127+
128+
try (CopilotClient client = ctx.createClient()) {
129+
CopilotSession session = client.createSession(config).get();
130+
131+
session.on(SessionErrorEvent.class, errorEvents::add);
132+
133+
AssistantMessageEvent response = session
134+
.sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'."))
135+
.get(60, TimeUnit.SECONDS);
136+
137+
// Should complete despite the error
138+
assertNotNull(response, "Should receive a response despite handler error");
139+
140+
// The response should indicate failure/inability
141+
String content = response.getData().getContent().toLowerCase();
142+
assertTrue(
143+
content.contains("fail") || content.contains("cannot") || content.contains("unable")
144+
|| content.contains("permission") || content.contains("denied"),
145+
"Response should indicate permission was denied: " + content);
146+
147+
session.close();
148+
}
149+
}
150+
151+
/**
152+
* Tests that session error events contain proper error information.
153+
*/
154+
@Test
155+
void testSessionErrorEventContainsDetails() throws Exception {
156+
ctx.configureForTest("permissions", "permission_handler_errors");
157+
158+
List<SessionErrorEvent> errorEvents = new ArrayList<>();
159+
160+
SessionConfig config = new SessionConfig().setOnPermissionRequest((request, invocation) -> {
161+
throw new RuntimeException("Test error message");
162+
});
163+
164+
try (CopilotClient client = ctx.createClient()) {
165+
CopilotSession session = client.createSession(config).get();
166+
167+
session.on(SessionErrorEvent.class, error -> {
168+
errorEvents.add(error);
169+
// Verify error event has data
170+
assertNotNull(error.getData(), "Error event should have data");
171+
});
172+
173+
try {
174+
session.sendAndWait(new MessageOptions().setPrompt("Run 'ls' command")).get(60, TimeUnit.SECONDS);
175+
} catch (Exception e) {
176+
// Error is expected in some cases
177+
}
178+
179+
session.close();
180+
}
181+
182+
// Note: Whether error events are emitted depends on the CLI version and
183+
// scenario
184+
// This test verifies the handler can receive them when they occur
185+
}
186+
187+
/**
188+
* Tests that the session continues to work after a tool error.
189+
*/
190+
@Test
191+
void testSessionContinuesAfterToolError() throws Exception {
192+
ctx.configureForTest("tools", "handles_tool_calling_errors");
193+
194+
ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location",
195+
Map.of("type", "object", "properties", Map.of()), (invocation) -> {
196+
return CompletableFuture.failedFuture(new RuntimeException("Service unavailable"));
197+
});
198+
199+
try (CopilotClient client = ctx.createClient()) {
200+
CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get();
201+
202+
// First request that will cause tool error
203+
AssistantMessageEvent response = session
204+
.sendAndWait(new MessageOptions()
205+
.setPrompt("What is my location? If you can't find out, just say 'unknown'."))
206+
.get(60, TimeUnit.SECONDS);
207+
208+
assertNotNull(response, "Should receive first response");
209+
210+
// Session should still be usable - the sendAndWait completed
211+
// This verifies the session didn't enter an error state
212+
213+
session.close();
214+
}
215+
}
216+
}

0 commit comments

Comments
 (0)