Skip to content

Commit 80bddc7

Browse files
Copilotedburns
andcommitted
Add E2E integration test for ergonomic @copilotTool + ToolDefinition.fromObject() API (#1787)
* Initial plan * Initial plan * Initial plan * Add E2E integration test for ergonomic @copilotTool + ToolDefinition.fromObject() API Create ErgonomicToolDefinitionIT that proves the ergonomic annotation-based API produces identical wire behavior to the low-level ToolDefinition.create() API, tested against the replay proxy. Files added: - test/snapshots/tools/ergonomic_tool_definition.yaml (identical to low_level_tool_definition.yaml since wire format is the same) - java/src/test/java/com/github/copilot/e2e/ErgonomicToolDefinitionIT.java - java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools.java - java/src/test/java/com/github/copilot/e2e/ErgonomicTestTools$$CopilotToolMeta.java Closes #1762 * spotless * fix: use passed ObjectMapper for record-parameter conversion The single-record-parameter shortcut in CopilotToolProcessor generated invocation.getArgumentsAs() which uses an unconfigured ObjectMapper internally (no JavaTimeModule, no SDK settings). Switch to mapper.convertValue(args, RecordType.class) which uses the SDK-configured mapper passed to the definitions() method. Addresses review comment r3469523760. * fix: exclude Optional types from required list in generated schema CopilotToolProcessor.generateSchemaWithParamMetadata() now checks if a parameter type is Optional/OptionalInt/OptionalLong/OptionalDouble before adding it to the JSON Schema required list. This aligns with SchemaGenerator which already treats these types as optional. Addresses review comment r3469523801. * fix: correct misleading Javadoc in ToolDefinitionFromObjectTest The class-level Javadoc incorrectly stated that the annotation processor generates $$CopilotToolMeta fixtures during test compilation. In reality, the module has <proc>none</proc> and these fixtures are hand-written classes under com.github.copilot.rpc.fixtures. Addresses review comment r3469523833. * fix: remove unused grep override tool from E2E test The ErgonomicToolDefinitionIT snapshot only exercises set_current_phase and search_items. The grep tool (with overridesBuiltInTool=true) was never invoked, making it dead code that contradicted the PR description. Addresses review comment r3469523851. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Ed Burns <edburns@microsoft.com>
1 parent 1765d42 commit 80bddc7

6 files changed

Lines changed: 208 additions & 5 deletions

File tree

java/src/main/java/com/github/copilot/tool/CopilotToolProcessor.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,12 @@ private String generateSchemaWithParamMetadata(List<? extends VariableElement> p
237237
// Cast to Map<String, Object> via raw type for consistent Map.ofEntries typing
238238
propertyEntries.add("Map.entry(\"" + paramName + "\", (Map<String, Object>)(Map) " + propertySchema + ")");
239239

240-
// Determine if required
241-
if (paramAnnotation == null || paramAnnotation.required()) {
240+
// Determine if required (Optional* types are never required)
241+
boolean isOptionalType = paramType.getKind() == TypeKind.DECLARED && Set
242+
.of("java.util.Optional", "java.util.OptionalInt", "java.util.OptionalLong",
243+
"java.util.OptionalDouble")
244+
.contains(((TypeElement) ((DeclaredType) paramType).asElement()).getQualifiedName().toString());
245+
if (!isOptionalType && (paramAnnotation == null || paramAnnotation.required())) {
242246
requiredNames.add("\"" + paramName + "\"");
243247
}
244248
}
@@ -284,7 +288,7 @@ private String generateLambdaBody(ExecutableElement method) {
284288
String typeName = getTypeString(params.get(0).asType());
285289
String paramName = params.get(0).getSimpleName().toString();
286290
sb.append(" ").append(typeName).append(" ").append(paramName)
287-
.append(" = invocation.getArgumentsAs(").append(typeName).append(".class);\n");
291+
.append(" = mapper.convertValue(args, ").append(typeName).append(".class);\n");
288292
} else {
289293
for (VariableElement param : params) {
290294
String paramName = getParamName(param);
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Hand-written test fixture mimicking CopilotToolProcessor output.
2+
package com.github.copilot.e2e;
3+
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.github.copilot.rpc.ToolDefinition;
6+
import com.github.copilot.tool.CopilotToolMetadataProvider;
7+
8+
import java.util.*;
9+
import java.util.concurrent.CompletableFuture;
10+
11+
public final class ErgonomicTestTools$$CopilotToolMeta implements CopilotToolMetadataProvider<ErgonomicTestTools> {
12+
13+
private static Map<String, Object> withMeta(Map<String, Object> base, String description, Object defaultValue) {
14+
var result = new LinkedHashMap<String, Object>(base);
15+
if (description != null)
16+
result.put("description", description);
17+
if (defaultValue != null)
18+
result.put("default", defaultValue);
19+
return Collections.unmodifiableMap(result);
20+
}
21+
22+
@Override
23+
@SuppressWarnings({"unchecked", "rawtypes"})
24+
public List<ToolDefinition> definitions(ErgonomicTestTools instance, ObjectMapper mapper) {
25+
return List.of(new ToolDefinition("set_current_phase", "Sets the current phase of the agent",
26+
Map.of("type", "object", "properties",
27+
Map.ofEntries(Map.entry("phase",
28+
(Map<String, Object>) (Map) withMeta(Map.of("type", "string"),
29+
"The phase to transition to", null))),
30+
"required", List.of("phase")),
31+
invocation -> {
32+
Map<String, Object> args = invocation.getArguments();
33+
String phase = (String) args.get("phase");
34+
return CompletableFuture.completedFuture(instance.setCurrentPhase(phase));
35+
}, null, null, null),
36+
new ToolDefinition(
37+
"search_items", "Search for items by keyword", Map
38+
.of("type", "object", "properties",
39+
Map.ofEntries(Map.entry("keyword",
40+
(Map<String, Object>) (Map) withMeta(Map.of("type", "string"),
41+
"Search keyword", null))),
42+
"required", List.of("keyword")),
43+
invocation -> {
44+
Map<String, Object> args = invocation.getArguments();
45+
String keyword = (String) args.get("keyword");
46+
return CompletableFuture.completedFuture(instance.searchItems(keyword));
47+
}, null, null, null));
48+
}
49+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.e2e;
6+
7+
import com.github.copilot.tool.CopilotTool;
8+
import com.github.copilot.tool.Param;
9+
10+
/**
11+
* Tool fixture for the ergonomic {@code @CopilotTool} E2E integration test.
12+
*
13+
* <p>
14+
* This class exercises the annotation-based tool definition API, producing
15+
* identical wire-level tool schemas to the low-level
16+
* {@code ToolDefinition.create()} API.
17+
*/
18+
class ErgonomicTestTools {
19+
20+
String currentPhase;
21+
22+
@CopilotTool("Sets the current phase of the agent")
23+
public String setCurrentPhase(@Param("The phase to transition to") String phase) {
24+
currentPhase = phase;
25+
return "Phase set to " + phase;
26+
}
27+
28+
@CopilotTool("Search for items by keyword")
29+
public String searchItems(@Param("Search keyword") String keyword) {
30+
return "Found: item_alpha, item_beta";
31+
}
32+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
package com.github.copilot.e2e;
6+
7+
import static org.junit.jupiter.api.Assertions.assertNotNull;
8+
import static org.junit.jupiter.api.Assertions.assertTrue;
9+
10+
import java.util.List;
11+
import java.util.concurrent.TimeUnit;
12+
13+
import org.junit.jupiter.api.AfterAll;
14+
import org.junit.jupiter.api.BeforeAll;
15+
import org.junit.jupiter.api.Test;
16+
17+
import com.github.copilot.CopilotClient;
18+
import com.github.copilot.CopilotSession;
19+
import com.github.copilot.E2ETestContext;
20+
import com.github.copilot.generated.AssistantMessageEvent;
21+
import com.github.copilot.rpc.MessageOptions;
22+
import com.github.copilot.rpc.PermissionHandler;
23+
import com.github.copilot.rpc.SessionConfig;
24+
import com.github.copilot.rpc.ToolDefinition;
25+
import com.github.copilot.rpc.ToolSet;
26+
27+
/**
28+
* Failsafe integration test for the ergonomic {@code @CopilotTool} +
29+
* {@code ToolDefinition.fromObject()} API.
30+
*
31+
* <p>
32+
* This test proves that the ergonomic annotation-based API produces identical
33+
* wire behavior to the low-level {@code ToolDefinition.create()} API tested in
34+
* {@code LowLevelToolDefinitionIT}.
35+
*
36+
* @see Snapshot: tools/ergonomic_tool_definition
37+
*/
38+
class ErgonomicToolDefinitionIT {
39+
40+
private static E2ETestContext ctx;
41+
42+
@BeforeAll
43+
static void setup() throws Exception {
44+
ctx = E2ETestContext.create();
45+
}
46+
47+
@AfterAll
48+
static void teardown() throws Exception {
49+
if (ctx != null) {
50+
ctx.close();
51+
}
52+
}
53+
54+
@Test
55+
void ergonomicToolDefinition() throws Exception {
56+
ctx.configureForTest("tools", "ergonomic_tool_definition");
57+
58+
ErgonomicTestTools tools = new ErgonomicTestTools();
59+
List<ToolDefinition> toolDefs = ToolDefinition.fromObject(tools);
60+
61+
try (CopilotClient client = ctx.createClient()) {
62+
CopilotSession session = client
63+
.createSession(new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
64+
.setAvailableTools(new ToolSet().addCustom("*").addBuiltIn("web_fetch")).setTools(toolDefs))
65+
.get(30, TimeUnit.SECONDS);
66+
67+
try {
68+
AssistantMessageEvent response = session.sendAndWait(new MessageOptions().setPrompt(
69+
"First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and search results."),
70+
60_000).get(90, TimeUnit.SECONDS);
71+
72+
assertNotNull(response, "Expected a response from the assistant");
73+
String content = response.getData().content().toLowerCase();
74+
assertTrue(content.contains("analyzing"),
75+
"Response should contain the updated phase: " + response.getData().content());
76+
assertTrue(content.contains("item_alpha") || content.contains("item_beta"),
77+
"Response should contain search results: " + response.getData().content());
78+
assertTrue("analyzing".equals(tools.currentPhase),
79+
"Expected currentPhase to be 'analyzing' but was: " + tools.currentPhase);
80+
} finally {
81+
session.close();
82+
}
83+
}
84+
}
85+
}

java/src/test/java/com/github/copilot/rpc/ToolDefinitionFromObjectTest.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,9 @@
3434
/**
3535
* End-to-end tests for {@link ToolDefinition#fromObject(Object)}.
3636
* <p>
37-
* The annotation processor generates {@code $$CopilotToolMeta} companion
38-
* classes for the fixture classes during test compilation.
37+
* These tests use hand-written {@code $$CopilotToolMeta} companion classes
38+
* under {@code com.github.copilot.rpc.fixtures} that mimic
39+
* {@link com.github.copilot.tool.CopilotToolProcessor} output.
3940
*/
4041
@AllowCopilotExperimental
4142
class ToolDefinitionFromObjectTest {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
models:
2+
- claude-sonnet-4.5
3+
conversations:
4+
- messages:
5+
- role: system
6+
content: ${system}
7+
- role: user
8+
content: First, set the current phase to 'analyzing'. Then search for items with keyword 'copilot'. Report the phase and
9+
search results.
10+
- role: assistant
11+
content: I'll set the phase and run the search now.
12+
tool_calls:
13+
- id: toolcall_0
14+
type: function
15+
function:
16+
name: set_current_phase
17+
arguments: '{"phase":"analyzing"}'
18+
- id: toolcall_1
19+
type: function
20+
function:
21+
name: search_items
22+
arguments: '{"keyword":"copilot"}'
23+
- role: tool
24+
tool_call_id: toolcall_0
25+
content: Phase set to analyzing
26+
- role: tool
27+
tool_call_id: toolcall_1
28+
content: "Found: item_alpha, item_beta"
29+
- role: assistant
30+
content: |-
31+
Current phase: analyzing
32+
Search results: item_alpha, item_beta

0 commit comments

Comments
 (0)