Skip to content

Commit f625779

Browse files
stephentoubCopilot
andauthored
Handle empty session fork behavior in E2E tests (#1247)
Allow the empty-session fork tests to accept either the older runtime error or a successful empty fork, and apply the expectation across C#, Node, Python, and Go. Also mark the C# test project explicitly as a test project so dotnet test discovers the xUnit tests with newer SDKs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent d5c8db4 commit f625779

5 files changed

Lines changed: 96 additions & 25 deletions

File tree

dotnet/test/E2E/RpcSessionStateE2ETests.cs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,14 +276,29 @@ public async Task Should_Fork_Session_With_Persisted_Messages()
276276
}
277277

278278
[Fact]
279-
public async Task Should_Report_Error_When_Forking_Session_Without_Persisted_Events()
279+
public async Task Should_Handle_Forking_Session_Without_Persisted_Events()
280280
{
281281
await using var session = await CreateSessionAsync();
282282

283-
var ex = await Assert.ThrowsAnyAsync<Exception>(() => Client.Rpc.Sessions.ForkAsync(session.SessionId));
283+
SessionsForkResult? fork = null;
284+
var ex = await Record.ExceptionAsync(async () =>
285+
{
286+
fork = await Client.Rpc.Sessions.ForkAsync(session.SessionId);
287+
});
284288

285-
Assert.Contains("not found or has no persisted events", ex.ToString(), StringComparison.OrdinalIgnoreCase);
286-
Assert.DoesNotContain("Unhandled method sessions.fork", ex.ToString(), StringComparison.OrdinalIgnoreCase);
289+
if (ex is not null)
290+
{
291+
Assert.Contains("not found or has no persisted events", ex.ToString(), StringComparison.OrdinalIgnoreCase);
292+
Assert.DoesNotContain("Unhandled method sessions.fork", ex.ToString(), StringComparison.OrdinalIgnoreCase);
293+
return;
294+
}
295+
296+
var forkSessionId = Assert.IsType<SessionsForkResult>(fork).SessionId;
297+
Assert.False(string.IsNullOrWhiteSpace(forkSessionId));
298+
Assert.NotEqual(session.SessionId, forkSessionId);
299+
300+
await using var forkedSession = await ResumeSessionAsync(forkSessionId);
301+
Assert.Empty(GetConversationMessages(await forkedSession.GetMessagesAsync()));
287302
}
288303

289304
[Fact]

dotnet/test/GitHub.Copilot.SDK.Test.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
<PropertyGroup>
44
<IsPackable>false</IsPackable>
5+
<IsTestProject>true</IsTestProject>
56
<NoWarn>$(NoWarn);GHCP001</NoWarn>
67
</PropertyGroup>
78

go/internal/e2e/rpc_session_state_e2e_test.go

Lines changed: 35 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -297,23 +297,50 @@ func TestRpcSessionStateE2E(t *testing.T) {
297297
forkedSession.Disconnect()
298298
})
299299

