Skip to content

Commit 991b977

Browse files
test: add E2E tests for new RPC features across all SDKs
- Python: test_rpc.py - mode, plan, workspace tests - Go: rpc_test.go - mode, plan, workspace tests - .NET: RpcTests.cs - mode, plan, workspace tests - Fix C# codegen to use enum type for method signatures (not just request classes) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent dd06281 commit 991b977

File tree

5 files changed

+355
-2
lines changed

5 files changed

+355
-2
lines changed

dotnet/src/Generated/Rpc.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ public async Task<SessionModeGetResult> GetAsync(CancellationToken cancellationT
513513
}
514514

515515
/// <summary>Calls "session.mode.set".</summary>
516-
public async Task<SessionModeSetResult> SetAsync(string mode, CancellationToken cancellationToken = default)
516+
public async Task<SessionModeSetResult> SetAsync(SessionModeGetResultMode mode, CancellationToken cancellationToken = default)
517517
{
518518
var request = new SetRequest { SessionId = _sessionId, Mode = mode };
519519
return await CopilotClient.InvokeRpcAsync<SessionModeSetResult>(_rpc, "session.mode.set", [request], cancellationToken);

dotnet/test/RpcTests.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
* Copyright (c) Microsoft Corporation. All rights reserved.
33
*--------------------------------------------------------------------------------------------*/
44

5+
using GitHub.Copilot.SDK.Rpc;
56
using GitHub.Copilot.SDK.Test.Harness;
67
using Xunit;
78
using Xunit.Abstractions;
@@ -79,4 +80,83 @@ public async Task Should_Call_Session_Rpc_Model_SwitchTo()
7980
var after = await session.Rpc.Model.GetCurrentAsync();
8081
Assert.Equal("gpt-4.1", after.ModelId);
8182
}
83+
84+
[Fact]
85+
public async Task Should_Get_And_Set_Session_Mode()
86+
{
87+
var session = await Client.CreateSessionAsync();
88+
89+
// Get initial mode (default should be interactive)
90+
var initial = await session.Rpc.Mode.GetAsync();
91+
Assert.Equal(SessionModeGetResultMode.Interactive, initial.Mode);
92+
93+
// Switch to plan mode
94+
var planResult = await session.Rpc.Mode.SetAsync(SessionModeGetResultMode.Plan);
95+
Assert.Equal(SessionModeGetResultMode.Plan, planResult.Mode);
96+
97+
// Verify mode persisted
98+
var afterPlan = await session.Rpc.Mode.GetAsync();
99+
Assert.Equal(SessionModeGetResultMode.Plan, afterPlan.Mode);
100+
101+
// Switch back to interactive
102+
var interactiveResult = await session.Rpc.Mode.SetAsync(SessionModeGetResultMode.Interactive);
103+
Assert.Equal(SessionModeGetResultMode.Interactive, interactiveResult.Mode);
104+
}
105+
106+
[Fact]
107+
public async Task Should_Read_Update_And_Delete_Plan()
108+
{
109+
var session = await Client.CreateSessionAsync();
110+
111+
// Initially plan should not exist
112+
var initial = await session.Rpc.Plan.ReadAsync();
113+
Assert.False(initial.Exists);
114+
Assert.Null(initial.Content);
115+
116+
// Create/update plan
117+
var planContent = "# Test Plan\n\n- Step 1\n- Step 2";
118+
await session.Rpc.Plan.UpdateAsync(planContent);
119+
120+
// Verify plan exists and has correct content
121+
var afterUpdate = await session.Rpc.Plan.ReadAsync();
122+
Assert.True(afterUpdate.Exists);
123+
Assert.Equal(planContent, afterUpdate.Content);
124+
125+
// Delete plan
126+
await session.Rpc.Plan.DeleteAsync();
127+
128+
// Verify plan is deleted
129+
var afterDelete = await session.Rpc.Plan.ReadAsync();
130+
Assert.False(afterDelete.Exists);
131+
Assert.Null(afterDelete.Content);
132+
}
133+
134+
[Fact]
135+
public async Task Should_Create_List_And_Read_Workspace_Files()
136+
{
137+
var session = await Client.CreateSessionAsync();
138+
139+
// Initially no files
140+
var initialFiles = await session.Rpc.Workspace.ListFilesAsync();
141+
Assert.Empty(initialFiles.Files);
142+
143+
// Create a file
144+
var fileContent = "Hello, workspace!";
145+
await session.Rpc.Workspace.CreateFileAsync("test.txt", fileContent);
146+
147+
// List files
148+
var afterCreate = await session.Rpc.Workspace.ListFilesAsync();
149+
Assert.Contains("test.txt", afterCreate.Files);
150+
151+
// Read file
152+
var readResult = await session.Rpc.Workspace.ReadFileAsync("test.txt");
153+
Assert.Equal(fileContent, readResult.Content);
154+
155+
// Create nested file
156+
await session.Rpc.Workspace.CreateFileAsync("subdir/nested.txt", "Nested content");
157+
158+
var afterNested = await session.Rpc.Workspace.ListFilesAsync();
159+
Assert.Contains("test.txt", afterNested.Files);
160+
Assert.Contains(afterNested.Files, f => f.Contains("nested.txt"));
161+
}
82162
}

