/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ package com.github.copilot.sdk; import static org.junit.jupiter.api.Assertions.*; import java.util.HashMap; import java.util.Map; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import com.github.copilot.sdk.generated.ExitPlanModeCompletedEvent; import com.github.copilot.sdk.generated.ExitPlanModeRequestedEvent; import com.github.copilot.sdk.json.AutoModeSwitchRequest; import com.github.copilot.sdk.json.AutoModeSwitchResponse; import com.github.copilot.sdk.json.CopilotClientOptions; import com.github.copilot.sdk.json.ExitPlanModeRequest; import com.github.copilot.sdk.json.ExitPlanModeResult; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; /** * E2E tests for exit-plan-mode and auto-mode-switch handler APIs. * *

* Ported from {@code ModeHandlersE2ETests.cs} in the reference implementation * dotnet SDK. *

*/ public class ModeHandlersTest { private static final String TOKEN = "mode-handler-token"; private static E2ETestContext ctx; @BeforeAll static void setup() throws Exception { ctx = E2ETestContext.create(); } @AfterAll static void teardown() throws Exception { if (ctx != null) { ctx.close(); } } private CopilotClient createAuthenticatedClient() { Map env = new HashMap<>(ctx.getEnvironment()); env.put("COPILOT_DEBUG_GITHUB_API_URL", ctx.getProxyUrl()); return ctx.createClient(new CopilotClientOptions().setEnvironment(env)); } private void configureAuthenticatedUser(String testName) throws Exception { ctx.configureForTest("mode_handlers", testName); ctx.setCopilotUserByToken(TOKEN, "mode-handler-user", "individual_pro", ctx.getProxyUrl(), "https://localhost:1/telemetry", "mode-handler-tracking-id"); } @Test void shouldInvokeExitPlanModeHandlerWhenModelUsesTool() throws Exception { final String summary = "Greeting file implementation plan"; configureAuthenticatedUser("should_invoke_exit_plan_mode_handler_when_model_uses_tool"); var handlerCalled = new CompletableFuture(); try (var client = createAuthenticatedClient()) { var session = client.createSession(new SessionConfig().setGitHubToken(TOKEN) .setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setOnExitPlanMode((request, invocation) -> { handlerCalled.complete(request); return CompletableFuture.completedFuture(new ExitPlanModeResult().setApproved(true) .setSelectedAction("interactive").setFeedback("Approved by the Java E2E test")); })).get(30, TimeUnit.SECONDS); var requestedEvent = new CompletableFuture(); var completedEvent = new CompletableFuture(); session.on(event -> { if (event instanceof ExitPlanModeRequestedEvent requested && summary.equals(requested.getData().summary())) { requestedEvent.complete(requested); } else if (event instanceof ExitPlanModeCompletedEvent completed && Boolean.TRUE.equals(completed.getData().approved()) && "interactive".equals(completed.getData().selectedAction())) { completedEvent.complete(completed); } }); var response = session.sendAndWait(new MessageOptions().setPrompt( "Create a brief implementation plan for adding a greeting.txt file, then request approval with exit_plan_mode.") .setMode("plan")).get(120, TimeUnit.SECONDS); var request = handlerCalled.get(10, TimeUnit.SECONDS); assertEquals(summary, request.getSummary()); assertNotNull(request.getActions()); assertTrue(request.getActions().contains("interactive")); assertNotNull(request.getPlanContent()); var reqEvent = requestedEvent.get(10, TimeUnit.SECONDS); assertEquals(request.getSummary(), reqEvent.getData().summary()); var compEvent = completedEvent.get(10, TimeUnit.SECONDS); assertTrue(compEvent.getData().approved()); assertEquals("interactive", compEvent.getData().selectedAction()); assertNotNull(response); session.close(); } } @Test void shouldInvokeAutoModeSwitchHandlerWhenRateLimited() throws Exception { configureAuthenticatedUser("should_invoke_auto_mode_switch_handler_when_rate_limited"); var handlerCalled = new CompletableFuture(); try (var client = createAuthenticatedClient()) { var session = client.createSession( new SessionConfig().setGitHubToken(TOKEN).setOnPermissionRequest(PermissionHandler.APPROVE_ALL) .setOnAutoModeSwitch((request, invocation) -> { handlerCalled.complete(request); return CompletableFuture.completedFuture(AutoModeSwitchResponse.YES); })) .get(30, TimeUnit.SECONDS); var messageId = session .send(new MessageOptions() .setPrompt("Explain that auto mode recovered from a rate limit in one short sentence.")) .get(30, TimeUnit.SECONDS); assertNotNull(messageId); assertFalse(messageId.isEmpty()); var request = handlerCalled.get(30, TimeUnit.SECONDS); assertEquals("user_weekly_rate_limited", request.getErrorCode()); assertEquals(1.0, request.getRetryAfterSeconds()); session.close(); } } }