/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ package com.github.copilot.sdk; import static org.junit.jupiter.api.Assertions.*; import java.util.ArrayList; import java.util.logging.Logger; import java.util.List; 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.events.AbstractSessionEvent; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.events.SessionErrorEvent; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.SessionConfig; import com.github.copilot.sdk.json.ToolDefinition; import java.util.Map; /** * E2E tests for error handling scenarios. *

* These tests verify that the SDK properly handles errors in various scenarios * including tool errors, permission handler errors, and session errors. *

*/ public class ErrorHandlingTest { private static final Logger LOG = Logger.getLogger(ErrorHandlingTest.class.getName()); private static E2ETestContext ctx; @BeforeAll static void setup() throws Exception { ctx = E2ETestContext.create(); } @AfterAll static void teardown() throws Exception { if (ctx != null) { ctx.close(); } } /** * Verifies that tool errors are handled gracefully and don't crash the session. * * @see Snapshot: tools/handles_tool_calling_errors */ @Test void testHandlesToolCallingErrors_toolErrorDoesNotCrashSession() throws Exception { LOG.info("Running test: testHandlesToolCallingErrors_toolErrorDoesNotCrashSession"); ctx.configureForTest("tools", "handles_tool_calling_errors"); var allEvents = new ArrayList(); ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location", Map.of("type", "object", "properties", Map.of()), (invocation) -> { CompletableFuture future = new CompletableFuture<>(); future.completeExceptionally(new RuntimeException("Location service unavailable")); return future; }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get(); session.on(event -> allEvents.add(event)); AssistantMessageEvent response = session .sendAndWait(new MessageOptions() .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) .get(60, TimeUnit.SECONDS); // Session should complete without crashing assertNotNull(response, "Should receive a response even when tool fails"); // Should have received session.idle (indicating successful completion) assertTrue(allEvents.stream().anyMatch(e -> e instanceof com.github.copilot.sdk.events.SessionIdleEvent), "Session should reach idle state after handling tool error"); session.close(); } } /** * Verifies that returning a failure result from a tool is handled properly. * * @see Snapshot: tools/handles_tool_calling_errors */ @Test void testHandlesToolCallingErrors_toolReturnsFailureResult() throws Exception { LOG.info("Running test: testHandlesToolCallingErrors_toolReturnsFailureResult"); ctx.configureForTest("tools", "handles_tool_calling_errors"); ToolDefinition failTool = ToolDefinition.create("get_user_location", "Gets the user's location", Map.of("type", "object", "properties", Map.of()), (invocation) -> { // Return a structured failure result via exception (matching the snapshot // behavior) return CompletableFuture.failedFuture(new RuntimeException("Location unavailable")); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(failTool))).get(); AssistantMessageEvent response = session .sendAndWait(new MessageOptions() .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) .get(60, TimeUnit.SECONDS); assertNotNull(response, "Should receive a response with failure result"); session.close(); } } /** * Verifies that permission handler errors result in denied permission. * * @see Snapshot: permissions/should_handle_permission_handler_errors_gracefully */ @Test void testShouldHandlePermissionHandlerErrorsGracefully_deniesPermission() throws Exception { LOG.info("Running test: testShouldHandlePermissionHandlerErrorsGracefully_deniesPermission"); ctx.configureForTest("permissions", "should_handle_permission_handler_errors_gracefully"); var errorEvents = new ArrayList(); var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { throw new RuntimeException("Permission handler crashed"); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); session.on(SessionErrorEvent.class, errorEvents::add); AssistantMessageEvent response = session .sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'.")) .get(60, TimeUnit.SECONDS); // Should complete despite the error assertNotNull(response, "Should receive a response despite handler error"); // The response should indicate failure/inability String content = response.getData().getContent().toLowerCase(); assertTrue( content.contains("fail") || content.contains("cannot") || content.contains("unable") || content.contains("permission") || content.contains("denied"), "Response should indicate permission was denied: " + content); session.close(); } } /** * Verifies that session error events contain proper error information. * * @see Snapshot: permissions/permission_handler_errors */ @Test void testPermissionHandlerErrors_sessionErrorEventContainsDetails() throws Exception { LOG.info("Running test: testPermissionHandlerErrors_sessionErrorEventContainsDetails"); ctx.configureForTest("permissions", "permission_handler_errors"); var errorEvents = new ArrayList(); var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { throw new RuntimeException("Test error message"); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); session.on(SessionErrorEvent.class, error -> { errorEvents.add(error); // Verify error event has data assertNotNull(error.getData(), "Error event should have data"); }); try { // Use prompt that matches the snapshot session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'.")) .get(60, TimeUnit.SECONDS); } catch (Exception e) { // Error is expected in some cases } session.close(); } // Note: Whether error events are emitted depends on the CLI version and // scenario // This test verifies the handler can receive them when they occur } /** * Verifies that the session continues to work after a tool error. * * @see Snapshot: tools/handles_tool_calling_errors */ @Test void testHandlesToolCallingErrors_sessionContinuesAfterToolError() throws Exception { LOG.info("Running test: testHandlesToolCallingErrors_sessionContinuesAfterToolError"); ctx.configureForTest("tools", "handles_tool_calling_errors"); ToolDefinition errorTool = ToolDefinition.create("get_user_location", "Gets the user's location", Map.of("type", "object", "properties", Map.of()), (invocation) -> { return CompletableFuture.failedFuture(new RuntimeException("Service unavailable")); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(errorTool))).get(); // First request that will cause tool error AssistantMessageEvent response = session .sendAndWait(new MessageOptions() .setPrompt("What is my location? If you can't find out, just say 'unknown'.")) .get(60, TimeUnit.SECONDS); assertNotNull(response, "Should receive first response"); // Session should still be usable - the sendAndWait completed // This verifies the session didn't enter an error state session.close(); } } }