/*--------------------------------------------------------------------------------------------- * 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.List; 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; /** * 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(); } } @Test void testAssistantTurnEventsEmitted() throws Exception { ctx.configureForTest("events", "assistant_turn_events_emitted"); List allEvents = new ArrayList<>(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession().get(); session.on(event -> allEvents.add(event)); session.sendAndWait(new MessageOptions().setPrompt("Say hello")).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"); } } @Test void testUserMessageEventEmitted() throws Exception { ctx.configureForTest("events", "user_message_event_emitted"); List userMessages = new ArrayList<>(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession().get(); session.on(UserMessageEvent.class, userMessages::add); session.sendAndWait(new MessageOptions().setPrompt("Hello, Copilot!")).get(60, TimeUnit.SECONDS); // Verify user message was captured assertFalse(userMessages.isEmpty(), "Should receive user.message event"); } } @Test void testToolExecutionCompleteEventEmitted() throws Exception { ctx.configureForTest("events", "tool_execution_complete_event_emitted"); List toolStarts = new ArrayList<>(); List toolCompletes = new ArrayList<>(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession().get(); session.on(ToolExecutionStartEvent.class, toolStarts::add); session.on(ToolExecutionCompleteEvent.class, toolCompletes::add); // Create a file for the model to read Path testFile = ctx.getWorkDir().resolve("test-events.txt"); Files.writeString(testFile, "Event test content"); session.sendAndWait(new MessageOptions().setPrompt("Read the contents of test-events.txt")).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().isSuccess()), "At least one tool execution should be successful"); } } @Test void testAssistantUsageEventEmitted() throws Exception { ctx.configureForTest("events", "assistant_usage_event_emitted"); List usageEvents = new ArrayList<>(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession().get(); session.on(AssistantUsageEvent.class, usageEvents::add); session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).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 } } @Test void testSessionIdleEventAfterMessageComplete() throws Exception { ctx.configureForTest("events", "session_idle_after_message"); List allEvents = new ArrayList<>(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession().get(); session.on(event -> allEvents.add(event)); session.sendAndWait(new MessageOptions().setPrompt("Say OK")).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"); } } @Test void testEventOrderDuringToolExecution() throws Exception { ctx.configureForTest("events", "event_order_during_tool_execution"); List eventTypes = new ArrayList<>(); try (CopilotClient client = ctx.createClient()) { CopilotSession session = client.createSession().get(); session.on(event -> eventTypes.add(event.getType())); // Create a file for the model to read Path testFile = ctx.getWorkDir().resolve("order-test.txt"); Files.writeString(testFile, "Order test content"); session.sendAndWait(new MessageOptions().setPrompt("Read the contents of order-test.txt")).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"); } } }