/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. *--------------------------------------------------------------------------------------------*/ package com.github.copilot.sdk; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; 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.AssistantTurnEndEvent; import com.github.copilot.sdk.events.AssistantTurnStartEvent; import com.github.copilot.sdk.events.AssistantUsageEvent; import com.github.copilot.sdk.events.SessionIdleEvent; import com.github.copilot.sdk.events.ToolExecutionCompleteEvent; import com.github.copilot.sdk.events.ToolExecutionStartEvent; import com.github.copilot.sdk.events.UserMessageEvent; import com.github.copilot.sdk.json.MessageOptions; import com.github.copilot.sdk.json.PermissionHandler; import com.github.copilot.sdk.json.SessionConfig; /** * E2E tests for session events to verify event lifecycle. *

* These tests verify that various session events are properly emitted during * typical interaction flows with the Copilot CLI. *

*/ public class SessionEventsE2ETest { 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 assistant turn events (turn_start, turn_end) are emitted. * * @see Snapshot: session/should_receive_session_events */ @Test void testShouldReceiveSessionEvents_assistantTurnEvents() throws Exception { // Use existing session snapshot that emits turn events ctx.configureForTest("session", "should_receive_session_events"); var allEvents = new ArrayList(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(event -> allEvents.add(event)); // Use prompt that matches the snapshot session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); // Verify turn lifecycle events assertTrue(allEvents.stream().anyMatch(e -> e instanceof AssistantTurnStartEvent), "Should receive assistant.turn_start event"); assertTrue(allEvents.stream().anyMatch(e -> e instanceof AssistantTurnEndEvent), "Should receive assistant.turn_end event"); // Verify order: turn_start should come before turn_end int turnStartIndex = -1; int turnEndIndex = -1; for (int i = 0; i < allEvents.size(); i++) { if (allEvents.get(i) instanceof AssistantTurnStartEvent && turnStartIndex == -1) { turnStartIndex = i; } if (allEvents.get(i) instanceof AssistantTurnEndEvent) { turnEndIndex = i; } } assertTrue(turnStartIndex < turnEndIndex, "turn_start should come before turn_end"); } } /** * Verifies that user message events are emitted. * * @see Snapshot: session/should_receive_session_events */ @Test void testShouldReceiveSessionEvents_userMessageEvent() throws Exception { // Use existing session snapshot ctx.configureForTest("session", "should_receive_session_events"); var userMessages = new ArrayList(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(UserMessageEvent.class, userMessages::add); // Use prompt that matches the snapshot session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); // Verify user message was captured assertFalse(userMessages.isEmpty(), "Should receive user.message event"); } } /** * Verifies that tool execution complete events are emitted. * * @see Snapshot: tools/invokes_built_in_tools */ @Test void testInvokesBuiltInTools_toolExecutionCompleteEvent() throws Exception { // Use existing tools snapshot for built-in tool invocation ctx.configureForTest("tools", "invokes_built_in_tools"); var toolStarts = new ArrayList(); var toolCompletes = new ArrayList(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(ToolExecutionStartEvent.class, toolStarts::add); session.on(ToolExecutionCompleteEvent.class, toolCompletes::add); // Create the README.md file expected by the snapshot - must have ONLY one line // to match the snapshot's expected tool response: "1. # ELIZA, the only chatbot // you'll ever need" Path testFile = ctx.getWorkDir().resolve("README.md"); Files.writeString(testFile, "# ELIZA, the only chatbot you'll ever need"); // Use prompt that matches the snapshot session.sendAndWait(new MessageOptions().setPrompt("What's the first line of README.md in this directory?")) .get(60, TimeUnit.SECONDS); // Verify tool execution events assertFalse(toolStarts.isEmpty(), "Should receive tool.execution_start event"); assertFalse(toolCompletes.isEmpty(), "Should receive tool.execution_complete event"); // Verify tool execution completed successfully assertTrue(toolCompletes.stream().anyMatch(e -> e.getData().success()), "At least one tool execution should be successful"); } } /** * Verifies that assistant usage events are handled when emitted. * * @see Snapshot: session/should_receive_session_events */ @Test void testShouldReceiveSessionEvents_assistantUsageEvent() throws Exception { // Use existing session snapshot ctx.configureForTest("session", "should_receive_session_events"); var usageEvents = new ArrayList(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(AssistantUsageEvent.class, usageEvents::add); // Use prompt that matches the snapshot session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); // Usage events may or may not be emitted depending on the model/API version // This test verifies the event handler works when they are emitted // We don't assert they must be present since it depends on the backend } } /** * Verifies that session.idle event is emitted after message completion. * * @see Snapshot: session/should_receive_session_events */ @Test void testShouldReceiveSessionEvents_sessionIdleAfterMessage() throws Exception { // Use existing session snapshot ctx.configureForTest("session", "should_receive_session_events"); var allEvents = new ArrayList(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(event -> allEvents.add(event)); // Use prompt that matches the snapshot session.sendAndWait(new MessageOptions().setPrompt("What is 100+200?")).get(60, TimeUnit.SECONDS); // Verify session.idle is emitted after assistant.message assertTrue(allEvents.stream().anyMatch(e -> e instanceof SessionIdleEvent), "Should receive session.idle event"); assertTrue(allEvents.stream().anyMatch(e -> e instanceof AssistantMessageEvent), "Should receive assistant.message event"); // Verify order: assistant.message should come before session.idle int messageIndex = -1; int idleIndex = -1; for (int i = 0; i < allEvents.size(); i++) { if (allEvents.get(i) instanceof AssistantMessageEvent) { messageIndex = i; } if (allEvents.get(i) instanceof SessionIdleEvent) { idleIndex = i; } } assertTrue(messageIndex < idleIndex, "assistant.message should come before session.idle"); } } /** * Verifies the order of events during tool execution. * * @see Snapshot: tools/invokes_built_in_tools */ @Test void testInvokesBuiltInTools_eventOrderDuringToolExecution() throws Exception { // Use existing tools snapshot for built-in tool invocation ctx.configureForTest("tools", "invokes_built_in_tools"); var eventTypes = new ArrayList(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client .createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(); session.on(event -> eventTypes.add(event.getType())); // Create the README.md file expected by the snapshot - must have ONLY one line // to match the snapshot's expected tool response: "1. # ELIZA, the only chatbot // you'll ever need" Path testFile = ctx.getWorkDir().resolve("README.md"); Files.writeString(testFile, "# ELIZA, the only chatbot you'll ever need"); // Use prompt that matches the snapshot session.sendAndWait(new MessageOptions().setPrompt("What's the first line of README.md in this directory?")) .get(60, TimeUnit.SECONDS); // Verify expected event types are present assertTrue(eventTypes.contains("user.message"), "Should have user.message"); assertTrue(eventTypes.contains("assistant.turn_start"), "Should have assistant.turn_start"); assertTrue(eventTypes.contains("tool.execution_start"), "Should have tool.execution_start"); assertTrue(eventTypes.contains("tool.execution_complete"), "Should have tool.execution_complete"); assertTrue(eventTypes.contains("assistant.message"), "Should have assistant.message"); assertTrue(eventTypes.contains("assistant.turn_end"), "Should have assistant.turn_end"); assertTrue(eventTypes.contains("session.idle"), "Should have session.idle"); // Verify tool execution is between turn_start and turn_end int turnStartIdx = eventTypes.indexOf("assistant.turn_start"); int toolStartIdx = eventTypes.indexOf("tool.execution_start"); int toolCompleteIdx = eventTypes.indexOf("tool.execution_complete"); int turnEndIdx = eventTypes.lastIndexOf("assistant.turn_end"); assertTrue(turnStartIdx < toolStartIdx, "turn_start should be before tool.execution_start"); assertTrue(toolStartIdx < toolCompleteIdx, "tool.execution_start should be before tool.execution_complete"); assertTrue(toolCompleteIdx < turnEndIdx, "tool.execution_complete should be before turn_end"); } } }