Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 19 additions & 4 deletions dotnet/test/E2E/RpcSessionStateE2ETests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,14 +276,29 @@ public async Task Should_Fork_Session_With_Persisted_Messages()
}

[Fact]
public async Task Should_Report_Error_When_Forking_Session_Without_Persisted_Events()
public async Task Should_Handle_Forking_Session_Without_Persisted_Events()
{
await using var session = await CreateSessionAsync();

var ex = await Assert.ThrowsAnyAsync<Exception>(() => Client.Rpc.Sessions.ForkAsync(session.SessionId));
SessionsForkResult? fork = null;
var ex = await Record.ExceptionAsync(async () =>
{
fork = await Client.Rpc.Sessions.ForkAsync(session.SessionId);
});

Assert.Contains("not found or has no persisted events", ex.ToString(), StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("Unhandled method sessions.fork", ex.ToString(), StringComparison.OrdinalIgnoreCase);
if (ex is not null)
{
Assert.Contains("not found or has no persisted events", ex.ToString(), StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain("Unhandled method sessions.fork", ex.ToString(), StringComparison.OrdinalIgnoreCase);
return;
}

var forkSessionId = Assert.IsType<SessionsForkResult>(fork).SessionId;
Assert.False(string.IsNullOrWhiteSpace(forkSessionId));
Assert.NotEqual(session.SessionId, forkSessionId);

await using var forkedSession = await ResumeSessionAsync(forkSessionId);
Assert.Empty(GetConversationMessages(await forkedSession.GetMessagesAsync()));
}

[Fact]
Expand Down
1 change: 1 addition & 0 deletions dotnet/test/GitHub.Copilot.SDK.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<NoWarn>$(NoWarn);GHCP001</NoWarn>
</PropertyGroup>

Expand Down
43 changes: 35 additions & 8 deletions go/internal/e2e/rpc_session_state_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,23 +297,50 @@ func TestRpcSessionStateE2E(t *testing.T) {
forkedSession.Disconnect()
})

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

_, err = client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{SessionID: session.SessionID})
if err == nil {
t.Fatal("Expected fork on empty session to fail")
fork, err := client.RPC.Sessions.Fork(t.Context(), &rpc.SessionsForkRequest{SessionID: session.SessionID})
if err != nil {
errText := strings.ToLower(err.Error())
if !strings.Contains(errText, "not found or has no persisted events") {
t.Errorf("Expected error mentioning 'not found or has no persisted events', got %v", err)
}
if strings.Contains(errText, "unhandled method sessions.fork") {
t.Errorf("sessions.fork should be implemented; error suggests it isn't: %v", err)
}
return
}
if !strings.Contains(strings.ToLower(err.Error()), "not found or has no persisted events") {
t.Errorf("Expected error mentioning 'not found or has no persisted events', got %v", err)
if fork == nil {
t.Fatal("Expected non-nil fork result")
}
if strings.Contains(strings.ToLower(err.Error()), "unhandled method sessions.fork") {
t.Errorf("sessions.fork should be implemented; error suggests it isn't: %v", err)
if strings.TrimSpace(fork.SessionID) == "" {
t.Fatal("Expected non-empty fork session id")
}
if fork.SessionID == session.SessionID {
t.Errorf("Expected fork session id to differ from source %q", session.SessionID)
}

forkedSession, err := client.ResumeSession(t.Context(), fork.SessionID, &copilot.ResumeSessionConfig{
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
})
if err != nil {
t.Fatalf("Failed to resume forked session: %v", err)
}
defer forkedSession.Disconnect()

forkedMessages, err := forkedSession.GetMessages(t.Context())
if err != nil {
t.Fatalf("Failed to read forked messages: %v", err)
}
if forkedConversation := conversationMessages(forkedMessages); len(forkedConversation) != 0 {
t.Errorf("Expected empty forked conversation, got %v", forkedConversation)
}
})

Expand Down
28 changes: 21 additions & 7 deletions nodejs/test/e2e/rpc_session_state.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,20 +191,34 @@ describe("Session-scoped RPC", async () => {
await session.disconnect();
});

it("should report error when forking session without persisted events", async () => {
it("should handle forking session without persisted events", async () => {
const session = await client.createSession({ onPermissionRequest: approveAll });

await expect(client.rpc.sessions.fork({ sessionId: session.sessionId })).rejects.toSatisfy(
(err: unknown) => {
try {
let fork: Awaited<ReturnType<typeof client.rpc.sessions.fork>>;
try {
fork = await client.rpc.sessions.fork({ sessionId: session.sessionId });
} catch (err: unknown) {
const text =
err instanceof Error ? `${err.message}\n${err.stack ?? ""}` : String(err);
expect(text.toLowerCase()).toContain("not found or has no persisted events");
expect(text.toLowerCase()).not.toContain("unhandled method sessions.fork");
return true;
return;
}
);

await session.disconnect();
expect(fork.sessionId.trim()).toBeTruthy();
expect(fork.sessionId).not.toBe(session.sessionId);

const forkedSession = await client.resumeSession(fork.sessionId, {
onPermissionRequest: approveAll,
});
try {
expect(getConversationMessages(await forkedSession.getMessages())).toEqual([]);
} finally {
await forkedSession.disconnect();
}
} finally {
await session.disconnect();
}
});

it("should fork session to event id excluding boundary event", async () => {
Expand Down
26 changes: 20 additions & 6 deletions python/e2e/test_rpc_session_state_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,20 +216,34 @@ async def test_should_fork_session_with_persisted_messages(self, ctx: E2ETestCon
finally:
await session.disconnect()

async def test_should_report_error_when_forking_session_without_persisted_events(
async def test_should_handle_forking_session_without_persisted_events(
self, ctx: E2ETestContext
):
session = await ctx.client.create_session(
on_permission_request=PermissionHandler.approve_all,
)
try:
with pytest.raises(Exception) as excinfo:
await ctx.client.rpc.sessions.fork(
try:
fork = await ctx.client.rpc.sessions.fork(
SessionsForkRequest(session_id=session.session_id)
)
text = str(excinfo.value).lower()
assert "not found or has no persisted events" in text
assert "unhandled method sessions.fork" not in text
except Exception as exc:
text = str(exc).lower()
assert "not found or has no persisted events" in text
assert "unhandled method sessions.fork" not in text
return

assert fork.session_id.strip()
assert fork.session_id != session.session_id

forked_session = await ctx.client.resume_session(
fork.session_id,
on_permission_request=PermissionHandler.approve_all,
)
try:
assert _conversation_messages(await forked_session.get_messages()) == []
finally:
await forked_session.disconnect()
finally:
await session.disconnect()

Expand Down
Loading