300-
t.Run("should report error when forking session without persisted events", func(t *testing.T) {
300+
t.Run("should handle forking session without persisted events", func(t *testing.T) {
301301
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
302302
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
303303
})
304304
if err != nil {
305305
t.Fatalf("Failed to create session: %v", err)
306306
}
307+
defer session.Disconnect()
307308

308-
_, err = client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{SessionID: session.SessionID})
309-
if err == nil {
310-
t.Fatal("Expected fork on empty session to fail")
309+
fork, err := client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{SessionID: session.SessionID})
310+
if err != nil {
311+
errText := strings.ToLower(err.Error())
312+
if !strings.Contains(errText, "not found or has no persisted events") {
313+
t.Errorf("Expected error mentioning 'not found or has no persisted events', got %v", err)
314+
}
315+
if strings.Contains(errText, "unhandled method sessions.fork") {
316+
t.Errorf("sessions.fork should be implemented; error suggests it isn't: %v", err)
317+
}
318+
return
311319
}
312-
if !strings.Contains(strings.ToLower(err.Error()), "not found or has no persisted events") {
313-
t.Errorf("Expected error mentioning 'not found or has no persisted events', got %v", err)
320+
if fork == nil {
321+
t.Fatal("Expected non-nil fork result")
314322
}
315-
if strings.Contains(strings.ToLower(err.Error()), "unhandled method sessions.fork") {
316-
t.Errorf("sessions.fork should be implemented; error suggests it isn't: %v", err)
323+
if strings.TrimSpace(fork.SessionID) == "" {
324+
t.Fatal("Expected non-empty fork session id")
325+
}
326+
if fork.SessionID == session.SessionID {
327+
t.Errorf("Expected fork session id to differ from source %q", session.SessionID)
328+
}
329+
330+
forkedSession, err := client.ResumeSession(t.Context(), fork.SessionID, &copilot.ResumeSessionConfig{
331+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
332+
})
333+
if err != nil {
334+
t.Fatalf("Failed to resume forked session: %v", err)
335+
}
336+
defer forkedSession.Disconnect()
337+
338+
forkedMessages, err := forkedSession.GetMessages(t.Context())
339+
if err != nil {
340+
t.Fatalf("Failed to read forked messages: %v", err)
341+
}
342+
if forkedConversation := conversationMessages(forkedMessages); len(forkedConversation) != 0 {
343+
t.Errorf("Expected empty forked conversation, got %v", forkedConversation)
317344
}
318345
})
319346

nodejs/test/e2e/rpc_session_state.e2e.test.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -191,20 +191,34 @@ describe("Session-scoped RPC", async () => {
191191
await session.disconnect();
192192
});
193193

194-
it("should report error when forking session without persisted events", async () => {
194+
it("should handle forking session without persisted events", async () => {
195195
const session = await client.createSession({ onPermissionRequest: approveAll });
196-
197-
await expect(client.rpc.sessions.fork({ sessionId: session.sessionId })).rejects.toSatisfy(
198-
(err: unknown) => {
196+
try {
197+
let fork: Awaited<ReturnType<typeof client.rpc.sessions.fork>>;
198+
try {
199+
fork = await client.rpc.sessions.fork({ sessionId: session.sessionId });
200+
} catch (err: unknown) {
199201
const text =
200202
err instanceof Error ? `${err.message}\n${err.stack ?? ""}` : String(err);
201203
expect(text.toLowerCase()).toContain("not found or has no persisted events");
202204
expect(text.toLowerCase()).not.toContain("unhandled method sessions.fork");
203-
return true;
205+
return;
204206
}
205-
);
206207

207-
await session.disconnect();
208+
expect(fork.sessionId.trim()).toBeTruthy();
209+
expect(fork.sessionId).not.toBe(session.sessionId);
210+
211+
const forkedSession = await client.resumeSession(fork.sessionId, {
212+
onPermissionRequest: approveAll,
213+
});
214+
try {
215+
expect(getConversationMessages(await forkedSession.getMessages())).toEqual([]);
216+
} finally {
217+
await forkedSession.disconnect();
218+
}
219+
} finally {
220+
await session.disconnect();
221+
}
208222
});
209223

210224
it("should fork session to event id excluding boundary event", async () => {

python/e2e/test_rpc_session_state_e2e.py

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -216,20 +216,34 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon
216216
finally:
217217
await session.disconnect()
218218

219-
async def test_should_report_error_when_forking_session_without_persisted_events(
219+
async def test_should_handle_forking_session_without_persisted_events(
220220
self, ctx: E2ETestContext
221221
):
222222
session = await ctx.client.create_session(
223223
on_permission_request=PermissionHandler.approve_all,
224224
)
225225
try:
226-
with pytest.raises(Exception) as excinfo:
227-
await ctx.client.rpc.sessions.fork(
226+
try:
227+
fork = await ctx.client.rpc.sessions.fork(
228228
SessionsForkRequest(session_id=session.session_id)
229229
)
230-
text = str(excinfo.value).lower()
231-
assert "not found or has no persisted events" in text
232-
assert "unhandled method sessions.fork" not in text
230+
except Exception as exc:
231+
text = str(exc).lower()
232+
assert "not found or has no persisted events" in text
233+
assert "unhandled method sessions.fork" not in text
234+
return
235+
236+
assert fork.session_id.strip()
237+
assert fork.session_id != session.session_id
238+
239+
forked_session = await ctx.client.resume_session(
240+
fork.session_id,
241+
on_permission_request=PermissionHandler.approve_all,
242+
)
243+
try:
244+
assert _conversation_messages(await forked_session.get_messages()) == []
245+
finally:
246+
await forked_session.disconnect()
233247
finally:
234248
await session.disconnect()
235249

0 commit comments

Comments
 (0)