go/internal/e2e/rpc_test.go

Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package e2e
22

33
import (
4+
"strings"
45
"testing"
56

67
copilot "github.com/github/copilot-sdk/go"
@@ -185,4 +186,185 @@ func TestSessionRpc(t *testing.T) {
185186
t.Errorf("Expected modelId 'gpt-4.1' after switch, got %v", after.ModelID)
186187
}
187188
})
189+
190+
t.Run("should get and set session mode", func(t *testing.T) {
191+
session, err := client.CreateSession(t.Context(), nil)
192+
if err != nil {
193+
t.Fatalf("Failed to create session: %v", err)
194+
}
195+
196+
// Get initial mode (default should be interactive)
197+
initial, err := session.RPC.Mode.Get(t.Context())
198+
if err != nil {
199+
t.Fatalf("Failed to get mode: %v", err)
200+
}
201+
if initial.Mode != rpc.Interactive {
202+
t.Errorf("Expected initial mode 'interactive', got %q", initial.Mode)
203+
}
204+
205+
// Switch to plan mode
206+
planResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Plan})
207+
if err != nil {
208+
t.Fatalf("Failed to set mode to plan: %v", err)
209+
}
210+
if planResult.Mode != rpc.Plan {
211+
t.Errorf("Expected mode 'plan', got %q", planResult.Mode)
212+
}
213+
214+
// Verify mode persisted
215+
afterPlan, err := session.RPC.Mode.Get(t.Context())
216+
if err != nil {
217+
t.Fatalf("Failed to get mode after plan: %v", err)
218+
}
219+
if afterPlan.Mode != rpc.Plan {
220+
t.Errorf("Expected mode 'plan' after set, got %q", afterPlan.Mode)
221+
}
222+
223+
// Switch back to interactive
224+
interactiveResult, err := session.RPC.Mode.Set(t.Context(), &rpc.SessionModeSetParams{Mode: rpc.Interactive})
225+
if err != nil {
226+
t.Fatalf("Failed to set mode to interactive: %v", err)
227+
}
228+
if interactiveResult.Mode != rpc.Interactive {
229+
t.Errorf("Expected mode 'interactive', got %q", interactiveResult.Mode)
230+
}
231+
})
232+
233+
t.Run("should read, update, and delete plan", func(t *testing.T) {
234+
session, err := client.CreateSession(t.Context(), nil)
235+
if err != nil {
236+
t.Fatalf("Failed to create session: %v", err)
237+
}
238+
239+
// Initially plan should not exist
240+
initial, err := session.RPC.Plan.Read(t.Context())
241+
if err != nil {
242+
t.Fatalf("Failed to read plan: %v", err)
243+
}
244+
if initial.Exists {
245+
t.Error("Expected plan to not exist initially")
246+
}
247+
if initial.Content != nil {
248+
t.Error("Expected content to be nil initially")
249+
}
250+
251+
// Create/update plan
252+
planContent := "# Test Plan\n\n- Step 1\n- Step 2"
253+
_, err = session.RPC.Plan.Update(t.Context(), &rpc.SessionPlanUpdateParams{Content: planContent})
254+
if err != nil {
255+
t.Fatalf("Failed to update plan: %v", err)
256+
}
257+
258+
// Verify plan exists and has correct content
259+
afterUpdate, err := session.RPC.Plan.Read(t.Context())
260+
if err != nil {
261+
t.Fatalf("Failed to read plan after update: %v", err)
262+
}
263+
if !afterUpdate.Exists {
264+
t.Error("Expected plan to exist after update")
265+
}
266+
if afterUpdate.Content == nil || *afterUpdate.Content != planContent {
267+
t.Errorf("Expected content %q, got %v", planContent, afterUpdate.Content)
268+
}
269+
270+
// Delete plan
271+
_, err = session.RPC.Plan.Delete(t.Context())
272+
if err != nil {
273+
t.Fatalf("Failed to delete plan: %v", err)
274+
}
275+
276+
// Verify plan is deleted
277+
afterDelete, err := session.RPC.Plan.Read(t.Context())
278+
if err != nil {
279+
t.Fatalf("Failed to read plan after delete: %v", err)
280+
}
281+
if afterDelete.Exists {
282+
t.Error("Expected plan to not exist after delete")
283+
}
284+
if afterDelete.Content != nil {
285+
t.Error("Expected content to be nil after delete")
286+
}
287+
})
288+
289+
t.Run("should create, list, and read workspace files", func(t *testing.T) {
290+
session, err := client.CreateSession(t.Context(), nil)
291+
if err != nil {
292+
t.Fatalf("Failed to create session: %v", err)
293+
}
294+
295+
// Initially no files
296+
initialFiles, err := session.RPC.Workspace.ListFiles(t.Context())
297+
if err != nil {
298+
t.Fatalf("Failed to list files: %v", err)
299+
}
300+
if len(initialFiles.Files) != 0 {
301+
t.Errorf("Expected no files initially, got %v", initialFiles.Files)
302+
}
303+
304+
// Create a file
305+
fileContent := "Hello, workspace!"
306+
_, err = session.RPC.Workspace.CreateFile(t.Context(), &rpc.SessionWorkspaceCreateFileParams{
307+
Path: "test.txt",
308+
Content: fileContent,
309+
})
310+
if err != nil {
311+
t.Fatalf("Failed to create file: %v", err)
312+
}
313+
314+
// List files
315+
afterCreate, err := session.RPC.Workspace.ListFiles(t.Context())
316+
if err != nil {
317+
t.Fatalf("Failed to list files after create: %v", err)
318+
}
319+
if !containsString(afterCreate.Files, "test.txt") {
320+
t.Errorf("Expected files to contain 'test.txt', got %v", afterCreate.Files)
321+
}
322+
323+
// Read file
324+
readResult, err := session.RPC.Workspace.ReadFile(t.Context(), &rpc.SessionWorkspaceReadFileParams{
325+
Path: "test.txt",
326+
})
327+
if err != nil {
328+
t.Fatalf("Failed to read file: %v", err)
329+
}
330+
if readResult.Content != fileContent {
331+
t.Errorf("Expected content %q, got %q", fileContent, readResult.Content)
332+
}
333+
334+
// Create nested file
335+
_, err = session.RPC.Workspace.CreateFile(t.Context(), &rpc.SessionWorkspaceCreateFileParams{
336+
Path: "subdir/nested.txt",
337+
Content: "Nested content",
338+
})
339+
if err != nil {
340+
t.Fatalf("Failed to create nested file: %v", err)
341+
}
342+
343+
afterNested, err := session.RPC.Workspace.ListFiles(t.Context())
344+
if err != nil {
345+
t.Fatalf("Failed to list files after nested: %v", err)
346+
}
347+
if !containsString(afterNested.Files, "test.txt") {
348+
t.Errorf("Expected files to contain 'test.txt', got %v", afterNested.Files)
349+
}
350+
hasNested := false
351+
for _, f := range afterNested.Files {
352+
if strings.Contains(f, "nested.txt") {
353+
hasNested = true
354+
break
355+
}
356+
}
357+
if !hasNested {
358+
t.Errorf("Expected files to contain 'nested.txt', got %v", afterNested.Files)
359+
}
360+
})
361+
}
362+
363+
func containsString(slice []string, str string) bool {
364+
for _, s := range slice {
365+
if s == str {
366+
return true
367+
}
368+
}
369+
return false
188370
}

