Skip to content

Commit b8b7a7d

Browse files
committed
test: refactor CopilotSessionTest to use String overloads for send methods and add SessionHandlerTest for internal handler methods
1 parent 1c48adb commit b8b7a7d

File tree

2 files changed

+327
-3
lines changed

2 files changed

+327
-3
lines changed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,8 @@ void testSendReturnsImmediatelyWhileEventsStreamInBackground() throws Exception
196196
});
197197

198198
// Use a slow command so we can verify send() returns before completion
199-
session.send(new MessageOptions().setPrompt("Run 'sleep 2 && echo done'")).get();
199+
// Use String convenience overload (covers send(String) path)
200+
session.send("Run 'sleep 2 && echo done'").get();
200201

201202
// At this point, we might not have received session.idle yet
202203
// The event handling happens asynchronously
@@ -231,8 +232,8 @@ void testSendAndWaitBlocksUntilSessionIdleAndReturnsFinalAssistantMessage() thro
231232
var events = new ArrayList<String>();
232233
session.on(evt -> events.add(evt.getType()));
233234

234-
AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60,
235-
TimeUnit.SECONDS);
235+
// Use String convenience overload (covers sendAndWait(String) path)
236+
AssistantMessageEvent response = session.sendAndWait("What is 2+2?").get(60, TimeUnit.SECONDS);
236237

