Skip to content

Commit 282a297

Browse files
patnikoCopilot
authored andcommitted
test: add E2E tests and scenario for tool overrides
Add E2E tests across all 4 SDKs verifying that registering a custom tool with the same name as a built-in tool (e.g., 'grep') causes the custom tool to be invoked instead of the built-in. This validates the mergeExcludedTools feature end-to-end. - Add 'overrides built-in tool with custom tool' test to Node, Python, Go, .NET - Add YAML snapshot for the replay proxy - Add test/scenarios/tools/tool-overrides/ with all 4 language implementations Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 94e9daa commit 282a297

16 files changed

Lines changed: 506 additions & 0 deletions

File tree

dotnet/test/ToolsTests.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,29 @@ record City(int CountryId, string CityName, int Population);
152152
[JsonSerializable(typeof(JsonElement))]
153153
private partial class ToolsTestsJsonContext : JsonSerializerContext;
154154

155+
[Fact]
156+
public async Task Overrides_Built_In_Tool_With_Custom_Tool()
157+
{
158+
var session = await CreateSessionAsync(new SessionConfig
159+
{
160+
Tools = [AIFunctionFactory.Create(CustomGrep, "grep")],
161+
OnPermissionRequest = PermissionHandler.ApproveAll,
162+
});
163+
164+
await session.SendAsync(new MessageOptions
165+
{
166+
Prompt = "Use grep to search for the word 'hello'"
167+
});
168+
169+
var assistantMessage = await TestHelper.GetFinalAssistantMessageAsync(session);
170+
Assert.NotNull(assistantMessage);
171+
Assert.Contains("CUSTOM_GREP_RESULT", assistantMessage!.Data.Content ?? string.Empty);
172+
173+
[Description("A custom grep implementation that overrides the built-in")]
174+
static string CustomGrep([Description("Search query")] string query)
175+
=> $"CUSTOM_GREP_RESULT: {query}";
176+
}
177+
155178
[Fact(Skip = "Behaves as if no content was in the result. Likely that binary results aren't fully implemented yet.")]
156179
public async Task Can_Return_Binary_Result()
157180
{

go/internal/e2e/tools_test.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,41 @@ func TestTools(t *testing.T) {
264264
}
265265
})
266266

