/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ package com.github.copilot.sdk; import static org.junit.jupiter.api.Assertions.*; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; 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 org.junit.jupiter.api.TestInfo; import com.github.copilot.sdk.events.AssistantMessageEvent; import com.github.copilot.sdk.json.PermissionRequest; import com.github.copilot.sdk.json.PermissionRequestResult; import com.github.copilot.sdk.json.SessionConfig; import com.github.copilot.sdk.json.ResumeSessionConfig; import com.github.copilot.sdk.json.MessageOptions; /** * Tests for permission callback functionality. * *

* These tests use the shared CapiProxy infrastructure for deterministic API * response replay. Snapshots are stored in test/snapshots/permissions/. *

*/ public class PermissionsTest { 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 permission handler is invoked for write operations. * * @see Snapshot: permissions/permission_handler_for_write_operations */ @Test void testPermissionHandlerForWriteOperations(TestInfo testInfo) throws Exception { ctx.configureForTest("permissions", "permission_handler_for_write_operations"); var permissionRequests = new ArrayList(); final String[] sessionIdHolder = new String[1]; var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { permissionRequests.add(request); assertEquals(sessionIdHolder[0], invocation.getSessionId()); // Approve the permission return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); sessionIdHolder[0] = session.getSessionId(); // Write a test file Path testFile = ctx.getWorkDir().resolve("test.txt"); Files.writeString(testFile, "original content"); session.sendAndWait(new MessageOptions().setPrompt("Edit test.txt and replace 'original' with 'modified'")) .get(60, TimeUnit.SECONDS); // Should have received at least one permission request assertFalse(permissionRequests.isEmpty(), "Should have received permission requests"); // Should include write permission request boolean hasWriteRequest = permissionRequests.stream().anyMatch(req -> "write".equals(req.getKind())); assertTrue(hasWriteRequest, "Should have received a write permission request"); session.close(); } } /** * Verifies that permissions can be denied. * * @see Snapshot: permissions/deny_permission */ @Test void testDenyPermission(TestInfo testInfo) throws Exception { ctx.configureForTest("permissions", "deny_permission"); var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { // Deny all permissions return CompletableFuture .completedFuture(new PermissionRequestResult().setKind("denied-interactively-by-user")); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); String originalContent = "protected content"; Path testFile = ctx.getWorkDir().resolve("protected.txt"); Files.writeString(testFile, originalContent); session.sendAndWait( new MessageOptions().setPrompt("Edit protected.txt and replace 'protected' with 'hacked'.")) .get(60, TimeUnit.SECONDS); // Verify the file was NOT modified String content = Files.readString(testFile); assertEquals(originalContent, content, "File should not have been modified"); session.close(); } } /** * Verifies that sessions work without a permission handler. * * @see Snapshot: permissions/without_permission_handler */ @Test void testWithoutPermissionHandler(TestInfo testInfo) throws Exception { ctx.configureForTest("permissions", "without_permission_handler"); try (CopilotClient client = ctx.createClient()) { // Create session without onPermissionRequest handler CopilotSession session = client.createSession().get(); AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60, TimeUnit.SECONDS); assertNotNull(response); assertTrue(response.getData().getContent().contains("4"), "Response should contain 4: " + response.getData().getContent()); session.close(); } } /** * Verifies that async permission handlers work correctly. * * @see Snapshot: permissions/async_permission_handler */ @Test void testAsyncPermissionHandler(TestInfo testInfo) throws Exception { ctx.configureForTest("permissions", "async_permission_handler"); var permissionRequests = new ArrayList(); var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { permissionRequests.add(request); // Simulate async permission check with delay return CompletableFuture.supplyAsync(() -> { try { Thread.sleep(10); // Small delay to simulate async check } catch (InterruptedException e) { Thread.currentThread().interrupt(); } return new PermissionRequestResult().setKind("approved"); }); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test' and tell me what happens")).get(60, TimeUnit.SECONDS); // Should have received permission requests assertFalse(permissionRequests.isEmpty(), "Should have received permission requests"); session.close(); } } /** * Verifies that permission handlers work when resuming a session. * * @see Snapshot: permissions/resume_session_with_permission_handler */ @Test void testResumeSessionWithPermissionHandler(TestInfo testInfo) throws Exception { ctx.configureForTest("permissions", "resume_session_with_permission_handler"); var permissionRequests = new ArrayList(); try (CopilotClient client = ctx.createClient()) { // Create session without permission handler CopilotSession session1 = client.createSession().get(); String sessionId = session1.getSessionId(); session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS); // Resume with permission handler var resumeConfig = new ResumeSessionConfig().setOnPermissionRequest((request, invocation) -> { permissionRequests.add(request); return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); }); CopilotSession session2 = client.resumeSession(sessionId, resumeConfig).get(); assertEquals(sessionId, session2.getSessionId()); session2.sendAndWait(new MessageOptions().setPrompt("Run 'echo resumed' for me")).get(60, TimeUnit.SECONDS); // Should have permission requests from resumed session assertFalse(permissionRequests.isEmpty(), "Should have received permission requests from resumed session"); session2.close(); } } /** * Verifies that tool call IDs are included in permission requests. * * @see Snapshot: permissions/tool_call_id_in_permission_requests */ @Test void testToolCallIdInPermissionRequests(TestInfo testInfo) throws Exception { ctx.configureForTest("permissions", "tool_call_id_in_permission_requests"); final boolean[] receivedToolCallId = {false}; var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { if (request.getToolCallId() != null) { receivedToolCallId[0] = true; assertFalse(request.getToolCallId().isEmpty(), "Tool call ID should not be empty"); } return CompletableFuture.completedFuture(new PermissionRequestResult().setKind("approved")); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); session.sendAndWait(new MessageOptions().setPrompt("Run 'echo test'")).get(60, TimeUnit.SECONDS); assertTrue(receivedToolCallId[0], "Should have received toolCallId in permission request"); session.close(); } } /** * Verifies that permission handler errors are handled gracefully. *

* When the handler throws an exception, the SDK should deny the permission and * the assistant should indicate it couldn't complete the task. *

* * @see Snapshot: permissions/should_handle_permission_handler_errors_gracefully */ @Test void testShouldHandlePermissionHandlerErrorsGracefully(TestInfo testInfo) throws Exception { ctx.configureForTest("permissions", "should_handle_permission_handler_errors_gracefully"); var config = new SessionConfig().setOnPermissionRequest((request, invocation) -> { // Throw an error in the handler throw new RuntimeException("Handler error"); }); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession(config).get(); AssistantMessageEvent response = session .sendAndWait(new MessageOptions().setPrompt("Run 'echo test'. If you can't, say 'failed'.")) .get(60, TimeUnit.SECONDS); // Should handle the error and deny permission assertNotNull(response); String content = response.getData().getContent().toLowerCase(); assertTrue(content.contains("fail") || content.contains("cannot") || content.contains("unable") || content.contains("permission"), "Response should indicate failure: " + content); session.close(); } } }