237238
assertNotNull(response);
238239
assertEquals("assistant.message", response.getType());
Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
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.List;
10+
import java.util.Map;
11+
import java.util.concurrent.CompletableFuture;
12+
import java.util.concurrent.ExecutionException;
13+
14+
import org.junit.jupiter.api.BeforeEach;
15+
import org.junit.jupiter.api.Test;
16+
17+
import com.fasterxml.jackson.databind.JsonNode;
18+
import com.fasterxml.jackson.databind.ObjectMapper;
19+
import com.github.copilot.sdk.json.PermissionRequestResult;
20+
import com.github.copilot.sdk.json.SessionEndHookOutput;
21+
import com.github.copilot.sdk.json.SessionHooks;
22+
import com.github.copilot.sdk.json.SessionStartHookOutput;
23+
import com.github.copilot.sdk.json.ToolDefinition;
24+
import com.github.copilot.sdk.json.UserInputRequest;
25+
import com.github.copilot.sdk.json.UserInputResponse;
26+
import com.github.copilot.sdk.json.UserPromptSubmittedHookOutput;
27+
28+
/**
29+
* Unit tests for CopilotSession internal handler methods.
30+
* <p>
31+
* Tests package-private handler and hook dispatch logic that doesn't require a
32+
* live CLI connection.
33+
*/
34+
public class SessionHandlerTest {
35+
36+
private static final ObjectMapper MAPPER = JsonRpcClient.getObjectMapper();
37+
38+
private CopilotSession session;
39+
40+
@BeforeEach
41+
void setup() throws Exception {
42+
var constructor = CopilotSession.class.getDeclaredConstructor(String.class, JsonRpcClient.class, String.class);
43+
constructor.setAccessible(true);
44+
session = constructor.newInstance("handler-test-session", null, null);
45+
}
46+
47+
// ===== setEventErrorPolicy =====
48+
49+
@Test
50+
void testSetEventErrorPolicyNullThrowsNPE() {
51+
assertThrows(NullPointerException.class, () -> session.setEventErrorPolicy(null));
52+
}
53+
54+
@Test
55+
void testSetEventErrorPolicySetsValue() {
56+
session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS);
57+
// No exception means success; the policy is stored internally
58+
}
59+
60+
// ===== handlePermissionRequest: no handler registered =====
61+
62+
@Test
63+
void testHandlePermissionRequestWithNoHandlerReturnsDenied() throws Exception {
64+
JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file", "resource", "/tmp/test"));
65+
66+
PermissionRequestResult result = session.handlePermissionRequest(data).get();
67+
68+
assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.getKind());
69+
}
70+
71+
// ===== handlePermissionRequest: handler throws =====
72+
73+
@Test
74+
void testHandlePermissionRequestHandlerExceptionReturnsDenied() throws Exception {
75+
session.registerPermissionHandler((request, invocation) -> {
76+
throw new RuntimeException("handler boom");
77+
});
78+
79+
JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file"));
80+
81+
PermissionRequestResult result = session.handlePermissionRequest(data).get();
82+
83+
assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.getKind());
84+
}
85+
86+
// ===== handlePermissionRequest: handler future fails =====
87+
88+
@Test
89+
void testHandlePermissionRequestHandlerFutureFailsReturnsDenied() throws Exception {
90+
session.registerPermissionHandler(
91+
(request, invocation) -> CompletableFuture.failedFuture(new RuntimeException("async handler boom")));
92+
93+
JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file"));
94+
95+
PermissionRequestResult result = session.handlePermissionRequest(data).get();
96+
97+
assertEquals("denied-no-approval-rule-and-could-not-request-from-user", result.getKind());
98+
}
99+
100+
// ===== handlePermissionRequest: handler succeeds =====
101+
102+
@Test
103+
void testHandlePermissionRequestHandlerSucceeds() throws Exception {
104+
session.registerPermissionHandler((request, invocation) -> {
105+
assertEquals("handler-test-session", invocation.getSessionId());
106+
var res = new PermissionRequestResult();
107+
res.setKind("allow");
108+
return CompletableFuture.completedFuture(res);
109+
});
110+
111+
JsonNode data = MAPPER.valueToTree(Map.of("tool", "read_file"));
112+
113+
PermissionRequestResult result = session.handlePermissionRequest(data).get();
114+
115+
assertEquals("allow", result.getKind());
116+
}
117+
118+
// ===== handleUserInputRequest: no handler registered =====
119+
120+
@Test
121+
void testHandleUserInputRequestNoHandler() {
122+
var request = new UserInputRequest();
123+
124+
ExecutionException ex = assertThrows(ExecutionException.class,
125+
() -> session.handleUserInputRequest(request).get());
126+
assertInstanceOf(IllegalStateException.class, ex.getCause());
127+
}
128+
129+
// ===== handleUserInputRequest: handler throws synchronously =====
130+
131+
@Test
132+
void testHandleUserInputRequestHandlerThrowsSynchronously() {
133+
session.registerUserInputHandler((req, invocation) -> {
134+
throw new RuntimeException("sync user input boom");
135+
});
136+
137+
var request = new UserInputRequest();
138+
139+
ExecutionException ex = assertThrows(ExecutionException.class,
140+
() -> session.handleUserInputRequest(request).get());
141+
assertInstanceOf(RuntimeException.class, ex.getCause());
142+
}
143+
144+
// ===== handleUserInputRequest: handler future fails =====
145+
146+
@Test
147+
void testHandleUserInputRequestHandlerFutureFails() {
148+
session.registerUserInputHandler(
149+
(req, invocation) -> CompletableFuture.failedFuture(new RuntimeException("async user input boom")));
150+
151+
var request = new UserInputRequest();
152+
153+
ExecutionException ex = assertThrows(ExecutionException.class,
154+
() -> session.handleUserInputRequest(request).get());
155+
assertInstanceOf(RuntimeException.class, ex.getCause());
156+
}
157+
158+
// ===== handleUserInputRequest: handler succeeds =====
159+
160+
@Test
161+
void testHandleUserInputRequestHandlerSucceeds() throws Exception {
162+
session.registerUserInputHandler((req, invocation) -> {
163+
assertEquals("handler-test-session", invocation.getSessionId());
164+
return CompletableFuture.completedFuture(new UserInputResponse().setAnswer("user typed this"));
165+
});
166+
167+
var request = new UserInputRequest();
168+
169+
UserInputResponse response = session.handleUserInputRequest(request).get();
170+
171+
assertEquals("user typed this", response.getAnswer());
172+
}
173+
174+
// ===== handleHooksInvoke: no hooks registered =====
175+
176+
@Test
177+
void testHandleHooksInvokeNoHooksReturnsNull() throws Exception {
178+
JsonNode input = MAPPER.valueToTree(Map.of());
179+
180+
Object result = session.handleHooksInvoke("preToolUse", input).get();
181+
182+
assertNull(result);
183+
}
184+
185+
// ===== handleHooksInvoke: userPromptSubmitted =====
186+
187+
@Test
188+
void testHandleHooksInvokeUserPromptSubmitted() throws Exception {
189+
var hooks = new SessionHooks().setOnUserPromptSubmitted((hookInput, invocation) -> {
190+
assertEquals("handler-test-session", invocation.getSessionId());
191+
return CompletableFuture
192+
.completedFuture(new UserPromptSubmittedHookOutput("modified prompt", "extra context", false));
193+
});
194+
session.registerHooks(hooks);
195+
196+
JsonNode input = MAPPER
197+
.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "prompt", "original prompt"));
198+
199+
Object result = session.handleHooksInvoke("userPromptSubmitted", input).get();
200+
201+
assertInstanceOf(UserPromptSubmittedHookOutput.class, result);
202+
var output = (UserPromptSubmittedHookOutput) result;
203+
assertEquals("modified prompt", output.modifiedPrompt());
204+
}
205+
206+
// ===== handleHooksInvoke: sessionStart =====
207+
208+
@Test
209+
void testHandleHooksInvokeSessionStart() throws Exception {
210+
var hooks = new SessionHooks().setOnSessionStart((hookInput, invocation) -> {
211+
assertEquals("handler-test-session", invocation.getSessionId());
212+
return CompletableFuture.completedFuture(new SessionStartHookOutput("additional context", null));
213+
});
214+
session.registerHooks(hooks);
215+
216+
JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test"));
217+
218+
Object result = session.handleHooksInvoke("sessionStart", input).get();
219+
220+
assertInstanceOf(SessionStartHookOutput.class, result);
221+
var output = (SessionStartHookOutput) result;
222+
assertEquals("additional context", output.additionalContext());
223+
}
224+
225+
// ===== handleHooksInvoke: sessionEnd =====
226+
227+
@Test
228+
void testHandleHooksInvokeSessionEnd() throws Exception {
229+
var hooks = new SessionHooks().setOnSessionEnd((hookInput, invocation) -> {
230+
assertEquals("handler-test-session", invocation.getSessionId());
231+
return CompletableFuture.completedFuture(new SessionEndHookOutput(false, null, "summary"));
232+
});
233+
session.registerHooks(hooks);
234+
235+
JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "reason", "user_closed"));
236+
237+
Object result = session.handleHooksInvoke("sessionEnd", input).get();
238+
239+
assertInstanceOf(SessionEndHookOutput.class, result);
240+
var output = (SessionEndHookOutput) result;
241+
assertEquals("summary", output.sessionSummary());
242+
}
243+
244+
// ===== handleHooksInvoke: unhandled hook type =====
245+
246+
@Test
247+
void testHandleHooksInvokeUnhandledHookType() throws Exception {
248+
session.registerHooks(new SessionHooks());
249+
250+
JsonNode input = MAPPER.valueToTree(Map.of());
251+
252+
Object result = session.handleHooksInvoke("unknownHookType", input).get();
253+
254+
assertNull(result);
255+
}
256+
257+
// ===== handleHooksInvoke: handler throws =====
258+
259+
@Test
260+
void testHandleHooksInvokeHandlerThrows() throws Exception {
261+
var hooks = new SessionHooks().setOnSessionStart((hookInput, invocation) -> {
262+
throw new RuntimeException("hook boom");
263+
});
264+
session.registerHooks(hooks);
265+
266+
JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test"));
267+
268+
ExecutionException ex = assertThrows(ExecutionException.class,
269+
() -> session.handleHooksInvoke("sessionStart", input).get());
270+
assertInstanceOf(RuntimeException.class, ex.getCause());
271+
}
272+
273+
// ===== handleHooksInvoke: invalid JSON for hook input =====
274+
275+
@Test
276+
void testHandleHooksInvokeInvalidJsonFails() throws Exception {
277+
var hooks = new SessionHooks().setOnSessionStart(
278+
(hookInput, invocation) -> CompletableFuture.completedFuture(new SessionStartHookOutput(null, null)));
279+
session.registerHooks(hooks);
280+
281+
// Pass an array node which can't be deserialized into SessionStartHookInput
282+
JsonNode input = MAPPER.valueToTree(List.of("not", "an", "object"));
283+
284+
ExecutionException ex = assertThrows(ExecutionException.class,
285+
() -> session.handleHooksInvoke("sessionStart", input).get());
286+
assertInstanceOf(Exception.class, ex.getCause());
287+
}
288+
289+
// ===== handleHooksInvoke: hook handler with null callback =====
290+
291+
@Test
292+
void testHandleHooksInvokeNullCallbackReturnsNull() throws Exception {
293+
// SessionHooks with only userPromptSubmitted set, sessionStart is null
294+
var hooks = new SessionHooks().setOnUserPromptSubmitted((hookInput, invocation) -> CompletableFuture
295+
.completedFuture(new UserPromptSubmittedHookOutput(null, null, null)));
296+
session.registerHooks(hooks);
297+
298+
// Invoke sessionStart hook - its handler is null
299+
JsonNode input = MAPPER.valueToTree(Map.of("timestamp", 1735689600L, "cwd", "/tmp", "source", "test"));
300+
301+
Object result = session.handleHooksInvoke("sessionStart", input).get();
302+
303+
assertNull(result);
304+
}
305+
306+
// ===== registerTools =====
307+
308+
@Test
309+
void testRegisterToolsNullIsSafe() {
310+
session.registerTools(null);
311+
assertNull(session.getTool("anything"));
312+
}
313+
314+
@Test
315+
void testRegisterToolsEmptyListClearsTools() {
316+
session.registerTools(List.of(ToolDefinition.create("my_tool", "desc", Map.of(),
317+
invocation -> CompletableFuture.completedFuture("result"))));
318+
assertNotNull(session.getTool("my_tool"));
319+
320+
session.registerTools(List.of());
321+
assertNull(session.getTool("my_tool"));
322+
}
323+
}

0 commit comments

Comments
 (0)