/*---------------------------------------------------------------------------------------------
* 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.List;
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.SessionEvent;
import com.github.copilot.sdk.generated.ToolExecutionCompleteEvent;
import com.github.copilot.sdk.json.MessageOptions;
import com.github.copilot.sdk.json.PermissionHandler;
import com.github.copilot.sdk.json.SessionConfig;
import com.github.copilot.sdk.json.ToolDefinition;
import com.github.copilot.sdk.json.ToolResultObject;
/**
* E2E tests for tool result types — verifying that rejected and denied result
* types are handled correctly by the runtime.
*
*
* Snapshots are stored in {@code test/snapshots/tool_results/}.
*
*/
public class ToolResultsTest {
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 a tool returning a "rejected" resultType is reported as a
* failed tool execution with the correct error code.
*
* @see Snapshot:
* tool_results/should_handle_tool_result_with_rejected_resulttype
*/
@Test
void testShouldHandleToolResultWithRejectedResultType() throws Exception {
ctx.configureForTest("tool_results", "should_handle_tool_result_with_rejected_resulttype");
var toolHandlerCalled = new boolean[]{false};
Map params = Map.of("type", "object", "properties", Map.of(), "required", List.of());
ToolDefinition deployTool = ToolDefinition.create("deploy_service", "Deploys a service", params,
(invocation) -> {
toolHandlerCalled[0] = true;
return CompletableFuture.completedFuture(new ToolResultObject("rejected",
"Deployment rejected: policy violation - production deployments require approval", null,
null, null, null));
});
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(deployTool))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
List events = new ArrayList<>();
session.on(events::add);
session.sendAndWait(new MessageOptions().setPrompt(
"Deploy the service using deploy_service. If it's rejected, tell me it was 'rejected by policy'."))
.get(60, TimeUnit.SECONDS);
assertTrue(toolHandlerCalled[0], "Tool handler should have been called");
List toolEvents = events.stream()
.filter(e -> e instanceof ToolExecutionCompleteEvent).map(e -> (ToolExecutionCompleteEvent) e)
.toList();
assertFalse(toolEvents.isEmpty(), "Should have a tool.execution_complete event");
ToolExecutionCompleteEvent toolEvt = toolEvents.get(0);
assertFalse(toolEvt.getData().success(), "Tool execution should not be marked as successful");
assertNotNull(toolEvt.getData().error(), "Should have error details");
assertEquals("rejected", toolEvt.getData().error().code(), "Error code should be 'rejected'");
session.close();
}
}
/**
* Verifies that a tool returning a "denied" resultType is reported as a failed
* tool execution with the correct error code.
*
* @see Snapshot: tool_results/should_handle_tool_result_with_denied_resulttype
*/
@Test
void testShouldHandleToolResultWithDeniedResultType() throws Exception {
ctx.configureForTest("tool_results", "should_handle_tool_result_with_denied_resulttype");
var toolHandlerCalled = new boolean[]{false};
Map params = Map.of("type", "object", "properties", Map.of(), "required", List.of());
ToolDefinition accessTool = ToolDefinition.create("access_secret", "Accesses a secret", params,
(invocation) -> {
toolHandlerCalled[0] = true;
return CompletableFuture.completedFuture(new ToolResultObject("denied",
"Access denied: insufficient permissions to read secrets", null, null, null, null));
});
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client.createSession(new SessionConfig().setTools(List.of(accessTool))
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
List events = new ArrayList<>();
session.on(events::add);
session.sendAndWait(new MessageOptions().setPrompt(
"Use access_secret to get the API key. If access is denied, tell me it was 'access denied'."))
.get(60, TimeUnit.SECONDS);
assertTrue(toolHandlerCalled[0], "Tool handler should have been called");
List toolEvents = events.stream()
.filter(e -> e instanceof ToolExecutionCompleteEvent).map(e -> (ToolExecutionCompleteEvent) e)
.toList();
assertFalse(toolEvents.isEmpty(), "Should have a tool.execution_complete event");
ToolExecutionCompleteEvent toolEvt = toolEvents.get(0);
assertFalse(toolEvt.getData().success(), "Tool execution should not be marked as successful");
assertNotNull(toolEvt.getData().error(), "Should have error details");
assertEquals("denied", toolEvt.getData().error().code(), "Error code should be 'denied'");
session.close();
}
}
}