Skip to content

Commit 93ca61a

Browse files
Further E2E tests for resume (#665)
1 parent 427205f commit 93ca61a

13 files changed

+518
-291
lines changed

dotnet/test/SessionTests.cs

Lines changed: 10 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,11 @@ public async Task Should_Resume_A_Session_Using_The_Same_Client()
167167
var answer2 = await TestHelper.GetFinalAssistantMessageAsync(session2);
168168
Assert.NotNull(answer2);
169169
Assert.Contains("2", answer2!.Data.Content ?? string.Empty);
170+
171+
// Can continue the conversation statefully
172+
var answer3 = await session2.SendAndWaitAsync(new MessageOptions { Prompt = "Now if you double that, what do you get?" });
173+
Assert.NotNull(answer3);
174+
Assert.Contains("4", answer3!.Data.Content ?? string.Empty);
170175
}
171176

172177
[Fact]
@@ -187,6 +192,11 @@ public async Task Should_Resume_A_Session_Using_A_New_Client()
187192
var messages = await session2.GetMessagesAsync();
188193
Assert.Contains(messages, m => m is UserMessageEvent);
189194
Assert.Contains(messages, m => m is SessionResumeEvent);
195+
196+
// Can continue the conversation statefully
197+
var answer2 = await session2.SendAndWaitAsync(new MessageOptions { Prompt = "Now if you double that, what do you get?" });
198+
Assert.NotNull(answer2);
199+
Assert.Contains("4", answer2!.Data.Content ?? string.Empty);
190200
}
191201

192202
[Fact]
@@ -231,68 +241,6 @@ await session.SendAsync(new MessageOptions
231241
Assert.Contains("4", answer!.Data.Content ?? string.Empty);
232242
}
233243

