Skip to content

Commit 6f509f0

Browse files
Copilotedburns
andauthored
Add unit and E2E tests for mode handler APIs
Co-authored-by: edburns <75821+edburns@users.noreply.github.com>
1 parent 5d7a410 commit 6f509f0

3 files changed

Lines changed: 259 additions & 0 deletions

File tree

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -375,4 +375,32 @@ void copilotClientOptionsSessionIdleTimeoutCloned() {
375375

376376
assertEquals(600, cloned.getSessionIdleTimeoutSeconds());
377377
}
378+
379+
@Test
380+
void sessionConfigCloneCopiesModeSwitchHandlers() {
381+
SessionConfig original = new SessionConfig();
382+
original.setOnExitPlanMode((request, invocation) -> CompletableFuture
383+
.completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult()));
384+
original.setOnAutoModeSwitch((request, invocation) -> CompletableFuture
385+
.completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO));
386+
387+
SessionConfig cloned = original.clone();
388+
389+
assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode());
390+
assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch());
391+
}
392+
393+
@Test
394+
void resumeSessionConfigCloneCopiesModeSwitchHandlers() {
395+
ResumeSessionConfig original = new ResumeSessionConfig();
396+
original.setOnExitPlanMode((request, invocation) -> CompletableFuture
397+
.completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult()));
398+
original.setOnAutoModeSwitch((request, invocation) -> CompletableFuture
399+
.completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO));
400+
401+
ResumeSessionConfig cloned = original.clone();
402+
403+
assertSame(original.getOnExitPlanMode(), cloned.getOnExitPlanMode());
404+
assertSame(original.getOnAutoModeSwitch(), cloned.getOnAutoModeSwitch());
405+
}
378406
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
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.HashMap;
10+
import java.util.concurrent.CompletableFuture;
11+
import java.util.concurrent.TimeUnit;
12+
13+
import org.junit.jupiter.api.AfterAll;
14+
import org.junit.jupiter.api.BeforeAll;
15+
import org.junit.jupiter.api.Test;
16+
17+
import com.github.copilot.sdk.json.AutoModeSwitchRequest;
18+
import com.github.copilot.sdk.json.AutoModeSwitchResponse;
19+
import com.github.copilot.sdk.json.CopilotClientOptions;
20+
import com.github.copilot.sdk.json.ExitPlanModeRequest;
21+
import com.github.copilot.sdk.json.ExitPlanModeResult;
22+
import com.github.copilot.sdk.json.MessageOptions;
23+
import com.github.copilot.sdk.json.PermissionHandler;
24+
import com.github.copilot.sdk.json.SessionConfig;
25+
26+
/**
27+
* Tests for mode handler (exit-plan-mode and auto-mode-switch) functionality.
28+
*
29+
* <p>
30+
* These tests use the shared CapiProxy infrastructure for deterministic API
31+
* response replay. Snapshots are stored in test/snapshots/mode_handlers/.
32+
* </p>
33+
*/
34+
public class ModeHandlersTest {
35+
36+
private static final String TOKEN = "mode-handler-token";
37+
38+
private static E2ETestContext ctx;
39+
40+
@BeforeAll
41+
static void setup() throws Exception {
42+
ctx = E2ETestContext.create();
43+
}
44+
45+
@AfterAll
46+
static void teardown() throws Exception {
47+
if (ctx != null) {
48+
ctx.close();
49+
}
50+
}
51+
52+
/**
53+
* Verifies that exit-plan-mode handler is invoked when model uses
54+
* exit_plan_mode tool.
55+
*
56+
* @see Snapshot:
57+
* mode_handlers/should_invoke_exit_plan_mode_handler_when_model_uses_tool
58+
*/
59+
@Test
60+
void testShouldInvokeExitPlanModeHandlerWhenModelUsesTool() throws Exception {
61+
ctx.configureForTest("mode_handlers", "should_invoke_exit_plan_mode_handler_when_model_uses_tool");
62+
configureAuthenticatedUser();
63+
64+
final var exitPlanModeRequests = new java.util.ArrayList<ExitPlanModeRequest>();
65+
final String[] sessionIdHolder = new String[1];
66+
67+
var config = new SessionConfig().setGitHubToken(TOKEN).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
68+
.setOnExitPlanMode((request, invocation) -> {
69+
exitPlanModeRequests.add(request);
70+
assertEquals(sessionIdHolder[0], invocation.getSessionId());
71+
return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true)
72+
.setSelectedAction("interactive").setFeedback("Approved by the Java E2E test"));
73+
});
74+
75+
try (CopilotClient client = createAuthenticatedClient()) {
76+
CopilotSession session = client.createSession(config).get();
77+
sessionIdHolder[0] = session.getSessionId();
78+
79+
var response = session.sendAndWait(new MessageOptions().setMode("plan")
80+
.setPrompt("Create a brief implementation plan for adding a greeting.txt file, "
81+
+ "then request approval with exit_plan_mode."))
82+
.get(120, TimeUnit.SECONDS);
83+
84+
assertNotNull(response, "Response should not be null");
85+
assertFalse(exitPlanModeRequests.isEmpty(), "Should have received exit plan mode requests");
86+
87+
ExitPlanModeRequest request = exitPlanModeRequests.get(0);
88+
assertEquals("Greeting file implementation plan", request.getSummary());
89+
assertNotNull(request.getActions());
90+
assertNotNull(request.getRecommendedAction());
91+
}
92+
}
93+
94+
/**
95+
* Verifies that auto-mode-switch handler is invoked when rate limited.
96+
*
97+
* @see Snapshot:
98+
* mode_handlers/should_invoke_auto_mode_switch_handler_when_rate_limited
99+
*/
100+
@Test
101+
void testShouldInvokeAutoModeSwitchHandlerWhenRateLimited() throws Exception {
102+
ctx.configureForTest("mode_handlers", "should_invoke_auto_mode_switch_handler_when_rate_limited");
103+
configureAuthenticatedUser();
104+
105+
final var autoModeSwitchRequests = new java.util.ArrayList<AutoModeSwitchRequest>();
106+
final String[] sessionIdHolder = new String[1];
107+
108+
var config = new SessionConfig().setGitHubToken(TOKEN).setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
109+
.setOnAutoModeSwitch((request, invocation) -> {
110+
autoModeSwitchRequests.add(request);
111+
assertEquals(sessionIdHolder[0], invocation.getSessionId());
112+
return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES);
113+
});
114+
115+
try (CopilotClient client = createAuthenticatedClient()) {
116+
CopilotSession session = client.createSession(config).get();
117+
sessionIdHolder[0] = session.getSessionId();
118+
119+
String messageId = session
120+
.send(new MessageOptions()
121+
.setPrompt("Explain that auto mode recovered from a rate limit in one short sentence."))
122+
.get(30, TimeUnit.SECONDS);
123+
assertNotNull(messageId, "Message ID should not be null");
124+
125+
// Wait for the auto mode switch handler to be invoked
126+
for (int i = 0; i < 30 && autoModeSwitchRequests.isEmpty(); i++) {
127+
Thread.sleep(1000);
128+
}
129+
130+
assertFalse(autoModeSwitchRequests.isEmpty(), "Should have received auto mode switch requests");
131+
132+
AutoModeSwitchRequest request = autoModeSwitchRequests.get(0);
133+
assertEquals("user_weekly_rate_limited", request.getErrorCode());
134+
assertEquals(1.0, request.getRetryAfterSeconds());
135+
}
136+
}
137+
138+
private CopilotClient createAuthenticatedClient() {
139+
var env = new HashMap<>(ctx.getEnvironment());
140+
env.put("COPILOT_DEBUG_GITHUB_API_URL", ctx.getProxyUrl());
141+
142+
var options = new CopilotClientOptions().setEnvironment(env);
143+
return ctx.createClient(options);
144+
}
145+
146+
private void configureAuthenticatedUser() throws Exception {
147+
ctx.setCopilotUserByToken(TOKEN, "mode-handler-user", "individual_pro", ctx.getProxyUrl(),
148+
"https://localhost:1/telemetry", "mode-handler-tracking-id");
149+
}
150+
}

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

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -535,4 +535,85 @@ void testResumeRequestOmitsEnableSessionTelemetryWhenNull() throws Exception {
535535
var json = mapper.writeValueAsString(request);
536536
assertFalse(json.contains("enableSessionTelemetry"), "enableSessionTelemetry should be omitted when null");
537537
}
538+
539+
@Test
540+
void testBuildCreateRequestSetsExitPlanModeFlag() {
541+
var config = new SessionConfig().setOnExitPlanMode(
542+
(req, inv) -> CompletableFuture.completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult()));
543+
CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
544+
assertTrue(request.getRequestExitPlanMode());
545+
}
546+
547+
@Test
548+
void testBuildCreateRequestOmitsExitPlanModeFlagWhenNull() {
549+
var config = new SessionConfig();
550+
CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
551+
assertNull(request.getRequestExitPlanMode());
552+
}
553+
554+
@Test
555+
void testBuildCreateRequestSetsAutoModeSwitchFlag() {
556+
var config = new SessionConfig().setOnAutoModeSwitch(
557+
(req, inv) -> CompletableFuture.completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO));
558+
CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
559+
assertTrue(request.getRequestAutoModeSwitch());
560+
}
561+
562+
@Test
563+
void testBuildCreateRequestOmitsAutoModeSwitchFlagWhenNull() {
564+
var config = new SessionConfig();
565+
CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
566+
assertNull(request.getRequestAutoModeSwitch());
567+
}
568+
569+
@Test
570+
void testBuildResumeRequestSetsExitPlanModeFlag() {
571+
var config = new ResumeSessionConfig().setOnExitPlanMode(
572+
(req, inv) -> CompletableFuture.completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult()));
573+
ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config);
574+
assertTrue(request.getRequestExitPlanMode());
575+
}
576+
577+
@Test
578+
void testBuildResumeRequestSetsAutoModeSwitchFlag() {
579+
var config = new ResumeSessionConfig().setOnAutoModeSwitch((req, inv) -> CompletableFuture
580+
.completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES));
581+
ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config);
582+
assertTrue(request.getRequestAutoModeSwitch());
583+
}
584+
585+
@Test
586+
void testCreateRequestSerializesModeRequestFlags() throws Exception {
587+
var config = new SessionConfig().setOnExitPlanMode(
588+
(req, inv) -> CompletableFuture.completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult()))
589+
.setOnAutoModeSwitch((req, inv) -> CompletableFuture
590+
.completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO));
591+
CreateSessionRequest request = SessionRequestBuilder.buildCreateRequest(config);
592+
var mapper = JsonRpcClient.getObjectMapper();
593+
var json = mapper.writeValueAsString(request);
594+
assertTrue(json.contains("\"requestExitPlanMode\":true"));
595+
assertTrue(json.contains("\"requestAutoModeSwitch\":true"));
596+
}
597+
598+
@Test
599+
void testResumeRequestSerializesModeRequestFlags() throws Exception {
600+
var config = new ResumeSessionConfig().setOnExitPlanMode(
601+
(req, inv) -> CompletableFuture.completedFuture(new com.github.copilot.sdk.json.ExitPlanModeResult()))
602+
.setOnAutoModeSwitch((req, inv) -> CompletableFuture
603+
.completedFuture(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES));
604+
ResumeSessionRequest request = SessionRequestBuilder.buildResumeRequest("sid-1", config);
605+
var mapper = JsonRpcClient.getObjectMapper();
606+
var json = mapper.writeValueAsString(request);
607+
assertTrue(json.contains("\"requestExitPlanMode\":true"));
608+
assertTrue(json.contains("\"requestAutoModeSwitch\":true"));
609+
}
610+
611+
@Test
612+
void testAutoModeSwitchResponseSerialization() throws Exception {
613+
var mapper = JsonRpcClient.getObjectMapper();
614+
assertEquals("\"yes_always\"",
615+
mapper.writeValueAsString(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES_ALWAYS));
616+
assertEquals("\"yes\"", mapper.writeValueAsString(com.github.copilot.sdk.json.AutoModeSwitchResponse.YES));
617+
assertEquals("\"no\"", mapper.writeValueAsString(com.github.copilot.sdk.json.AutoModeSwitchResponse.NO));
618+
}
538619
}

0 commit comments

Comments
 (0)