This repository was archived by the owner on Mar 25, 2026. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathCopilotSessionTest.java
More file actions
824 lines (676 loc) · 35.3 KB
/
CopilotSessionTest.java
File metadata and controls
824 lines (676 loc) · 35.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
*--------------------------------------------------------------------------------------------*/
package com.github.copilot.sdk;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
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.AbortEvent;
import com.github.copilot.sdk.events.AssistantMessageEvent;
import com.github.copilot.sdk.events.SessionIdleEvent;
import com.github.copilot.sdk.events.SessionStartEvent;
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.ResumeSessionConfig;
import com.github.copilot.sdk.json.SessionConfig;
import com.github.copilot.sdk.json.SystemMessageConfig;
import com.github.copilot.sdk.json.ToolDefinition;
/**
* Tests for CopilotSession.
*
* <p>
* These tests use the shared CapiProxy infrastructure for deterministic API
* response replay. Snapshots are stored in test/snapshots/session/.
* </p>
*/
public class CopilotSessionTest {
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 session can be created and closed properly.
*
* @see Snapshot: session/should_receive_session_events
*/
@Test
void testShouldReceiveSessionEvents_createAndDestroy() throws Exception {
ctx.configureForTest("session", "should_receive_session_events");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client.createSession(new SessionConfig()
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL).setModel("fake-test-model")).get();
assertNotNull(session.getSessionId());
assertTrue(session.getSessionId().matches("^[a-f0-9-]+$"));
List<AbstractSessionEvent> messages = session.getMessages().get();
assertFalse(messages.isEmpty());
assertTrue(messages.get(0) instanceof SessionStartEvent);
session.close();
// Session should no longer be accessible - now throws IllegalStateException
try {
session.getMessages().get();
fail("Expected exception for closed session");
} catch (Exception e) {
// After our changes, we now get IllegalStateException directly
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);
}
}
}
/**
* Verifies that sessions maintain conversation state across multiple messages.
*
* @see Snapshot: session/should_have_stateful_conversation
*/
@Test
void testShouldHaveStatefulConversation() throws Exception {
ctx.configureForTest("session", "should_have_stateful_conversation");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
AssistantMessageEvent response1 = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?"), 60000)
.get(90, TimeUnit.SECONDS);
assertNotNull(response1);
assertTrue(response1.getData().content().contains("2"),
"Response should contain 2: " + response1.getData().content());
AssistantMessageEvent response2 = session
.sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"), 60000)
.get(90, TimeUnit.SECONDS);
assertNotNull(response2);
assertTrue(response2.getData().content().contains("4"),
"Response should contain 4: " + response2.getData().content());
session.close();
}
}
/**
* Verifies that session events (user.message, assistant.message, session.idle)
* are properly received.
*
* @see Snapshot: session/should_receive_session_events
*/
@Test
void testShouldReceiveSessionEvents() throws Exception {
ctx.configureForTest("session", "should_receive_session_events");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
List<AbstractSessionEvent> receivedEvents = new ArrayList<>();
CompletableFuture<Void> idleReceived = new CompletableFuture<>();
session.on(evt -> {
receivedEvents.add(evt);
if (evt instanceof SessionIdleEvent) {
idleReceived.complete(null);
}
});
session.send(new MessageOptions().setPrompt("What is 100+200?")).get();
idleReceived.get(60, TimeUnit.SECONDS);
assertFalse(receivedEvents.isEmpty());
assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof UserMessageEvent));
assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof AssistantMessageEvent));
assertTrue(receivedEvents.stream().anyMatch(e -> e instanceof SessionIdleEvent));
// Find the assistant message
AssistantMessageEvent assistantMsg = receivedEvents.stream().filter(e -> e instanceof AssistantMessageEvent)
.map(e -> (AssistantMessageEvent) e).findFirst().orElse(null);
assertNotNull(assistantMsg);
assertTrue(assistantMsg.getData().content().contains("300"),
"Response should contain 300: " + assistantMsg.getData().content());
session.close();
}
}
/**
* Verifies that send() returns immediately while events stream in background.
*
* @see Snapshot:
* session/send_returns_immediately_while_events_stream_in_background
*/
@Test
void testSendReturnsImmediatelyWhileEventsStreamInBackground() throws Exception {
ctx.configureForTest("session", "send_returns_immediately_while_events_stream_in_background");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
var events = new ArrayList<String>();
var lastMessage = new AtomicReference<AssistantMessageEvent>();
var done = new CompletableFuture<Void>();
session.on(evt -> {
events.add(evt.getType());
if (evt instanceof AssistantMessageEvent msg) {
lastMessage.set(msg);
} else if (evt instanceof SessionIdleEvent) {
done.complete(null);
}
});
// Use a slow command so we can verify send() returns before completion
// Use String convenience overload (covers send(String) path)
session.send("Run 'sleep 2 && echo done'").get();
// At this point, we might not have received session.idle yet
// The event handling happens asynchronously
// Wait for completion
done.get(60, TimeUnit.SECONDS);
assertTrue(events.contains("session.idle"));
assertTrue(events.contains("assistant.message"));
assertNotNull(lastMessage.get());
assertTrue(lastMessage.get().getData().content().contains("done"),
"Response should contain done: " + lastMessage.get().getData().content());
session.close();
}
}
/**
* Verifies that sendAndWait blocks until session is idle and returns the final
* assistant message.
*
* @see Snapshot:
* session/sendandwait_blocks_until_session_idle_and_returns_final_assistant_message
*/
@Test
void testSendAndWaitBlocksUntilSessionIdleAndReturnsFinalAssistantMessage() throws Exception {
ctx.configureForTest("session", "sendandwait_blocks_until_session_idle_and_returns_final_assistant_message");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
var events = new ArrayList<String>();
session.on(evt -> events.add(evt.getType()));
// Use String convenience overload (covers sendAndWait(String) path)
AssistantMessageEvent response = session.sendAndWait("What is 2+2?").get(60, TimeUnit.SECONDS);
assertNotNull(response);
assertEquals("assistant.message", response.getType());
assertTrue(response.getData().content().contains("4"),
"Response should contain 4: " + response.getData().content());
assertTrue(events.contains("session.idle"));
assertTrue(events.contains("assistant.message"));
session.close();
}
}
/**
* Verifies that a session can be resumed using the same client.
*
* @see Snapshot: session/should_resume_a_session_using_the_same_client
*/
@Test
void testShouldResumeSessionUsingTheSameClient() throws Exception {
ctx.configureForTest("session", "should_resume_a_session_using_the_same_client");
try (CopilotClient client = ctx.createClient()) {
// Create initial session
CopilotSession session1 = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
String sessionId = session1.getSessionId();
AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60,
TimeUnit.SECONDS);
assertNotNull(answer);
assertTrue(answer.getData().content().contains("2"),
"Response should contain 2: " + answer.getData().content());
// Resume using the same client
CopilotSession session2 = client.resumeSession(sessionId,
new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
assertEquals(sessionId, session2.getSessionId());
// Verify resumed session has the previous messages
List<AbstractSessionEvent> messages = session2.getMessages().get(60, TimeUnit.SECONDS);
boolean hasAssistantMessage = messages.stream().filter(m -> m instanceof AssistantMessageEvent)
.map(m -> (AssistantMessageEvent) m).anyMatch(m -> m.getData().content().contains("2"));
assertTrue(hasAssistantMessage, "Should find previous assistant message containing 2");
// Can continue the conversation statefully
AssistantMessageEvent answer2 = session2
.sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"))
.get(60, TimeUnit.SECONDS);
assertNotNull(answer2);
assertTrue(answer2.getData().content().contains("4"),
"Follow-up response should contain 4: " + answer2.getData().content());
session2.close();
}
}
/**
* Verifies that a session can be resumed using a new client.
*
* @see Snapshot: session/should_resume_a_session_using_a_new_client
*/
@Test
void testShouldResumeSessionUsingNewClient() throws Exception {
ctx.configureForTest("session", "should_resume_a_session_using_a_new_client");
// Use a single try-with-resources for the first client to keep it alive
// throughout the test, matching the behavior of other SDK implementations
try (CopilotClient client1 = ctx.createClient()) {
// Create initial session
CopilotSession session1 = client1
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
String sessionId = session1.getSessionId();
AssistantMessageEvent answer = session1.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60,
TimeUnit.SECONDS);
assertNotNull(answer);
assertTrue(answer.getData().content().contains("2"),
"Response should contain 2: " + answer.getData().content());
// Resume using a new client (keeping client1 alive)
try (CopilotClient client2 = ctx.createClient()) {
CopilotSession session2 = client2.resumeSession(sessionId,
new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
assertEquals(sessionId, session2.getSessionId());
// When resuming with a new client, validate messages contain expected types
List<AbstractSessionEvent> messages = session2.getMessages().get(60, TimeUnit.SECONDS);
assertTrue(messages.stream().anyMatch(m -> m instanceof UserMessageEvent),
"Should contain user.message event");
assertTrue(messages.stream().anyMatch(m -> "session.resume".equals(m.getType())),
"Should contain session.resume event");
// Can continue the conversation statefully
AssistantMessageEvent answer2 = session2
.sendAndWait(new MessageOptions().setPrompt("Now if you double that, what do you get?"))
.get(60, TimeUnit.SECONDS);
assertNotNull(answer2);
assertTrue(answer2.getData().content().contains("4"),
"Follow-up response should contain 4: " + answer2.getData().content());
session2.close();
}
}
}
/**
* Verifies that sessions work with appended system message configuration.
*
* @see Snapshot:
* session/should_create_a_session_with_appended_systemmessage_config
*/
@Test
void testShouldCreateSessionWithAppendedSystemMessageConfig() throws Exception {
ctx.configureForTest("session", "should_create_a_session_with_appended_systemmessage_config");
try (CopilotClient client = ctx.createClient()) {
String systemMessageSuffix = "End each response with the phrase 'Have a nice day!'";
SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSystemMessage(new SystemMessageConfig().setContent(systemMessageSuffix)
.setMode(SystemMessageMode.APPEND));
CopilotSession session = client.createSession(config).get();
assertNotNull(session.getSessionId());
AssistantMessageEvent response = session
.sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS);
assertNotNull(response);
assertTrue(response.getData().content().contains("GitHub"),
"Response should contain GitHub: " + response.getData().content());
assertTrue(response.getData().content().contains("Have a nice day!"),
"Response should end with 'Have a nice day!': " + response.getData().content());
session.close();
}
}
/**
* Verifies that sessions work with replaced system message configuration.
*
* @see Snapshot:
* session/should_create_a_session_with_replaced_systemmessage_config
*/
@Test
void testShouldCreateSessionWithReplacedSystemMessageConfig() throws Exception {
ctx.configureForTest("session", "should_create_a_session_with_replaced_systemmessage_config");
try (CopilotClient client = ctx.createClient()) {
String testSystemMessage = "You are an assistant called Testy McTestface. Reply succinctly.";
SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setSystemMessage(
new SystemMessageConfig().setContent(testSystemMessage).setMode(SystemMessageMode.REPLACE));
CopilotSession session = client.createSession(config).get();
assertNotNull(session.getSessionId());
AssistantMessageEvent response = session
.sendAndWait(new MessageOptions().setPrompt("What is your full name?")).get(60, TimeUnit.SECONDS);
assertNotNull(response);
assertTrue(response.getData().content().contains("Testy McTestface"),
"Response should contain 'Testy McTestface': " + response.getData().content());
session.close();
}
}
/**
* Verifies that a session can be aborted during tool execution.
*
* @see Snapshot: session/should_abort_a_session
*/
@Test
void testShouldAbortSession() throws Exception {
ctx.configureForTest("session", "should_abort_a_session");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
assertNotNull(session.getSessionId());
// Set up wait for tool execution to start BEFORE sending
var toolStartFuture = new CompletableFuture<ToolExecutionStartEvent>();
var sessionIdleFuture = new CompletableFuture<SessionIdleEvent>();
session.on(evt -> {
if (evt instanceof ToolExecutionStartEvent toolStart && !toolStartFuture.isDone()) {
toolStartFuture.complete(toolStart);
} else if (evt instanceof SessionIdleEvent idle && !sessionIdleFuture.isDone()) {
sessionIdleFuture.complete(idle);
}
});
// Send a message that will trigger a long-running shell command
session.send(new MessageOptions()
.setPrompt("run the shell command 'sleep 100' (note this works on both bash and PowerShell)"))
.get();
// Wait for the tool to start executing
toolStartFuture.get(60, TimeUnit.SECONDS);
// Abort the session while the tool is running
session.abort();
// Wait for session to become idle after abort
sessionIdleFuture.get(30, TimeUnit.SECONDS);
// The session should still be alive and usable after abort
List<AbstractSessionEvent> messages = session.getMessages().get(60, TimeUnit.SECONDS);
assertFalse(messages.isEmpty());
// Verify an abort event exists in messages
assertTrue(messages.stream().anyMatch(m -> m instanceof AbortEvent), "Expected an abort event in messages");
// We should be able to send another message
AssistantMessageEvent answer = session.sendAndWait(new MessageOptions().setPrompt("What is 2+2?")).get(60,
TimeUnit.SECONDS);
assertNotNull(answer);
assertTrue(answer.getData().content().contains("4"),
"Response should contain 4: " + answer.getData().content());
session.close();
}
}
/**
* Verifies that sessions can be created with available tools configuration.
*
* @see Snapshot: session/should_create_a_session_with_availabletools
*/
@Test
void testShouldCreateSessionWithAvailableTools() throws Exception {
ctx.configureForTest("session", "should_create_a_session_with_availabletools");
try (CopilotClient client = ctx.createClient()) {
SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setAvailableTools(List.of("view", "edit"));
CopilotSession session = client.createSession(config).get();
assertNotNull(session.getSessionId());
AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60,
TimeUnit.SECONDS);
assertNotNull(response);
session.close();
}
}
/**
* Verifies that sessions can be created with excluded tools configuration.
*
* @see Snapshot: session/should_create_a_session_with_excludedtools
*/
@Test
void testShouldCreateSessionWithExcludedTools() throws Exception {
ctx.configureForTest("session", "should_create_a_session_with_excludedtools");
try (CopilotClient client = ctx.createClient()) {
SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setExcludedTools(List.of("view"));
CopilotSession session = client.createSession(config).get();
assertNotNull(session.getSessionId());
AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60,
TimeUnit.SECONDS);
assertNotNull(response);
assertTrue(response.getData().content().contains("2"),
"Response should contain 2: " + response.getData().content());
session.close();
}
}
/**
* Verifies that an error is thrown when resuming a non-existent session.
*
* @see Snapshot: session/should_receive_session_events
*/
@Test
void testShouldThrowErrorWhenResumingNonExistentSession() throws Exception {
ctx.configureForTest("session", "should_receive_session_events");
try (CopilotClient client = ctx.createClient()) {
try {
client.resumeSession("non-existent-session-id",
new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL))
.get(30, TimeUnit.SECONDS);
fail("Expected exception when resuming non-existent session");
} catch (Exception e) {
// Should throw an error
assertTrue(e.getMessage() != null || e.getCause() != null, "Exception should have a message or cause");
}
}
}
/**
* Verifies that sessions can be created with a custom config directory.
*
* @see Snapshot: session/should_create_session_with_custom_config_dir
*/
@Test
void testShouldCreateSessionWithCustomConfigDir() throws Exception {
ctx.configureForTest("session", "should_create_session_with_custom_config_dir");
try (CopilotClient client = ctx.createClient()) {
String customConfigDir = ctx.getWorkDir().resolve("custom-config").toString();
SessionConfig config = new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setConfigDir(customConfigDir);
CopilotSession session = client.createSession(config).get();
assertNotNull(session.getSessionId());
assertTrue(session.getSessionId().matches("^[a-f0-9-]+$"));
// Session should work normally with custom config dir
AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60,
TimeUnit.SECONDS);
assertNotNull(response);
assertTrue(response.getData().content().contains("2"),
"Response should contain 2: " + response.getData().content());
session.close();
}
}
// This test validates client-side timeout behavior. The snapshot has no
// assistant response because the test expects timeout BEFORE completion.
// Note: In CI mode, the proxy logs "No cached response found" errors to
// stderr, but these are expected - the timeout still triggers correctly.
/**
* Verifies that sendAndWait throws an exception on timeout.
*
* @see Snapshot: session/sendandwait_throws_on_timeout
*/
@Test
void testSendAndWaitThrowsOnTimeout() throws Exception {
ctx.configureForTest("session", "sendandwait_throws_on_timeout");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
// Use a short timeout that will trigger before any response
try {
session.sendAndWait(new MessageOptions().setPrompt("Run 'sleep 2 && echo done'"), 100).get(30,
TimeUnit.SECONDS);
fail("Expected timeout exception");
} catch (Exception e) {
// Should throw a timeout-related error from sendAndWait
String message = e.getMessage() != null ? e.getMessage().toLowerCase() : "";
String causeMessage = e.getCause() != null && e.getCause().getMessage() != null
? e.getCause().getMessage().toLowerCase()
: "";
assertTrue(
message.contains("timeout") || message.contains("sendandwait timed out")
|| causeMessage.contains("timeout") || causeMessage.contains("sendandwait timed out"),
"Should throw timeout exception, got: " + e.getMessage()
+ (e.getCause() != null ? " caused by: " + e.getCause().getMessage() : ""));
}
session.close();
}
}
/**
* Verifies that sessions can be listed.
*
* @see Snapshot: session/should_list_sessions
*/
@Test
void testShouldListSessions() throws Exception {
ctx.configureForTest("session", "should_list_sessions");
try (CopilotClient client = ctx.createClient()) {
// Create two sessions and send one message to each (matches snapshot format)
CopilotSession session1 = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
session1.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS);
CopilotSession session2 = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
session2.sendAndWait(new MessageOptions().setPrompt("Say goodbye")).get(60, TimeUnit.SECONDS);
// Small delay to ensure session files are written to disk
Thread.sleep(200);
// List all sessions
var sessions = client.listSessions().get(30, TimeUnit.SECONDS);
// Should have at least the sessions we created
assertNotNull(sessions);
assertFalse(sessions.isEmpty(), "Should have at least 1 session");
// Our sessions should be in the list
var sessionIds = sessions.stream().map(s -> s.getSessionId()).toList();
assertTrue(sessionIds.contains(session1.getSessionId()), "Session 1 should be in the list");
assertTrue(sessionIds.contains(session2.getSessionId()), "Session 2 should be in the list");
session1.close();
session2.close();
}
}
/**
* Verifies that sessions can be deleted.
*
* @see Snapshot: session/should_delete_session
*/
@Test
void testShouldDeleteSession() throws Exception {
ctx.configureForTest("session", "should_delete_session");
try (CopilotClient client = ctx.createClient()) {
// Create a session
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
String sessionId = session.getSessionId();
session.sendAndWait(new MessageOptions().setPrompt("Hello")).get(60, TimeUnit.SECONDS);
// Delete the session using the client API
// In CI mode with replaying proxy, session files may not be persisted,
// so we handle the "session not found" case as acceptable
try {
client.deleteSession(sessionId).get(30, TimeUnit.SECONDS);
} catch (Exception e) {
// In CI replay mode, session files don't exist - this is expected
if (System.getenv("CI") != null && e.getMessage() != null && e.getMessage().contains("not found")) {
return; // Test passes - CI mode doesn't persist sessions
}
throw e;
}
// Trying to resume the deleted session should fail
try {
client.resumeSession(sessionId,
new ResumeSessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL))
.get(30, TimeUnit.SECONDS);
fail("Expected exception when resuming deleted session");
} catch (Exception e) {
// Should throw an error indicating session not found
assertTrue(e.getMessage() != null || e.getCause() != null, "Exception should have a message or cause");
}
}
}
/**
* Verifies that sessions can be created with custom tools.
*
* @see Snapshot: session/should_create_session_with_custom_tool
*/
@Test
void testShouldCreateSessionWithCustomTool() throws Exception {
ctx.configureForTest("session", "should_create_session_with_custom_tool");
// Define a custom get_secret_number tool
Map<String, Object> parameters = new java.util.HashMap<>();
Map<String, Object> properties = new java.util.HashMap<>();
Map<String, Object> keyProp = new java.util.HashMap<>();
keyProp.put("type", "string");
keyProp.put("description", "Key");
properties.put("key", keyProp);
parameters.put("type", "object");
parameters.put("properties", properties);
parameters.put("required", java.util.List.of("key"));
ToolDefinition getSecretNumberTool = ToolDefinition.create("get_secret_number", "Gets the secret number",
parameters, (invocation) -> {
Map<String, Object> args = invocation.getArguments();
String key = (String) args.get("key");
// Return 54321 for ALPHA, 0 otherwise
int result = "ALPHA".equals(key) ? 54321 : 0;
return CompletableFuture.completedFuture(String.valueOf(result));
});
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
.setTools(java.util.List.of(getSecretNumberTool)))
.get();
AssistantMessageEvent response = session
.sendAndWait(new MessageOptions().setPrompt("What is the secret number for key ALPHA?"))
.get(60, TimeUnit.SECONDS);
assertNotNull(response);
assertTrue(response.getData().content().contains("54321"),
"Response should contain 54321: " + response.getData().content());
session.close();
}
}
/**
* Verifies that getLastSessionId returns the ID of the most recently used
* session.
*
* @see Snapshot: session/should_get_last_session_id
*/
@Test
void testShouldGetLastSessionId() throws Exception {
ctx.configureForTest("session", "should_get_last_session_id");
try (CopilotClient client = ctx.createClient()) {
CopilotSession session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
session.sendAndWait(new MessageOptions().setPrompt("Say hello")).get(60, TimeUnit.SECONDS);
String lastId = client.getLastSessionId().get(30, TimeUnit.SECONDS);
assertNotNull(lastId, "Last session ID should not be null");
assertEquals(session.getSessionId(), lastId, "Last session ID should match the current session ID");
session.close();
}
}
/**
* Verifies that listSessions returns metadata with optional context
* information.
*
* @see Snapshot: session/should_list_sessions
*/
@Test
void testListSessionsIncludesContextWhenAvailable() throws Exception {
ctx.configureForTest("session", "should_list_sessions");
try (CopilotClient client = ctx.createClient()) {
var session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
var sessions = client.listSessions().get(30, TimeUnit.SECONDS);
assertNotNull(sessions);
// List may be empty or contain sessions depending on test environment
// The main goal is to verify the API works and context field is accessible
for (var s : sessions) {
assertNotNull(s.getSessionId());
// Context field is optional
if (s.getContext() != null) {
// When context is present, cwd should be non-null
assertNotNull(s.getContext().getCwd());
}
}
session.close();
}
}
/**
* Verifies that SessionListFilter works with fluent setters.
*/
@Test
void testSessionListFilterFluentAPI() throws Exception {
ctx.configureForTest("session", "should_list_sessions");
try (CopilotClient client = ctx.createClient()) {
var session = client
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
var filter = new com.github.copilot.sdk.json.SessionListFilter().setCwd("/test/path")
.setRepository("owner/repo").setBranch("main").setGitRoot("/test");
assertEquals("/test/path", filter.getCwd());
assertEquals("owner/repo", filter.getRepository());
assertEquals("main", filter.getBranch());
assertEquals("/test", filter.getGitRoot());
var filteredSessions = client.listSessions(filter).get(30, TimeUnit.SECONDS);
assertNotNull(filteredSessions);
session.close();
}
}
}