234-
// TODO: This test requires the session-events.schema.json to include assistant.message_delta.
235-
// The CLI v0.0.376 emits delta events at runtime, but the schema hasn't been updated yet.
236-
// Once the schema is updated and types are regenerated, this test can be enabled.
237-
[Fact(Skip = "Requires schema update for AssistantMessageDeltaEvent type")]
238-
public async Task Should_Receive_Streaming_Delta_Events_When_Streaming_Is_Enabled()
239-
{
240-
var session = await CreateSessionAsync(new SessionConfig { Streaming = true });
241-
242-
var deltaContents = new List<string>();
243-
var doneEvent = new TaskCompletionSource<bool>();
244-
245-
session.On(evt =>
246-
{
247-
switch (evt)
248-
{
249-
// TODO: Uncomment once AssistantMessageDeltaEvent is generated
250-
// case AssistantMessageDeltaEvent delta:
251-
// if (!string.IsNullOrEmpty(delta.Data.DeltaContent))
252-
// deltaContents.Add(delta.Data.DeltaContent);
253-
// break;
254-
case SessionIdleEvent:
255-
doneEvent.TrySetResult(true);
256-
break;
257-
}
258-
});
259-
260-
await session.SendAsync(new MessageOptions { Prompt = "What is 2+2?" });
261-
262-
// Wait for completion
263-
var completed = await Task.WhenAny(doneEvent.Task, Task.Delay(TimeSpan.FromSeconds(60)));
264-
Assert.Equal(doneEvent.Task, completed);
265-
266-
// Should have received delta events
267-
Assert.NotEmpty(deltaContents);
268-
269-
// Get the final message to compare
270-
var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
271-
Assert.NotNull(assistantMessage);
272-
273-
// Accumulated deltas should equal the final message
274-
var accumulated = string.Join("", deltaContents);
275-
Assert.Equal(assistantMessage!.Data.Content, accumulated);
276-
277-
// Final message should contain the answer
278-
Assert.Contains("4", assistantMessage.Data.Content ?? string.Empty);
279-
}
280-
281-
[Fact]
282-
public async Task Should_Pass_Streaming_Option_To_Session_Creation()
283-
{
284-
// Verify that the streaming option is accepted without errors
285-
var session = await CreateSessionAsync(new SessionConfig { Streaming = true });
286-
287-
Assert.Matches(@"^[a-f0-9-]+$", session.SessionId);
288-
289-
// Session should still work normally
290-
await session.SendAsync(new MessageOptions { Prompt = "What is 1+1?" });
291-
var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
292-
Assert.NotNull(assistantMessage);
293-
Assert.Contains("2", assistantMessage!.Data.Content);
294-
}
295-
296244
[Fact]
297245
public async Task Should_Receive_Session_Events()
298246
{
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
using GitHub.Copilot.SDK.Test.Harness;
6+
using Xunit;
7+
using Xunit.Abstractions;
8+
9+
namespace GitHub.Copilot.SDK.Test;
10+
11+
public class StreamingFidelityTests(E2ETestFixture fixture, ITestOutputHelper output) : E2ETestBase(fixture, "streaming_fidelity", output)
12+
{
13+
[Fact]
14+
public async Task Should_Produce_Delta_Events_When_Streaming_Is_Enabled()
15+
{
16+
var session = await CreateSessionAsync(new SessionConfig { Streaming = true });
17+
18+
var events = new List<SessionEvent>();
19+
session.On(evt => events.Add(evt));
20+
21+
await session.SendAndWaitAsync(new MessageOptions { Prompt = "Count from 1 to 5, separated by commas." });
22+
23+
var types = events.Select(e => e.Type).ToList();
24+
25+
// Should have streaming deltas before the final message
26+
var deltaEvents = events.OfType<AssistantMessageDeltaEvent>().ToList();
27+
Assert.NotEmpty(deltaEvents);
28+
29+
// Deltas should have content
30+
foreach (var delta in deltaEvents)
31+
{
32+
Assert.False(string.IsNullOrEmpty(delta.Data.DeltaContent));
33+
}
34+
35+
// Should still have a final assistant.message
36+
Assert.Contains("assistant.message", types);
37+
38+
// Deltas should come before the final message
39+
var firstDeltaIdx = types.IndexOf("assistant.message_delta");
40+
var lastAssistantIdx = types.LastIndexOf("assistant.message");
41+
Assert.True(firstDeltaIdx < lastAssistantIdx);
42+
43+
await session.DisposeAsync();
44+
}
45+
46+
[Fact]
47+
public async Task Should_Not_Produce_Deltas_When_Streaming_Is_Disabled()
48+
{
49+
var session = await CreateSessionAsync(new SessionConfig { Streaming = false });
50+
51+
var events = new List<SessionEvent>();
52+
session.On(evt => events.Add(evt));
53+
54+
await session.SendAndWaitAsync(new MessageOptions { Prompt = "Say 'hello world'." });
55+
56+
var deltaEvents = events.OfType<AssistantMessageDeltaEvent>().ToList();
57+
58+
// No deltas when streaming is off
59+
Assert.Empty(deltaEvents);
60+
61+
// But should still have a final assistant.message
62+
var assistantEvents = events.OfType<AssistantMessageEvent>().ToList();
63+
Assert.NotEmpty(assistantEvents);
64+
65+
await session.DisposeAsync();
66+
}
67+
68+
[Fact]
69+
public async Task Should_Produce_Deltas_After_Session_Resume()
70+
{
71+
var session = await CreateSessionAsync(new SessionConfig { Streaming = false });
72+
await session.SendAndWaitAsync(new MessageOptions { Prompt = "What is 3 + 6?" });
73+
await session.DisposeAsync();
74+
75+
// Resume using a new client
76+
using var newClient = Ctx.CreateClient();
77+
var session2 = await newClient.ResumeSessionAsync(session.SessionId,
78+
new ResumeSessionConfig { OnPermissionRequest = PermissionHandler.ApproveAll, Streaming = true });
79+
80+
var events = new List<SessionEvent>();
81+
session2.On(evt => events.Add(evt));
82+
83+
var answer = await session2.SendAndWaitAsync(new MessageOptions { Prompt = "Now if you double that, what do you get?" });
84+
Assert.NotNull(answer);
85+
Assert.Contains("18", answer!.Data.Content ?? string.Empty);
86+
87+
// Should have streaming deltas before the final message
88+
var deltaEvents = events.OfType<AssistantMessageDeltaEvent>().ToList();
89+
Assert.NotEmpty(deltaEvents);
90+
91+
// Deltas should have content
92+
foreach (var delta in deltaEvents)
93+
{
94+
Assert.False(string.IsNullOrEmpty(delta.Data.DeltaContent));
95+
}
96+
97+
await session2.DisposeAsync();
98+
}
99+
}

go/internal/e2e/session_test.go

Lines changed: 18 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,15 @@ func TestSession(t *testing.T) {
368368
if answer2.Data.Content == nil || !strings.Contains(*answer2.Data.Content, "2") {
369369
t.Errorf("Expected resumed session answer to contain '2', got %v", answer2.Data.Content)
370370
}
371+
372+
// Can continue the conversation statefully
373+
answer3, err := session2.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Now if you double that, what do you get?"})
374+
if err != nil {
375+
t.Fatalf("Failed to send follow-up message: %v", err)
376+
}
377+
if answer3 == nil || answer3.Data.Content == nil || !strings.Contains(*answer3.Data.Content, "4") {
378+
t.Errorf("Expected follow-up answer to contain '4', got %v", answer3)
379+
}
371380
})
372381

