Skip to content

Commit c9c3648

Browse files
stephentoubCopilot
andauthored
Make agent reload test runtime-compatible (#1201)
* Make agent reload test runtime-compatible Use a unique reload agent name and assert reload results match the subsequent list result instead of depending on whether the runtime preserves session-configured agents after reload. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Align agent reload tests across SDKs Apply the same runtime-compatible reload test pattern to Go and Python by using unique reload agent names and asserting reload results match subsequent agent list results. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Add Node agent reload E2E coverage Add the Node.js counterpart for the runtime-compatible agent reload RPC test, including a unique agent name and reload/list consistency assertion. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 100390a commit c9c3648

4 files changed

Lines changed: 117 additions & 36 deletions

File tree

dotnet/test/E2E/RpcAgentE2ETests.cs

Lines changed: 13 additions & 4 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;
@@ -104,10 +105,11 @@ public async Task Should_Return_Empty_List_When_No_Custom_Agents_Configured()
104105
[Fact]
105106
public async Task Should_Call_Agent_Reload()
106107
{
107-
var session = await CreateSessionAsync(new SessionConfig { CustomAgents = [CreateReloadAgent()] });
108+
var reloadAgent = CreateReloadAgent($"reload-test-agent-{Guid.NewGuid():N}");
109+
var session = await CreateSessionAsync(new SessionConfig { CustomAgents = [reloadAgent] });
108110

109111
var before = await session.Rpc.Agent.ListAsync();
110-
Assert.Single(before.Agents, agent => string.Equals(agent.Name, "reload-test-agent", StringComparison.Ordinal));
112+
AssertReloadAgent(before.Agents, reloadAgent);
111113

112114
var result = await session.Rpc.Agent.ReloadAsync();
113115
var current = await session.Rpc.Agent.ListAsync();
@@ -120,6 +122,13 @@ public async Task Should_Call_Agent_Reload()
120122
current.Agents.Select(agent => agent.DisplayName).OrderBy(name => name, StringComparer.Ordinal));
121123
}
122124

125+
private static void AssertReloadAgent(IEnumerable<AgentInfo> agents, CustomAgentConfig expected)
126+
{
127+
var agent = Assert.Single(agents, agent => string.Equals(agent.Name, expected.Name, StringComparison.Ordinal));
128+
Assert.Equal(expected.DisplayName, agent.DisplayName);
129+
Assert.Equal(expected.Description, agent.Description);
130+
}
131+
123132
private static List<CustomAgentConfig> CreateCustomAgents() =>
124133
[
125134
new()
@@ -138,10 +147,10 @@ private static List<CustomAgentConfig> CreateCustomAgents() =>
138147
}
139148
];
140149