python/e2e/test_rpc.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,94 @@ async def test_should_call_session_rpc_model_switch_to(self, ctx: E2ETestContext
102102
# Verify the switch persisted
103103
after = await session.rpc.model.get_current()
104104
assert after.model_id == "gpt-4.1"
105+
106+
@pytest.mark.asyncio
107+
async def test_get_and_set_session_mode(self, ctx: E2ETestContext):
108+
"""Test getting and setting session mode"""
109+
from copilot.generated.rpc import Mode, SessionModeSetParams
110+
111+
session = await ctx.client.create_session({})
112+
113+
# Get initial mode (default should be interactive)
114+
initial = await session.rpc.mode.get()
115+
assert initial.mode == Mode.INTERACTIVE
116+
117+
# Switch to plan mode
118+
plan_result = await session.rpc.mode.set(SessionModeSetParams(mode=Mode.PLAN))
119+
assert plan_result.mode == Mode.PLAN
120+
121+
# Verify mode persisted
122+
after_plan = await session.rpc.mode.get()
123+
assert after_plan.mode == Mode.PLAN
124+
125+
# Switch back to interactive
126+
interactive_result = await session.rpc.mode.set(SessionModeSetParams(mode=Mode.INTERACTIVE))
127+
assert interactive_result.mode == Mode.INTERACTIVE
128+
129+
@pytest.mark.asyncio
130+
async def test_read_update_and_delete_plan(self, ctx: E2ETestContext):
131+
"""Test reading, updating, and deleting plan"""
132+
from copilot.generated.rpc import SessionPlanUpdateParams
133+
134+
session = await ctx.client.create_session({})
135+
136+
# Initially plan should not exist
137+
initial = await session.rpc.plan.read()
138+
assert initial.exists is False
139+
assert initial.content is None
140+
141+
# Create/update plan
142+
plan_content = "# Test Plan\n\n- Step 1\n- Step 2"
143+
await session.rpc.plan.update(SessionPlanUpdateParams(content=plan_content))
144+
145+
# Verify plan exists and has correct content
146+
after_update = await session.rpc.plan.read()
147+
assert after_update.exists is True
148+
assert after_update.content == plan_content
149+
150+
# Delete plan
151+
await session.rpc.plan.delete()
152+
153+
# Verify plan is deleted
154+
after_delete = await session.rpc.plan.read()
155+
assert after_delete.exists is False
156+
assert after_delete.content is None
157+
158+
@pytest.mark.asyncio
159+
async def test_create_list_and_read_workspace_files(self, ctx: E2ETestContext):
160+
"""Test creating, listing, and reading workspace files"""
161+
from copilot.generated.rpc import (
162+
SessionWorkspaceCreateFileParams,
163+
SessionWorkspaceReadFileParams,
164+
)
165+
166+
session = await ctx.client.create_session({})
167+
168+
# Initially no files
169+
initial_files = await session.rpc.workspace.list_files()
170+
assert initial_files.files == []
171+
172+
# Create a file
173+
file_content = "Hello, workspace!"
174+
await session.rpc.workspace.create_file(
175+
SessionWorkspaceCreateFileParams(content=file_content, path="test.txt")
176+
)
177+
178+
# List files
179+
after_create = await session.rpc.workspace.list_files()
180+
assert "test.txt" in after_create.files
181+
182+
# Read file
183+
read_result = await session.rpc.workspace.read_file(
184+
SessionWorkspaceReadFileParams(path="test.txt")
185+
)
186+
assert read_result.content == file_content
187+
188+
# Create nested file
189+
await session.rpc.workspace.create_file(
190+
SessionWorkspaceCreateFileParams(content="Nested content", path="subdir/nested.txt")
191+
)
192+
193+
after_nested = await session.rpc.workspace.list_files()
194+
assert "test.txt" in after_nested.files
195+
assert any("nested.txt" in f for f in after_nested.files)

scripts/codegen/csharp.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -697,7 +697,7 @@ function emitSessionApiClass(className: string, node: Record<string, unknown>, c
697697

698698
for (const [pName, pSchema] of paramEntries) {
699699
if (typeof pSchema !== "object") continue;
700-
const csType = schemaTypeToCSharp(pSchema as JSONSchema7, requiredSet.has(pName), rpcKnownTypes);
700+
const csType = resolveRpcType(pSchema as JSONSchema7, requiredSet.has(pName), requestClassName, toPascalCase(pName), classes);
701701
sigParams.push(`${csType} ${pName}`);
702702
bodyAssignments.push(`${toPascalCase(pName)} = ${pName}`);
703703
}

0 commit comments

Comments
 (0)