Skip to content

Commit 299c3e4

Browse files
edburnsCopilot
andcommitted
Add new-java-e2e-test-yaml-and-test skill
Packages the knowledge of creating net-new Java E2E integration tests with handcrafted replay proxy YAML snapshots into a reusable Copilot skill. New files: - .github/skills/new-java-e2e-test-yaml-and-test/SKILL.md Main skill instructions covering: YAML snapshot format, proxy matching logic, Java IT test template, verification commands, key classes/files reference table, and common pitfalls. Includes the critical constraint that Java's CapiProxy always sets GITHUB_ACTIONS=true so snapshots must be handcrafted rather than recorded. - .github/skills/new-java-e2e-test-yaml-and-test/examples.md Two working examples from the codebase: a simple single-turn test (Botanica identity REPLACE) and a multi-turn test with tool calls (system message transform with view tool). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 38b0af9 commit 299c3e4

5 files changed

Lines changed: 585 additions & 0 deletions

File tree

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
---
2+
name: new-java-e2e-test-yaml-and-test
3+
description: "Use this skill when creating a new Java E2E integration test (failsafe IT) that requires a new replay proxy YAML snapshot file in test/snapshots/"
4+
---
5+
6+
# Creating a New Java E2E Test with a Replay Proxy YAML Snapshot
7+
8+
This skill covers the complete workflow for adding a new Java failsafe
9+
integration test backed by a handcrafted YAML snapshot for the replay proxy.
10+
11+
## Overview
12+
13+
The Java E2E tests use a **replay proxy** (`test/harness/replayingCapiProxy.ts`)
14+
that intercepts HTTP calls to the Copilot API and returns pre-recorded responses
15+
from YAML snapshot files. This avoids needing real authentication in CI.
16+
17+
**Key constraint:** Java's `CapiProxy.java` always sets `GITHUB_ACTIONS=true`
18+
(line 104), which forces the replay proxy into read-only mode. You **cannot**
19+
record snapshots by running Java tests — you must handcraft the YAML.
20+
21+
## Step-by-Step Workflow
22+
23+
### Step 1: Choose a snapshot category and test name
24+
25+
- Category = a directory under `test/snapshots/` (e.g., `system_message_sections`)
26+
- Test name = the Java method name, converted to snake_case (e.g.,
27+
`shouldUseReplacedIdentitySectionInResponse``should_use_replaced_identity_section_in_response`)
28+
- Resulting file: `test/snapshots/<category>/<snake_case_test_name>.yaml`
29+
30+
### Step 2: Create the YAML snapshot file
31+
32+
The format is:
33+
34+
```yaml
35+
models:
36+
- claude-sonnet-4.5
37+
conversations:
38+
- messages:
39+
- role: system
40+
content: ${system}
41+
- role: user
42+
content: <the exact prompt your test will send>
43+
- role: assistant
44+
content: <the response the proxy will return>
45+
```
46+
47+
**Rules:**
48+
- `${system}` is a placeholder that matches ANY system message content
49+
- `${workdir}` in tool arguments is substituted with the actual temp workDir
50+
- Each conversation entry represents one request-response exchange
51+
- For multi-turn, add multiple conversation entries
52+
- For tool calls, include `tool_calls` on assistant messages and `role: tool` for results
53+
- The user content must **exactly match** what your test sends (after normalization)
54+
55+
### Step 3: Create the Java IT test class
56+
57+
Place it in `java/src/test/java/com/github/copilot/` with an `IT` suffix
58+
(e.g., `MyFeatureIT.java`). The failsafe plugin picks up `*IT.java` files.
59+
60+
**Template:**
61+
62+
```java
63+
package com.github.copilot;
64+
65+
import static org.junit.jupiter.api.Assertions.*;
66+
67+
import java.util.concurrent.TimeUnit;
68+
69+
import org.junit.jupiter.api.AfterAll;
70+
import org.junit.jupiter.api.BeforeAll;
71+
import org.junit.jupiter.api.Test;
72+
73+
import com.github.copilot.generated.AssistantMessageEvent;
74+
import com.github.copilot.rpc.MessageOptions;
75+
import com.github.copilot.rpc.PermissionHandler;
76+
import com.github.copilot.rpc.SessionConfig;
77+
// ... other imports as needed
78+
79+
class MyFeatureIT {
80+
81+
private static E2ETestContext ctx;
82+
83+
@BeforeAll
84+
static void setUp() throws Exception {
85+
ctx = E2ETestContext.create();
86+
}
87+
88+
@AfterAll
89+
static void tearDown() throws Exception {
90+
if (ctx != null) {
91+
ctx.close();
92+
}
93+
}
94+
95+
@Test
96+
void myTestMethod() throws Exception {
97+
// 1. Configure the proxy to use your snapshot
98+
ctx.configureForTest("my_category", "my_test_method");
99+
100+
// 2. Create a client (uses fake token + proxy automatically)
101+
try (CopilotClient client = ctx.createClient()) {
102+
103+
// 3. Create a session with desired config
104+
CopilotSession session = client.createSession(new SessionConfig()
105+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL))
106+
.get(30, TimeUnit.SECONDS);
107+
108+
try {
109+
// 4. Send the prompt (must match YAML exactly)
110+
AssistantMessageEvent response = session
111+
.sendAndWait(new MessageOptions().setPrompt("Your prompt here"), 60_000)
112+
.get(90, TimeUnit.SECONDS);
113+
114+
// 5. Assert on the response
115+
assertNotNull(response);
116+
String content = response.getData().content();
117+
assertTrue(content.contains("expected text"));
118+
} finally {
119+
session.close();
120+
}
121+
}
122+
}
123+
}
124+
```
125+
126+
### Step 4: Verify
127+
128+
```sh
129+
cd java
130+
mvn spotless:apply
131+
mvn failsafe:integration-test -Dit.test="MyFeatureIT#myTestMethod" -Denforcer.skip=true
132+
```
133+
134+
Then run the full build to confirm no regressions:
135+
136+
```sh
137+
mvn clean verify
138+
```
139+
140+
## Key Classes and Files
141+
142+
| What | Where |
143+
|------|-------|
144+
| Test context (manages proxy, workDir, CLI) | `java/src/test/java/com/github/copilot/E2ETestContext.java` |
145+
| Java proxy wrapper | `java/src/test/java/com/github/copilot/CapiProxy.java` |
146+
| Replay proxy (TypeScript) | `test/harness/replayingCapiProxy.ts` |
147+
| Proxy server entry point | `test/harness/server.ts` |
148+
| Snapshot files | `test/snapshots/<category>/<name>.yaml` |
149+
| Existing IT tests for reference | `java/src/test/java/com/github/copilot/*IT.java` |
150+
151+
## How the Proxy Matches Requests
152+
153+
1. The proxy normalizes the incoming request's messages
154+
2. It compares against each conversation in the YAML:
155+
- System message matches if YAML has `${system}` (wildcard)
156+
- User messages are compared by content (exact text match)
157+
- Tool results are compared after normalizing `${workdir}` paths
158+
3. If a match is found, the **last assistant message** in that conversation is returned
159+
4. If no match, in CI mode (`GITHUB_ACTIONS=true`) it errors with "No cached response found"
160+
161+
## YAML Format for Tool Calls
162+
163+
If your test involves tool use:
164+
165+
```yaml
166+
conversations:
167+
# First exchange: model wants to call a tool
168+
- messages:
169+
- role: system
170+
content: ${system}
171+
- role: user
172+
content: Read the file test.txt
173+
- role: assistant
174+
content: I'll read that file.
175+
tool_calls:
176+
- id: toolcall_0
177+
type: function
178+
function:
179+
name: view
180+
arguments: '{"path":"${workdir}/test.txt"}'
181+
# Second exchange: after tool result is provided, model gives final answer
182+
- messages:
183+
- role: system
184+
content: ${system}
185+
- role: user
186+
content: Read the file test.txt
187+
- role: assistant
188+
content: I'll read that file.
189+
tool_calls:
190+
- id: toolcall_0
191+
type: function
192+
function:
193+
name: view
194+
arguments: '{"path":"${workdir}/test.txt"}'
195+
- role: tool
196+
tool_call_id: toolcall_0
197+
content: "1. Hello world!"
198+
- role: assistant
199+
content: The file test.txt contains "Hello world!"
200+
```
201+
202+
**Important:** When the model calls tools like `view`, the CLI actually executes
203+
them locally. The file must exist in the test's workDir. Create it in your test
204+
before sending the prompt:
205+
206+
```java
207+
Files.writeString(ctx.getWorkDir().resolve("test.txt"), "Hello world!\n");
208+
```
209+
210+
## Common Pitfalls
211+
212+
1. **Prompt mismatch** — The user content in YAML must exactly match what
213+
`session.sendAndWait(new MessageOptions().setPrompt("..."))` sends.
214+
2. **Forgetting `${system}`** — Always use `${system}` for the system role content
215+
unless testing a specific system message matching scenario.
216+
3. **Tool execution** — If the snapshot has the model calling `view` or other
217+
built-in tools, the CLI will actually execute those tools. Files must exist.
218+
4. **Snake_case conversion** — `configureForTest("category", "myMethodName")`
219+
converts `myMethodName` to `my_method_name` for the filename lookup.
220+
5. **Cannot record via Java** — `CapiProxy.java` forces `GITHUB_ACTIONS=true`.
221+
Always handcraft snapshots or use the Node.js proxy directly for recording.
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Examples: New Java E2E Test with YAML Snapshot
2+
3+
## Example 1: Simple single-turn conversation (no tool calls)
4+
5+
### Snapshot YAML
6+
7+
File: `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml`
8+
9+
```yaml
10+
models:
11+
- claude-sonnet-4.5
12+
conversations:
13+
- messages:
14+
- role: system
15+
content: ${system}
16+
- role: user
17+
content: Who are you?
18+
- role: assistant
19+
content: >-
20+
I'm Botanica, your helpful gardening assistant! I'm here to help you
21+
with all things related to plants and gardening. Whether you have
22+
questions about plant care, garden design, soil preparation, pest
23+
management, or anything else in the world of gardening, I'm happy to
24+
help. What would you like to know about plants or gardening today?
25+
```
26+
27+
### Corresponding Java test method
28+
29+
```java
30+
@Test
31+
void shouldUseReplacedIdentitySectionInResponse() throws Exception {
32+
ctx.configureForTest("system_message_sections", "should_use_replaced_identity_section_in_response");
33+
34+
var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE)
35+
.setSections(Map.of(SystemMessageSections.IDENTITY,
36+
new SectionOverride().setAction(SectionOverrideAction.REPLACE)
37+
.setContent("You are a helpful gardening assistant called Botanica. "
38+
+ "You only answer questions about plants and gardening.")));
39+
40+
try (CopilotClient client = ctx.createClient()) {
41+
CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage)
42+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS);
43+
44+
try {
45+
AssistantMessageEvent response = session
46+
.sendAndWait(new MessageOptions().setPrompt("Who are you?"), 60_000).get(90, TimeUnit.SECONDS);
47+
48+
assertNotNull(response, "Expected a response from the assistant");
49+
String content = response.getData().content().toLowerCase();
50+
assertTrue(content.contains("botanica") || content.contains("garden") || content.contains("plant"),
51+
"Expected response to reflect the replaced identity section, but got: "
52+
+ response.getData().content());
53+
} finally {
54+
session.close();
55+
}
56+
}
57+
}
58+
```
59+
60+
**Key points:**
61+
- `configureForTest("system_message_sections", "should_use_replaced_identity_section_in_response")`
62+
maps to `test/snapshots/system_message_sections/should_use_replaced_identity_section_in_response.yaml`
63+
- The prompt `"Who are you?"` exactly matches the YAML's user content
64+
- `ctx.createClient()` uses `fake-token-for-e2e-tests` — works in CI
65+
66+
---
67+
68+
## Example 2: Multi-turn with tool calls (from existing tests)
69+
70+
### Snapshot YAML
71+
72+
File: `test/snapshots/system_message_transform/should_invoke_transform_callbacks_with_section_content.yaml`
73+
74+
```yaml
75+
models:
76+
- claude-sonnet-4.5
77+
conversations:
78+
# First exchange: model decides to call tools
79+
- messages:
80+
- role: system
81+
content: ${system}
82+
- role: user
83+
content: Read the contents of test.txt and tell me what it says
84+
- role: assistant
85+
content: I'll read the test.txt file for you.
86+
tool_calls:
87+
- id: toolcall_0
88+
type: function
89+
function:
90+
name: report_intent
91+
arguments: '{"intent":"Reading test.txt file"}'
92+
- id: toolcall_1
93+
type: function
94+
function:
95+
name: view
96+
arguments: '{"path":"${workdir}/test.txt"}'
97+
# Second exchange: after tool results come back, model gives final answer
98+
- messages:
99+
- role: system
100+
content: ${system}
101+
- role: user
102+
content: Read the contents of test.txt and tell me what it says
103+
- role: assistant
104+
content: I'll read the test.txt file for you.
105+
tool_calls:
106+
- id: toolcall_0
107+
type: function
108+
function:
109+
name: report_intent
110+
arguments: '{"intent":"Reading test.txt file"}'
111+
- id: toolcall_1
112+
type: function
113+
function:
114+
name: view
115+
arguments: '{"path":"${workdir}/test.txt"}'
116+
- role: tool
117+
tool_call_id: toolcall_0
118+
content: Intent logged
119+
- role: tool
120+
tool_call_id: toolcall_1
121+
content: 1. Hello transform!
122+
- role: assistant
123+
content: |-
124+
The file test.txt contains:
125+
```
126+
Hello transform!
127+
```
128+
```
129+
130+
### Corresponding Java test method
131+
132+
```java
133+
@Test
134+
void transformOnIdentitySectionReceivesNonEmptyContent() throws Exception {
135+
ctx.configureForTest("system_message_transform", "should_invoke_transform_callbacks_with_section_content");
136+
137+
ConcurrentHashMap<String, String> capturedContent = new ConcurrentHashMap<>();
138+
139+
var systemMessage = new SystemMessageConfig().setMode(SystemMessageMode.CUSTOMIZE)
140+
.setSections(Map.of(SystemMessageSections.IDENTITY, new SectionOverride().setTransform(content -> {
141+
capturedContent.put("identity", content);
142+
return CompletableFuture.completedFuture(content);
143+
}), SystemMessageSections.TONE, new SectionOverride().setTransform(content -> {
144+
capturedContent.put("tone", content);
145+
return CompletableFuture.completedFuture(content);
146+
})));
147+
148+
try (CopilotClient client = ctx.createClient()) {
149+
// Create the file the snapshot expects the CLI view tool to read
150+
Path testFile = ctx.getWorkDir().resolve("test.txt");
151+
Files.writeString(testFile, "Hello transform!");
152+
153+
CopilotSession session = client.createSession(new SessionConfig().setSystemMessage(systemMessage)
154+
.setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get(30, TimeUnit.SECONDS);
155+
156+
try {
157+
AssistantMessageEvent response = session
158+
.sendAndWait(new MessageOptions()
159+
.setPrompt("Read the contents of test.txt and tell me what it says"), 60_000)
160+
.get(90, TimeUnit.SECONDS);
161+
162+
assertNotNull(response, "Expected a response from the assistant");
163+
164+
String identityContent = capturedContent.get("identity");
165+
assertNotNull(identityContent, "Expected identity transform callback to be invoked");
166+
assertTrue(!identityContent.isBlank(), "Expected identity section content to be non-empty");
167+
} finally {
168+
session.close();
169+
}
170+
}
171+
}
172+
```
173+
174+
**Key points:**
175+
- The file `test.txt` must be created in `ctx.getWorkDir()` **before** sending the prompt
176+
- The CLI's `view` tool will actually read that file; the YAML's tool result `"1. Hello transform!"` must match what `view` returns for that file content
177+
- Two conversation entries: first for the tool-call decision, second for the final response after tool results

0 commit comments

Comments
 (0)