141-
private static CustomAgentConfig CreateReloadAgent() =>
150+
private static CustomAgentConfig CreateReloadAgent(string name) =>
142151
new()
143152
{
144-
Name = "reload-test-agent",
153+
Name = name,
145154
DisplayName = "Reload Test Agent",
146155
Description = "Used by the agent reload RPC test.",
147156
Prompt = "You are a reload test agent.",

go/internal/e2e/agent_and_compact_rpc_e2e_test.go

Lines changed: 48 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package e2e
22

33
import (
4+
"fmt"
5+
"slices"
46
"testing"
7+
"time"
58

69
copilot "github.com/github/copilot-sdk/go"
710
"github.com/github/copilot-sdk/go/internal/e2e/testharness"
@@ -253,6 +256,13 @@ func TestAgentSelectionRpcE2E(t *testing.T) {
253256
})
254257

255258
t.Run("should call agent reload", func(t *testing.T) {
259+
reloadAgent := copilot.CustomAgentConfig{
260+
Name: fmt.Sprintf("reload-test-agent-%d", time.Now().UnixNano()),
261+
DisplayName: "Reload Test Agent",
262+
Description: "Used by the agent reload RPC test.",
263+
Prompt: "You are a reload test agent.",
264+
}
265+
256266
client := copilot.NewClient(&copilot.ClientOptions{
257267
CLIPath: cliPath,
258268
UseStdio: copilot.Bool(true),
@@ -266,12 +276,7 @@ func TestAgentSelectionRpcE2E(t *testing.T) {
266276
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
267277
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
268278
CustomAgents: []copilot.CustomAgentConfig{
269-
{
270-
Name: "reload-test-agent",
271-
DisplayName: "Reload Test Agent",
272-
Description: "Used by the agent reload RPC test.",
273-
Prompt: "You are a reload test agent.",
274-
},
279+
reloadAgent,
275280
},
276281
})
277282
if err != nil {
@@ -282,35 +287,58 @@ func TestAgentSelectionRpcE2E(t *testing.T) {
282287
if err != nil {
283288
t.Fatalf("Failed to list agents: %v", err)
284289
}
285-
var sawReloadAgent bool
286-
for _, agent := range before.Agents {
287-
if agent.Name == "reload-test-agent" {
288-
sawReloadAgent = true
289-
break
290-
}
291-
}
292-
if !sawReloadAgent {
293-
t.Fatalf("Expected reload-test-agent in initial Agent.List, got %+v", before.Agents)
294-
}
290+
assertReloadAgent(t, before.Agents, reloadAgent)
295291

296-
// Reload should succeed; the runtime currently drops session-configured
297-
// CustomAgents on reload, so we only assert the result shape is non-nil.
298-
// Once that runtime behavior is fixed, tighten this to assert
299-
// reload-test-agent is still present.
300292
result, err := session.RPC.Agent.Reload(t.Context())
301293
if err != nil {
302294
t.Fatalf("Failed to reload agents: %v", err)
303295
}
304296
if result.Agents == nil {
305297
t.Errorf("Expected non-nil Agents after reload")
306298
}
299+
current, err := session.RPC.Agent.List(t.Context())
300+
if err != nil {
301+
t.Fatalf("Failed to list agents after reload: %v", err)
302+
}
303+
if got, want := agentSummaries(result.Agents), agentSummaries(current.Agents); !slices.Equal(got, want) {
304+
t.Errorf("Expected reload result agents to match current agents.\nGot: %v\nWant: %v", got, want)
305+
}
307306

308307
if err := client.Stop(); err != nil {
309308
t.Errorf("Expected no errors on stop, got %v", err)
310309
}
311310
})
312311
}
313312

313+
func assertReloadAgent(t *testing.T, agents []rpc.AgentInfo, expected copilot.CustomAgentConfig) {
314+
t.Helper()
315+
316+
var matches []rpc.AgentInfo
317+
for _, agent := range agents {
318+
if agent.Name == expected.Name {
319+
matches = append(matches, agent)
320+
}
321+
}
322+
if len(matches) != 1 {
323+
t.Fatalf("Expected exactly one %q in Agent.List, got %+v", expected.Name, agents)
324+
}
325+
if matches[0].DisplayName != expected.DisplayName {
326+
t.Errorf("Expected reload agent display name %q, got %q", expected.DisplayName, matches[0].DisplayName)
327+
}
328+
if matches[0].Description != expected.Description {
329+
t.Errorf("Expected reload agent description %q, got %q", expected.Description, matches[0].Description)
330+
}
331+
}
332+
333+
func agentSummaries(agents []rpc.AgentInfo) []string {
334+
summaries := make([]string, len(agents))
335+
for i, agent := range agents {
336+
summaries[i] = fmt.Sprintf("%s\x00%s", agent.Name, agent.DisplayName)
337+
}
338+
slices.Sort(summaries)
339+
return summaries
340+
}
341+
314342
func TestSessionCompactionRpcE2E(t *testing.T) {
315343
ctx := testharness.NewTestContext(t)
316344
client := ctx.NewClient()

nodejs/test/e2e/agent_and_compact_rpc.e2e.test.ts

Lines changed: 33 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+
import { randomUUID } from "node:crypto";
56
import { describe, expect, it } from "vitest";
67
import { approveAll } from "../../src/index.js";
78
import type { CustomAgentConfig } from "../../src/index.js";
@@ -127,6 +128,34 @@ describe("Agent Selection RPC", async () => {
127128

128129
await session.disconnect();
129130
});
131+
132+
it("should call agent reload", async () => {
133+
const reloadAgent: CustomAgentConfig = {
134+
name: `reload-test-agent-${randomUUID().replaceAll("-", "")}`,
135+
displayName: "Reload Test Agent",
136+
description: "Used by the agent reload RPC test.",
137+
prompt: "You are a reload test agent.",
138+
};
139+
140+
const session = await client.createSession({
141+
onPermissionRequest: approveAll,
142+
customAgents: [reloadAgent],
143+
});
144+
145+
const before = await session.rpc.agent.list();
146+
const match = before.agents.find((agent) => agent.name === reloadAgent.name);
147+
expect(match).toBeDefined();
148+
expect(match!.displayName).toBe(reloadAgent.displayName);
149+
expect(match!.description).toBe(reloadAgent.description);
150+
151+
const result = await session.rpc.agent.reload();
152+
expect(result.agents).toBeDefined();
153+
154+
const current = await session.rpc.agent.list();
155+
expect(summarizeAgents(result.agents)).toEqual(summarizeAgents(current.agents));
156+
157+
await session.disconnect();
158+
});
130159
});
131160

132161
describe("Session Compact RPC", async () => {
@@ -147,3 +176,7 @@ describe("Session Compact RPC", async () => {
147176
await session.disconnect();
148177
}, 60000);
149178
});
179+
180+
function summarizeAgents(agents: { name: string; displayName: string }[]) {
181+
return agents.map((agent) => `${agent.name}\x00${agent.displayName}`).sort();
182+
}

python/e2e/test_agent_and_compact_rpc_e2e.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
"""E2E tests for Agent Selection and Session Compaction RPC APIs."""
22

3+
import uuid
4+
35
import pytest
46

57
from copilot import CopilotClient
@@ -174,36 +176,45 @@ async def test_should_return_empty_list_when_no_custom_agents_configured(self):
174176
async def test_should_call_agent_reload(self):
175177
"""Test reloading agents via RPC."""
176178
client = CopilotClient(SubprocessConfig(cli_path=CLI_PATH, use_stdio=True))
179+
reload_agent = {
180+
"name": f"reload-test-agent-{uuid.uuid4().hex}",
181+
"display_name": "Reload Agent",
182+
"description": "An agent used to validate reload",
183+
"prompt": "You are a reload test agent.",
184+
}
177185

178186
try:
179187
await client.start()
180188
session = await client.create_session(
181189
on_permission_request=PermissionHandler.approve_all,
182-
custom_agents=[
183-
{
184-
"name": "reload-test-agent",
185-
"display_name": "Reload Agent",
186-
"description": "An agent used to validate reload",
187-
"prompt": "You are a reload test agent.",
188-
}
189-
],
190+
custom_agents=[reload_agent],
190191
)
191192

192193
before = await session.rpc.agent.list()
193-
assert any(agent.name == "reload-test-agent" for agent in before.agents)
194+
_assert_reload_agent(before.agents, reload_agent)
194195

195-
# Reload should succeed and return some agent set. The CLI currently
196-
# drops session-configured CustomAgents on reload, so we don't
197-
# require the reload-test-agent to remain present after reload.
198196
result = await session.rpc.agent.reload()
199197
assert result.agents is not None
198+
current = await session.rpc.agent.list()
199+
assert _agent_summaries(result.agents) == _agent_summaries(current.agents)
200200

201201
await session.disconnect()
202202
await client.stop()
203203
finally:
204204
await client.force_stop()
205205

206206

207+
def _assert_reload_agent(agents, expected):
208+
matches = [agent for agent in agents if agent.name == expected["name"]]
209+
assert len(matches) == 1
210+
assert matches[0].display_name == expected["display_name"]
211+
assert matches[0].description == expected["description"]
212+
213+
214+
def _agent_summaries(agents):
215+
return sorted((agent.name, agent.display_name) for agent in agents)
216+
217+
207218
class TestSessionCompactionRpc:
208219
@pytest.mark.asyncio
209220
async def test_should_compact_session_history_after_messages(self, ctx: E2ETestContext):

0 commit comments

Comments
 (0)