From be63014e2b2f796630f2d110cde4bbc18e80b9d5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:17:46 +0000 Subject: [PATCH 1/5] Initial plan From c8e1d43e8f19a9d95d98bba071604bbb72ee1dfc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:25:06 +0000 Subject: [PATCH 2/5] Add closed-session guard implementation Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CopilotSession.java | 45 ++- .../copilot/sdk/ClosedSessionGuardTest.java | 337 ++++++++++++++++++ 2 files changed, 381 insertions(+), 1 deletion(-) create mode 100644 src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 61ff49174..83dc197e4 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -97,6 +97,9 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference hooksHandler = new AtomicReference<>(); private volatile EventErrorHandler eventErrorHandler; private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; + + /** Tracks whether this session instance has been terminated via close(). */ + private volatile boolean isTerminated = false; /** * Creates a new session with the given ID and RPC client. @@ -186,11 +189,13 @@ public String getWorkspacePath() { * @param handler * the error handler, or {@code null} to use only the default logging * behavior + * @throws IllegalStateException if this session has been terminated * @see EventErrorHandler * @see #setEventErrorPolicy(EventErrorPolicy) * @since 1.0.8 */ public void setEventErrorHandler(EventErrorHandler handler) { + ensureNotTerminated(); this.eventErrorHandler = handler; } @@ -224,11 +229,13 @@ public void setEventErrorHandler(EventErrorHandler handler) { * @param policy * the error policy (default is * {@link EventErrorPolicy#PROPAGATE_AND_LOG_ERRORS}) + * @throws IllegalStateException if this session has been terminated * @see EventErrorPolicy * @see #setEventErrorHandler(EventErrorHandler) * @since 1.0.8 */ public void setEventErrorPolicy(EventErrorPolicy policy) { + ensureNotTerminated(); if (policy == null) { throw new NullPointerException("policy must not be null"); } @@ -244,9 +251,11 @@ public void setEventErrorPolicy(EventErrorPolicy policy) { * @param prompt * the message text to send * @return a future that resolves with the message ID assigned by the server + * @throws IllegalStateException if this session has been terminated * @see #send(MessageOptions) */ public CompletableFuture send(String prompt) { + ensureNotTerminated(); return send(new MessageOptions().setPrompt(prompt)); } @@ -260,9 +269,11 @@ public CompletableFuture send(String prompt) { * the message text to send * @return a future that resolves with the final assistant message event, or * {@code null} if no assistant message was received + * @throws IllegalStateException if this session has been terminated * @see #sendAndWait(MessageOptions) */ public CompletableFuture sendAndWait(String prompt) { + ensureNotTerminated(); return sendAndWait(new MessageOptions().setPrompt(prompt)); } @@ -275,10 +286,12 @@ public CompletableFuture sendAndWait(String prompt) { * @param options * the message options containing the prompt and attachments * @return a future that resolves with the message ID assigned by the server + * @throws IllegalStateException if this session has been terminated * @see #sendAndWait(MessageOptions) * @see #send(String) */ public CompletableFuture send(MessageOptions options) { + ensureNotTerminated(); var request = new SendMessageRequest(); request.setSessionId(sessionId); request.setPrompt(options.getPrompt()); @@ -304,10 +317,12 @@ public CompletableFuture send(MessageOptions options) { * {@code null} if no assistant message was received. The future * completes exceptionally with a TimeoutException if the timeout * expires. + * @throws IllegalStateException if this session has been terminated * @see #sendAndWait(MessageOptions) * @see #send(MessageOptions) */ public CompletableFuture sendAndWait(MessageOptions options, long timeoutMs) { + ensureNotTerminated(); var future = new CompletableFuture(); var lastAssistantMessage = new AtomicReference(); @@ -365,9 +380,11 @@ public CompletableFuture sendAndWait(MessageOptions optio * the message options containing the prompt and attachments * @return a future that resolves with the final assistant message event, or * {@code null} if no assistant message was received + * @throws IllegalStateException if this session has been terminated * @see #sendAndWait(MessageOptions, long) */ public CompletableFuture sendAndWait(MessageOptions options) { + ensureNotTerminated(); return sendAndWait(options, 60000); } @@ -397,11 +414,13 @@ public CompletableFuture sendAndWait(MessageOptions optio * @param handler * a callback to be invoked when a session event occurs * @return a Closeable that, when closed, unsubscribes the handler + * @throws IllegalStateException if this session has been terminated * @see #on(Class, Consumer) * @see AbstractSessionEvent * @see #setEventErrorPolicy(EventErrorPolicy) */ public Closeable on(Consumer handler) { + ensureNotTerminated(); eventHandlers.add(handler); return () -> eventHandlers.remove(handler); } @@ -447,10 +466,12 @@ public Closeable on(Consumer handler) { * @param handler * a callback invoked when events of this type occur * @return a Closeable that unsubscribes the handler when closed + * @throws IllegalStateException if this session has been terminated * @see #on(Consumer) * @see AbstractSessionEvent */ public Closeable on(Class eventType, Consumer handler) { + ensureNotTerminated(); Consumer wrapper = event -> { if (eventType.isInstance(event)) { handler.accept(eventType.cast(event)); @@ -708,9 +729,11 @@ CompletableFuture handleHooksInvoke(String hookType, JsonNode input) { * assistant responses, tool invocations, and other session events. * * @return a future that resolves with a list of all session events + * @throws IllegalStateException if this session has been terminated * @see AbstractSessionEvent */ public CompletableFuture> getMessages() { + ensureNotTerminated(); return rpc.invoke("session.getMessages", Map.of("sessionId", sessionId), GetMessagesResponse.class) .thenApply(response -> { var events = new ArrayList(); @@ -737,20 +760,40 @@ public CompletableFuture> getMessages() { * continuing to generate a response. * * @return a future that completes when the abort is acknowledged + * @throws IllegalStateException if this session has been terminated */ public CompletableFuture abort() { + ensureNotTerminated(); return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class); } + + /** + * Verifies that this session has not yet been terminated. + * + * @throws IllegalStateException if close() has already been invoked + */ + private void ensureNotTerminated() { + if (isTerminated) { + throw new IllegalStateException("Session is closed"); + } + } /** * Disposes the session and releases all associated resources. *

* This destroys the session on the server, clears all event handlers, and * releases tool and permission handlers. After calling this method, the session - * cannot be used again. + * cannot be used again. Subsequent calls to this method have no effect. */ @Override public void close() { + synchronized (this) { + if (isTerminated) { + return; // Already terminated - no-op + } + isTerminated = true; + } + try { rpc.invoke("session.destroy", Map.of("sessionId", sessionId), Void.class).get(5, TimeUnit.SECONDS); } catch (Exception e) { diff --git a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java new file mode 100644 index 000000000..485c39a52 --- /dev/null +++ b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java @@ -0,0 +1,337 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + *--------------------------------------------------------------------------------------------*/ + +package com.github.copilot.sdk; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import com.github.copilot.sdk.events.AssistantMessageEvent; +import com.github.copilot.sdk.json.MessageOptions; +import com.github.copilot.sdk.json.SessionConfig; + +/** + * Tests for closed-session guard functionality in CopilotSession. + * + *

+ * Verifies that all public methods that interact with session state throw + * IllegalStateException when invoked after close(), and that close() itself + * is idempotent. + *

+ */ +public class ClosedSessionGuardTest { + + 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 send(String) throws IllegalStateException after session is + * terminated. + */ + @Test + void testSendStringThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.send("test message"); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that send(MessageOptions) throws IllegalStateException after + * session is terminated. + */ + @Test + void testSendOptionsThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.send(new MessageOptions().setPrompt("test message")); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that sendAndWait(String) throws IllegalStateException after session + * is terminated. + */ + @Test + void testSendAndWaitStringThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.sendAndWait("test message"); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that sendAndWait(MessageOptions) throws IllegalStateException after + * session is terminated. + */ + @Test + void testSendAndWaitOptionsThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.sendAndWait(new MessageOptions().setPrompt("test message")); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that sendAndWait(MessageOptions, long) throws IllegalStateException + * after session is terminated. + */ + @Test + void testSendAndWaitWithTimeoutThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.sendAndWait(new MessageOptions().setPrompt("test message"), 5000); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that on(Consumer) throws IllegalStateException after session is + * terminated. + */ + @Test + void testOnConsumerThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.on(evt -> { + // Handler should never be registered + }); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that on(Class, Consumer) throws IllegalStateException after session + * is terminated. + */ + @Test + void testOnTypedConsumerThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.on(AssistantMessageEvent.class, msg -> { + // Handler should never be registered + }); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that getMessages() throws IllegalStateException after session is + * terminated. + */ + @Test + void testGetMessagesThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.getMessages(); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that abort() throws IllegalStateException after session is + * terminated. + */ + @Test + void testAbortThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.abort(); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that setEventErrorHandler() throws IllegalStateException after + * session is terminated. + */ + @Test + void testSetEventErrorHandlerThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.setEventErrorHandler((event, ex) -> { + // Handler should never be set + }); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that setEventErrorPolicy() throws IllegalStateException after + * session is terminated. + */ + @Test + void testSetEventErrorPolicyThrowsAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + session.close(); + + IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { + session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); + }); + assert thrown.getMessage().contains("closed"); + } + } + + /** + * Verifies that getSessionId() still works after session is terminated (it's + * just a field read). + */ + @Test + void testGetSessionIdWorksAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + String sessionIdBeforeClose = session.getSessionId(); + session.close(); + + String sessionIdAfterClose = session.getSessionId(); + assert sessionIdBeforeClose.equals(sessionIdAfterClose); + } + } + + /** + * Verifies that getWorkspacePath() still works after session is terminated + * (it's just a field read). + */ + @Test + void testGetWorkspacePathWorksAfterTermination() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + String pathBeforeClose = session.getWorkspacePath(); + session.close(); + + String pathAfterClose = session.getWorkspacePath(); + assert pathBeforeClose == pathAfterClose; // Both should be null or same value + } + } + + /** + * Verifies that close() is idempotent and can be called multiple times safely. + */ + @Test + void testCloseIsIdempotent() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + + // First close should succeed + assertDoesNotThrow(() -> session.close()); + + // Second close should also succeed (no-op) + assertDoesNotThrow(() -> session.close()); + + // Third close should also succeed (no-op) + assertDoesNotThrow(() -> session.close()); + } + } + + /** + * Verifies that try-with-resources double-close scenario works correctly. + */ + @Test + void testTryWithResourcesDoubleClose() throws Exception { + ctx.configureForTest("session", "should_receive_session_events"); + + try (CopilotClient client = ctx.createClient()) { + CopilotSession session = client.createSession(new SessionConfig().setModel("fake-test-model")).get(); + + try (session) { + // Manual close within try-with-resources + session.close(); + // Automatic close will happen at end of block + } // Second close happens here + + // Should be able to verify it's closed + assertThrows(IllegalStateException.class, () -> { + session.send("test"); + }); + } + } +} From 652fa6bf073d42e1abc3e34bd99fd31ba042b9a4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Feb 2026 23:31:45 +0000 Subject: [PATCH 3/5] Fix test to handle new IllegalStateException from closed sessions Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../github/copilot/sdk/CopilotSession.java | 42 ++++++++++++------- .../copilot/sdk/ClosedSessionGuardTest.java | 10 ++--- .../copilot/sdk/CopilotSessionTest.java | 8 ++-- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/main/java/com/github/copilot/sdk/CopilotSession.java b/src/main/java/com/github/copilot/sdk/CopilotSession.java index 83dc197e4..0a81cd719 100644 --- a/src/main/java/com/github/copilot/sdk/CopilotSession.java +++ b/src/main/java/com/github/copilot/sdk/CopilotSession.java @@ -97,7 +97,7 @@ public final class CopilotSession implements AutoCloseable { private final AtomicReference hooksHandler = new AtomicReference<>(); private volatile EventErrorHandler eventErrorHandler; private volatile EventErrorPolicy eventErrorPolicy = EventErrorPolicy.PROPAGATE_AND_LOG_ERRORS; - + /** Tracks whether this session instance has been terminated via close(). */ private volatile boolean isTerminated = false; @@ -189,7 +189,8 @@ public String getWorkspacePath() { * @param handler * the error handler, or {@code null} to use only the default logging * behavior - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see EventErrorHandler * @see #setEventErrorPolicy(EventErrorPolicy) * @since 1.0.8 @@ -229,7 +230,8 @@ public void setEventErrorHandler(EventErrorHandler handler) { * @param policy * the error policy (default is * {@link EventErrorPolicy#PROPAGATE_AND_LOG_ERRORS}) - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see EventErrorPolicy * @see #setEventErrorHandler(EventErrorHandler) * @since 1.0.8 @@ -251,7 +253,8 @@ public void setEventErrorPolicy(EventErrorPolicy policy) { * @param prompt * the message text to send * @return a future that resolves with the message ID assigned by the server - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see #send(MessageOptions) */ public CompletableFuture send(String prompt) { @@ -269,7 +272,8 @@ public CompletableFuture send(String prompt) { * the message text to send * @return a future that resolves with the final assistant message event, or * {@code null} if no assistant message was received - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see #sendAndWait(MessageOptions) */ public CompletableFuture sendAndWait(String prompt) { @@ -286,7 +290,8 @@ public CompletableFuture sendAndWait(String prompt) { * @param options * the message options containing the prompt and attachments * @return a future that resolves with the message ID assigned by the server - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see #sendAndWait(MessageOptions) * @see #send(String) */ @@ -317,7 +322,8 @@ public CompletableFuture send(MessageOptions options) { * {@code null} if no assistant message was received. The future * completes exceptionally with a TimeoutException if the timeout * expires. - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see #sendAndWait(MessageOptions) * @see #send(MessageOptions) */ @@ -380,7 +386,8 @@ public CompletableFuture sendAndWait(MessageOptions optio * the message options containing the prompt and attachments * @return a future that resolves with the final assistant message event, or * {@code null} if no assistant message was received - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see #sendAndWait(MessageOptions, long) */ public CompletableFuture sendAndWait(MessageOptions options) { @@ -414,7 +421,8 @@ public CompletableFuture sendAndWait(MessageOptions optio * @param handler * a callback to be invoked when a session event occurs * @return a Closeable that, when closed, unsubscribes the handler - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see #on(Class, Consumer) * @see AbstractSessionEvent * @see #setEventErrorPolicy(EventErrorPolicy) @@ -466,7 +474,8 @@ public Closeable on(Consumer handler) { * @param handler * a callback invoked when events of this type occur * @return a Closeable that unsubscribes the handler when closed - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see #on(Consumer) * @see AbstractSessionEvent */ @@ -729,7 +738,8 @@ CompletableFuture handleHooksInvoke(String hookType, JsonNode input) { * assistant responses, tool invocations, and other session events. * * @return a future that resolves with a list of all session events - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated * @see AbstractSessionEvent */ public CompletableFuture> getMessages() { @@ -760,17 +770,19 @@ public CompletableFuture> getMessages() { * continuing to generate a response. * * @return a future that completes when the abort is acknowledged - * @throws IllegalStateException if this session has been terminated + * @throws IllegalStateException + * if this session has been terminated */ public CompletableFuture abort() { ensureNotTerminated(); return rpc.invoke("session.abort", Map.of("sessionId", sessionId), Void.class); } - + /** * Verifies that this session has not yet been terminated. - * - * @throws IllegalStateException if close() has already been invoked + * + * @throws IllegalStateException + * if close() has already been invoked */ private void ensureNotTerminated() { if (isTerminated) { diff --git a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java index 485c39a52..f7f74a4c3 100644 --- a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java +++ b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java @@ -17,11 +17,11 @@ /** * Tests for closed-session guard functionality in CopilotSession. - * + * *

* Verifies that all public methods that interact with session state throw - * IllegalStateException when invoked after close(), and that close() itself - * is idempotent. + * IllegalStateException when invoked after close(), and that close() itself is + * idempotent. *

*/ public class ClosedSessionGuardTest { @@ -60,8 +60,8 @@ void testSendStringThrowsAfterTermination() throws Exception { } /** - * Verifies that send(MessageOptions) throws IllegalStateException after - * session is terminated. + * Verifies that send(MessageOptions) throws IllegalStateException after session + * is terminated. */ @Test void testSendOptionsThrowsAfterTermination() throws Exception { diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 398e6f2f8..02b8e6761 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -79,13 +79,15 @@ void testShouldReceiveSessionEvents_createAndDestroy() throws Exception { session.close(); - // Session should no longer be accessible + // Session should no longer be accessible - now throws IllegalStateException try { session.getMessages().get(); fail("Expected exception for closed session"); } catch (Exception e) { - assertTrue(e.getMessage().toLowerCase().contains("not found") - || e.getCause().getMessage().toLowerCase().contains("not found")); + // After our changes, we now get IllegalStateException directly + assertTrue(e.getMessage().toLowerCase().contains("closed") + || (e.getCause() != null + && e.getCause().getMessage().toLowerCase().contains("not found"))); } } } From 69dcf93205fe6acbcdd55ec8705cde966a02fab9 Mon Sep 17 00:00:00 2001 From: Bruno Borges Date: Fri, 6 Feb 2026 17:10:22 -0800 Subject: [PATCH 4/5] Update src/test/java/com/github/copilot/sdk/CopilotSessionTest.java Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../java/com/github/copilot/sdk/CopilotSessionTest.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java index 02b8e6761..781fb9a9a 100644 --- a/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java +++ b/src/test/java/com/github/copilot/sdk/CopilotSessionTest.java @@ -85,9 +85,11 @@ void testShouldReceiveSessionEvents_createAndDestroy() throws Exception { fail("Expected exception for closed session"); } catch (Exception e) { // After our changes, we now get IllegalStateException directly - assertTrue(e.getMessage().toLowerCase().contains("closed") - || (e.getCause() != null - && e.getCause().getMessage().toLowerCase().contains("not found"))); + String message = e.getMessage(); + String causeMessage = e.getCause() != null ? e.getCause().getMessage() : null; + boolean matchesClosed = message != null && message.toLowerCase().contains("closed"); + boolean matchesNotFound = causeMessage != null && causeMessage.toLowerCase().contains("not found"); + assertTrue(matchesClosed || matchesNotFound); } } } From fac2e20d8e2b50799ed95c50083fe67a75f6c458 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Feb 2026 01:14:10 +0000 Subject: [PATCH 5/5] Replace Java assert with JUnit assertions in tests Co-authored-by: brunoborges <129743+brunoborges@users.noreply.github.com> --- .../copilot/sdk/ClosedSessionGuardTest.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java index f7f74a4c3..f4606c646 100644 --- a/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java +++ b/src/test/java/com/github/copilot/sdk/ClosedSessionGuardTest.java @@ -5,7 +5,9 @@ package com.github.copilot.sdk; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -55,7 +57,7 @@ void testSendStringThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.send("test message"); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -74,7 +76,7 @@ void testSendOptionsThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.send(new MessageOptions().setPrompt("test message")); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -93,7 +95,7 @@ void testSendAndWaitStringThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.sendAndWait("test message"); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -112,7 +114,7 @@ void testSendAndWaitOptionsThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.sendAndWait(new MessageOptions().setPrompt("test message")); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -131,7 +133,7 @@ void testSendAndWaitWithTimeoutThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.sendAndWait(new MessageOptions().setPrompt("test message"), 5000); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -152,7 +154,7 @@ void testOnConsumerThrowsAfterTermination() throws Exception { // Handler should never be registered }); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -173,7 +175,7 @@ void testOnTypedConsumerThrowsAfterTermination() throws Exception { // Handler should never be registered }); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -192,7 +194,7 @@ void testGetMessagesThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.getMessages(); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -211,7 +213,7 @@ void testAbortThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.abort(); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -232,7 +234,7 @@ void testSetEventErrorHandlerThrowsAfterTermination() throws Exception { // Handler should never be set }); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -251,7 +253,7 @@ void testSetEventErrorPolicyThrowsAfterTermination() throws Exception { IllegalStateException thrown = assertThrows(IllegalStateException.class, () -> { session.setEventErrorPolicy(EventErrorPolicy.SUPPRESS_AND_LOG_ERRORS); }); - assert thrown.getMessage().contains("closed"); + assertTrue(thrown.getMessage().contains("closed"), "Exception message should mention session is closed"); } } @@ -269,7 +271,7 @@ void testGetSessionIdWorksAfterTermination() throws Exception { session.close(); String sessionIdAfterClose = session.getSessionId(); - assert sessionIdBeforeClose.equals(sessionIdAfterClose); + assertEquals(sessionIdBeforeClose, sessionIdAfterClose, "Session ID should remain accessible after close"); } } @@ -287,7 +289,7 @@ void testGetWorkspacePathWorksAfterTermination() throws Exception { session.close(); String pathAfterClose = session.getWorkspacePath(); - assert pathBeforeClose == pathAfterClose; // Both should be null or same value + assertEquals(pathBeforeClose, pathAfterClose, "Workspace path should remain accessible after close"); } }