267+
t.Run("overrides built-in tool with custom tool", func(t *testing.T) {
268+
ctx.ConfigureForTest(t)
269+
270+
type GrepParams struct {
271+
Query string `json:"query" jsonschema:"Search query"`
272+
}
273+
274+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
275+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
276+
Tools: []copilot.Tool{
277+
copilot.DefineTool("grep", "A custom grep implementation that overrides the built-in",
278+
func(params GrepParams, inv copilot.ToolInvocation) (string, error) {
279+
return "CUSTOM_GREP_RESULT: " + params.Query, nil
280+
}),
281+
},
282+
})
283+
if err != nil {
284+
t.Fatalf("Failed to create session: %v", err)
285+
}
286+
287+
_, err = session.Send(t.Context(), copilot.MessageOptions{Prompt: "Use grep to search for the word 'hello'"})
288+
if err != nil {
289+
t.Fatalf("Failed to send message: %v", err)
290+
}
291+
292+
answer, err := testharness.GetFinalAssistantMessage(t.Context(), session)
293+
if err != nil {
294+
t.Fatalf("Failed to get assistant message: %v", err)
295+
}
296+
297+
if answer.Data.Content == nil || !strings.Contains(*answer.Data.Content, "CUSTOM_GREP_RESULT") {
298+
t.Errorf("Expected answer to contain 'CUSTOM_GREP_RESULT', got %v", answer.Data.Content)
299+
}
300+
})
301+
267302
t.Run("invokes custom tool with permission handler", func(t *testing.T) {
268303
ctx.ConfigureForTest(t)
269304

nodejs/test/e2e/tools.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,26 @@ describe("Custom tools", async () => {
162162
expect(customToolRequests[0].toolName).toBe("encrypt_string");
163163
});
164164

165+
it("overrides built-in tool with custom tool", async () => {
166+
const session = await client.createSession({
167+
onPermissionRequest: approveAll,
168+
tools: [
169+
defineTool("grep", {
170+
description: "A custom grep implementation that overrides the built-in",
171+
parameters: z.object({
172+
query: z.string().describe("Search query"),
173+
}),
174+
handler: ({ query }) => `CUSTOM_GREP_RESULT: ${query}`,
175+
}),
176+
],
177+
});
178+
179+
const assistantMessage = await session.sendAndWait({
180+
prompt: "Use grep to search for the word 'hello'",
181+
});
182+
expect(assistantMessage?.data.content).toContain("CUSTOM_GREP_RESULT");
183+
});
184+
165185
it("denies custom tool when permission denied", async () => {
166186
let toolHandlerCalled = false;
167187

python/e2e/test_tools.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,22 @@ def db_query(params: DbQueryParams, invocation: ToolInvocation) -> list[City]:
133133
assert "135460" in response_content.replace(",", "")
134134
assert "204356" in response_content.replace(",", "")
135135

136+
async def test_overrides_built_in_tool_with_custom_tool(self, ctx: E2ETestContext):
137+
class GrepParams(BaseModel):
138+
query: str = Field(description="Search query")
139+
140+
@define_tool("grep", description="A custom grep implementation that overrides the built-in")
141+
def custom_grep(params: GrepParams, invocation: ToolInvocation) -> str:
142+
return f"CUSTOM_GREP_RESULT: {params.query}"
143+
144+
session = await ctx.client.create_session(
145+
{"tools": [custom_grep], "on_permission_request": PermissionHandler.approve_all}
146+
)
147+
148+
await session.send({"prompt": "Use grep to search for the word 'hello'"})
149+
assistant_message = await get_final_assistant_message(session)
150+
assert "CUSTOM_GREP_RESULT" in assistant_message.data.content
151+
136152
async def test_invokes_custom_tool_with_permission_handler(self, ctx: E2ETestContext):
137153
class EncryptParams(BaseModel):
138154
input: str = Field(description="String to encrypt")
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Config Sample: Tool Overrides
2+
3+
Demonstrates how registering a custom tool with the same name as a built-in tool automatically overrides the built-in. The SDK's `mergeExcludedTools` logic adds custom tool names to `excludedTools`, so the CLI uses your implementation instead.
4+
5+
## What Each Sample Does
6+
7+
1. Creates a session with a custom `grep` tool that returns `"CUSTOM_GREP_RESULT: <query>"`
8+
2. Sends: _"Use grep to search for the word 'hello'"_
9+
3. Prints the response — which should contain `CUSTOM_GREP_RESULT` (proving the custom tool ran, not the built-in)
10+
11+
## Configuration
12+
13+
| Option | Value | Effect |
14+
|--------|-------|--------|
15+
| `tools` | Custom `grep` tool | Overrides the built-in `grep` with a custom implementation |
16+
17+
Behind the scenes, the SDK automatically adds `"grep"` to `excludedTools` so the CLI's built-in grep is disabled.
18+
19+
## Run
20+
21+
```bash
22+
./verify.sh
23+
```
24+
25+
Requires the `copilot` binary (auto-detected or set `COPILOT_CLI_PATH`) and `GITHUB_TOKEN`.
26+
27+
## Verification
28+
29+
The verify script checks that:
30+
- The response contains `CUSTOM_GREP_RESULT` (custom tool was invoked)
31+
- The response does **not** contain typical built-in grep output patterns
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.ComponentModel;
2+
using GitHub.Copilot.SDK;
3+
using Microsoft.Extensions.AI;
4+
5+
using var client = new CopilotClient(new CopilotClientOptions
6+
{
7+
CliPath = Environment.GetEnvironmentVariable("COPILOT_CLI_PATH"),
8+
GitHubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"),
9+
});
10+
11+
await client.StartAsync();
12+
13+
try
14+
{
15+
await using var session = await client.CreateSessionAsync(new SessionConfig
16+
{
17+
Model = "claude-haiku-4.5",
18+
OnPermissionRequest = PermissionHandler.ApproveAll,
19+
Tools = [AIFunctionFactory.Create(CustomGrep, "grep")],
20+
});
21+
22+
var response = await session.SendAndWaitAsync(new MessageOptions
23+
{
24+
Prompt = "Use grep to search for the word 'hello'",
25+
});
26+
27+
if (response != null)
28+
{
29+
Console.WriteLine(response.Data?.Content);
30+
}
31+
}
32+
finally
33+
{
34+
await client.StopAsync();
35+
}
36+
37+
[Description("A custom grep implementation that overrides the built-in")]
38+
static string CustomGrep([Description("Search query")] string query)
39+
=> $"CUSTOM_GREP_RESULT: {query}";
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<OutputType>Exe</OutputType>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<RollForward>LatestMajor</RollForward>
6+
<ImplicitUsings>enable</ImplicitUsings>
7+
<Nullable>enable</Nullable>
8+
<CopilotSkipCliDownload>true</CopilotSkipCliDownload>
9+
</PropertyGroup>
10+
<ItemGroup>
11+
<ProjectReference Include="../../../../../dotnet/src/GitHub.Copilot.SDK.csproj" />
12+
</ItemGroup>
13+
</Project>
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/github/copilot-sdk/samples/tools/tool-overrides/go
2+
3+
go 1.24
4+
5+
require github.com/github/copilot-sdk/go v0.0.0
6+
7+
require github.com/google/jsonschema-go v0.4.2 // indirect
8+
9+
replace github.com/github/copilot-sdk/go => ../../../../../go
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
2+
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
3+
github.com/google/jsonschema-go v0.4.2 h1:tmrUohrwoLZZS/P3x7ex0WAVknEkBZM46iALbcqoRA8=
4+
github.com/google/jsonschema-go v0.4.2/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
9+
copilot "github.com/github/copilot-sdk/go"
10+
)
11+
12+
type GrepParams struct {
13+
Query string `json:"query" jsonschema:"Search query"`
14+
}
15+
16+
func main() {
17+
client := copilot.NewClient(&copilot.ClientOptions{
18+
GitHubToken: os.Getenv("GITHUB_TOKEN"),
19+
})
20+
21+
ctx := context.Background()
22+
if err := client.Start(ctx); err != nil {
23+
log.Fatal(err)
24+
}
25+
defer client.Stop()
26+
27+
session, err := client.CreateSession(ctx, &copilot.SessionConfig{
28+
Model: "claude-haiku-4.5",
29+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
30+
Tools: []copilot.Tool{
31+
copilot.DefineTool("grep", "A custom grep implementation that overrides the built-in",
32+
func(params GrepParams, inv copilot.ToolInvocation) (string, error) {
33+
return "CUSTOM_GREP_RESULT: " + params.Query, nil
34+
}),
35+
},
36+
})
37+
if err != nil {
38+
log.Fatal(err)
39+
}
40+
defer session.Destroy()
41+
42+
response, err := session.SendAndWait(ctx, copilot.MessageOptions{
43+
Prompt: "Use grep to search for the word 'hello'",
44+
})
45+
if err != nil {
46+
log.Fatal(err)
47+
}
48+
49+
if response != nil && response.Data.Content != nil {
50+
fmt.Println(*response.Data.Content)
51+
}
52+
}

0 commit comments

Comments
 (0)