373382
t.Run("should resume a session using a new client", func(t *testing.T) {
@@ -432,6 +441,15 @@ func TestSession(t *testing.T) {
432441
if !hasSessionResume {
433442
t.Error("Expected messages to contain 'session.resume'")
434443
}
444+
445+
// Can continue the conversation statefully
446+
answer3, err := session2.SendAndWait(t.Context(), copilot.MessageOptions{Prompt: "Now if you double that, what do you get?"})
447+
if err != nil {
448+
t.Fatalf("Failed to send follow-up message: %v", err)
449+
}
450+
if answer3 == nil || answer3.Data.Content == nil || !strings.Contains(*answer3.Data.Content, "4") {
451+
t.Errorf("Expected follow-up answer to contain '4', got %v", answer3)
452+
}
435453
})
436454

437455
t.Run("should throw error when resuming non-existent session", func(t *testing.T) {
@@ -565,99 +583,6 @@ func TestSession(t *testing.T) {
565583
}
566584
})
567585

568-
t.Run("should receive streaming delta events when streaming is enabled", func(t *testing.T) {
569-
ctx.ConfigureForTest(t)
570-
571-
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
572-
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
573-
Streaming: true,
574-
})
575-
if err != nil {
576-
t.Fatalf("Failed to create session with streaming: %v", err)
577-
}
578-
579-
var deltaContents []string
580-
done := make(chan bool)
581-
582-
session.On(func(event copilot.SessionEvent) {
583-
switch event.Type {
584-
case "assistant.message_delta":
585-
if event.Data.DeltaContent != nil {
586-
deltaContents = append(deltaContents, *event.Data.DeltaContent)
587-
}
588-
case "session.idle":
589-
close(done)
590-
}
591-
})
592-
593-
_, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 2+2?"})
594-
if err != nil {
595-
t.Fatalf("Failed to send message: %v", err)
596-
}
597-
598-
// Wait for completion
599-
select {
600-
case <-done:
601-
case <-time.After(60 * time.Second):
602-
t.Fatal("Timed out waiting for session.idle")
603-
}
604-
605-
// Should have received delta events
606-
if len(deltaContents) == 0 {
607-
t.Error("Expected to receive delta events, got none")
608-
}
609-
610-
// Get the final message to compare
611-
assistantMessage, err := testharness.GetFinalAssistantMessage(t.Context(), session)
612-
if err != nil {
613-
t.Fatalf("Failed to get assistant message: %v", err)
614-
}
615-
616-
// Accumulated deltas should equal the final message
617-
accumulated := strings.Join(deltaContents, "")
618-
if assistantMessage.Data.Content != nil && accumulated != *assistantMessage.Data.Content {
619-
t.Errorf("Accumulated deltas don't match final message.\nAccumulated: %q\nFinal: %q", accumulated, *assistantMessage.Data.Content)
620-
}
621-
622-
// Final message should contain the answer
623-
if assistantMessage.Data.Content == nil || !strings.Contains(*assistantMessage.Data.Content, "4") {
624-
t.Errorf("Expected assistant message to contain '4', got %v", assistantMessage.Data.Content)
625-
}
626-
})
627-
628-
t.Run("should pass streaming option to session creation", func(t *testing.T) {
629-
ctx.ConfigureForTest(t)
630-
631-
// Verify that the streaming option is accepted without errors
632-
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
633-
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
634-
Streaming: true,
635-
})
636-
if err != nil {
637-
t.Fatalf("Failed to create session with streaming: %v", err)
638-
}
639-
640-
matched, _ := regexp.MatchString(`^[a-f0-9-]+$`, session.SessionID)
641-
if !matched {
642-
t.Errorf("Expected session ID to match UUID pattern, got %q", session.SessionID)
643-
}
644-
645-
// Session should still work normally
646-
_, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "What is 1+1?"})
647-
if err != nil {
648-
t.Fatalf("Failed to send message: %v", err)
649-
}
650-
651-
assistantMessage, err := testharness.GetFinalAssistantMessage(t.Context(), session)
652-
if err != nil {
653-
t.Fatalf("Failed to get assistant message: %v", err)
654-
}
655-
656-
if assistantMessage.Data.Content == nil || !strings.Contains(*assistantMessage.Data.Content, "2") {
657-
t.Errorf("Expected assistant message to contain '2', got %v", assistantMessage.Data.Content)
658-
}
659-
})
660-
661586
t.Run("should receive session events", func(t *testing.T) {
662587
ctx.ConfigureForTest(t)
663588

0 commit comments

Comments
 (0)