Skip to content

Commit 60ac799

Browse files
stephentoubCopilot
andcommitted
Add cross-SDK E2E parity tests
Add missing E2E coverage so suspend RPC, event fidelity, built-in tool smoke tests, error resilience, and multi-turn scenarios are represented across all SDKs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 884d730 commit 60ac799

11 files changed

Lines changed: 1771 additions & 0 deletions
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package e2e
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"runtime"
7+
"strings"
8+
"testing"
9+
10+
copilot "github.com/github/copilot-sdk/go"
11+
"github.com/github/copilot-sdk/go/internal/e2e/testharness"
12+
)
13+
14+
func TestBuiltinToolsE2E(t *testing.T) {
15+
ctx := testharness.NewTestContext(t)
16+
client := ctx.NewClient()
17+
t.Cleanup(func() { client.ForceStop() })
18+
19+
t.Run("should capture exit code in output", func(t *testing.T) {
20+
ctx.ConfigureForTest(t)
21+
22+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
23+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
24+
})
25+
if err != nil {
26+
t.Fatalf("Failed to create session: %v", err)
27+
}
28+
t.Cleanup(func() { _ = session.Disconnect() })
29+
30+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
31+
Prompt: "Run 'echo hello && echo world'. Tell me the exact output.",
32+
})
33+
if err != nil {
34+
t.Fatalf("SendAndWait failed: %v", err)
35+
}
36+
37+
content := assistantContent(t, msg)
38+
if !strings.Contains(content, "hello") || !strings.Contains(content, "world") {
39+
t.Fatalf("Expected output to contain hello and world, got %q", content)
40+
}
41+
})
42+
43+
t.Run("should capture stderr output", func(t *testing.T) {
44+
if runtime.GOOS == "windows" {
45+
t.Skip("stderr prompt uses bash syntax")
46+
}
47+
48+
ctx.ConfigureForTest(t)
49+
50+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
51+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
52+
})
53+
if err != nil {
54+
t.Fatalf("Failed to create session: %v", err)
55+
}
56+
t.Cleanup(func() { _ = session.Disconnect() })
57+
58+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
59+
Prompt: "Run 'echo error_msg >&2; echo ok' and tell me what stderr said. Reply with just the stderr content.",
60+
})
61+
if err != nil {
62+
t.Fatalf("SendAndWait failed: %v", err)
63+
}
64+
65+
if content := assistantContent(t, msg); !strings.Contains(content, "error_msg") {
66+
t.Fatalf("Expected stderr response to contain error_msg, got %q", content)
67+
}
68+
})
69+
70+
t.Run("should read file with line range", func(t *testing.T) {
71+
ctx.ConfigureForTest(t)
72+
73+
if err := os.WriteFile(filepath.Join(ctx.WorkDir, "lines.txt"), []byte("line1\nline2\nline3\nline4\nline5\n"), 0644); err != nil {
74+
t.Fatalf("Failed to write lines.txt: %v", err)
75+
}
76+
77+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
78+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
79+
})
80+
if err != nil {
81+
t.Fatalf("Failed to create session: %v", err)
82+
}
83+
t.Cleanup(func() { _ = session.Disconnect() })
84+
85+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
86+
Prompt: "Read lines 2 through 4 of the file 'lines.txt' in this directory. Tell me what those lines contain.",
87+
})
88+
if err != nil {
89+
t.Fatalf("SendAndWait failed: %v", err)
90+
}
91+
92+
content := assistantContent(t, msg)
93+
if !strings.Contains(content, "line2") || !strings.Contains(content, "line4") {
94+
t.Fatalf("Expected response to contain line2 and line4, got %q", content)
95+
}
96+
})
97+
98+
t.Run("should handle nonexistent file gracefully", func(t *testing.T) {
99+
ctx.ConfigureForTest(t)
100+
101+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
102+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
103+
})
104+
if err != nil {
105+
t.Fatalf("Failed to create session: %v", err)
106+
}
107+
t.Cleanup(func() { _ = session.Disconnect() })
108+
109+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
110+
Prompt: "Try to read the file 'does_not_exist.txt'. If it doesn't exist, say 'FILE_NOT_FOUND'.",
111+
})
112+
if err != nil {
113+
t.Fatalf("SendAndWait failed: %v", err)
114+
}
115+
116+
content := strings.ToUpper(assistantContent(t, msg))
117+
if !strings.Contains(content, "NOT FOUND") &&
118+
!strings.Contains(content, "NOT EXIST") &&
119+
!strings.Contains(content, "NO SUCH") &&
120+
!strings.Contains(content, "FILE_NOT_FOUND") &&
121+
!strings.Contains(content, "DOES NOT EXIST") &&
122+
!strings.Contains(content, "ERROR") {
123+
t.Fatalf("Expected a not-found style response, got %q", content)
124+
}
125+
})
126+
127+
t.Run("should edit a file successfully", func(t *testing.T) {
128+
ctx.ConfigureForTest(t)
129+
130+
if err := os.WriteFile(filepath.Join(ctx.WorkDir, "edit_me.txt"), []byte("Hello World\nGoodbye World\n"), 0644); err != nil {
131+
t.Fatalf("Failed to write edit_me.txt: %v", err)
132+
}
133+
134+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
135+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
136+
})
137+
if err != nil {
138+
t.Fatalf("Failed to create session: %v", err)
139+
}
140+
t.Cleanup(func() { _ = session.Disconnect() })
141+
142+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
143+
Prompt: "Edit the file 'edit_me.txt': replace 'Hello World' with 'Hi Universe'. Then read it back and tell me its contents.",
144+
})
145+
if err != nil {
146+
t.Fatalf("SendAndWait failed: %v", err)
147+
}
148+
149+
if content := assistantContent(t, msg); !strings.Contains(content, "Hi Universe") {
150+
t.Fatalf("Expected response to contain Hi Universe, got %q", content)
151+
}
152+
})
153+
154+
t.Run("should create a new file", func(t *testing.T) {
155+
ctx.ConfigureForTest(t)
156+
157+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
158+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
159+
})
160+
if err != nil {
161+
t.Fatalf("Failed to create session: %v", err)
162+
}
163+
t.Cleanup(func() { _ = session.Disconnect() })
164+
165+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
166+
Prompt: "Create a file called 'new_file.txt' with the content 'Created by test'. Then read it back to confirm.",
167+
})
168+
if err != nil {
169+
t.Fatalf("SendAndWait failed: %v", err)
170+
}
171+
172+
if content := assistantContent(t, msg); !strings.Contains(content, "Created by test") {
173+
t.Fatalf("Expected response to contain Created by test, got %q", content)
174+
}
175+
})
176+
177+
t.Run("should search for patterns in files", func(t *testing.T) {
178+
ctx.ConfigureForTest(t)
179+
180+
if err := os.WriteFile(filepath.Join(ctx.WorkDir, "data.txt"), []byte("apple\nbanana\napricot\ncherry\n"), 0644); err != nil {
181+
t.Fatalf("Failed to write data.txt: %v", err)
182+
}
183+
184+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
185+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
186+
})
187+
if err != nil {
188+
t.Fatalf("Failed to create session: %v", err)
189+
}
190+
t.Cleanup(func() { _ = session.Disconnect() })
191+
192+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
193+
Prompt: "Search for lines starting with 'ap' in the file 'data.txt'. Tell me which lines matched.",
194+
})
195+
if err != nil {
196+
t.Fatalf("SendAndWait failed: %v", err)
197+
}
198+
199+
content := assistantContent(t, msg)
200+
if !strings.Contains(content, "apple") || !strings.Contains(content, "apricot") {
201+
t.Fatalf("Expected response to contain apple and apricot, got %q", content)
202+
}
203+
})
204+
205+
t.Run("should find files by pattern", func(t *testing.T) {
206+
ctx.ConfigureForTest(t)
207+
208+
if err := os.MkdirAll(filepath.Join(ctx.WorkDir, "src"), 0755); err != nil {
209+
t.Fatalf("Failed to create src directory: %v", err)
210+
}
211+
if err := os.WriteFile(filepath.Join(ctx.WorkDir, "src", "index.ts"), []byte("export const index = 1;"), 0644); err != nil {
212+
t.Fatalf("Failed to write index.ts: %v", err)
213+
}
214+
if err := os.WriteFile(filepath.Join(ctx.WorkDir, "README.md"), []byte("# Readme"), 0644); err != nil {
215+
t.Fatalf("Failed to write README.md: %v", err)
216+
}
217+
218+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
219+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
220+
})
221+
if err != nil {
222+
t.Fatalf("Failed to create session: %v", err)
223+
}
224+
t.Cleanup(func() { _ = session.Disconnect() })
225+
226+
msg, err := session.SendAndWait(t.Context(), copilot.MessageOptions{
227+
Prompt: "Find all .ts files in this directory (recursively). List the filenames you found.",
228+
})
229+
if err != nil {
230+
t.Fatalf("SendAndWait failed: %v", err)
231+
}
232+
233+
if content := assistantContent(t, msg); !strings.Contains(content, "index.ts") {
234+
t.Fatalf("Expected response to contain index.ts, got %q", content)
235+
}
236+
})
237+
}
238+
239+
func assistantContent(t *testing.T, event *copilot.SessionEvent) string {
240+
t.Helper()
241+
242+
if event == nil {
243+
t.Fatal("Expected assistant message, got nil")
244+
}
245+
data, ok := event.Data.(*copilot.AssistantMessageData)
246+
if !ok {
247+
t.Fatalf("Expected AssistantMessageData, got %T", event.Data)
248+
}
249+
return data.Content
250+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
package e2e
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
copilot "github.com/github/copilot-sdk/go"
9+
"github.com/github/copilot-sdk/go/internal/e2e/testharness"
10+
)
11+
12+
func TestErrorResilienceE2E(t *testing.T) {
13+
ctx := testharness.NewTestContext(t)
14+
client := ctx.NewClient()
15+
t.Cleanup(func() { client.ForceStop() })
16+
17+
t.Run("should throw when sending to disconnected session", func(t *testing.T) {
18+
ctx.ConfigureForTest(t)
19+
20+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
21+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
22+
})
23+
if err != nil {
24+
t.Fatalf("Failed to create session: %v", err)
25+
}
26+
if err := session.Disconnect(); err != nil {
27+
t.Fatalf("Disconnect failed: %v", err)
28+
}
29+
30+
timeoutCtx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
31+
defer cancel()
32+
if _, err := session.SendAndWait(timeoutCtx, copilot.MessageOptions{Prompt: "Hello"}); err == nil {
33+
t.Fatal("Expected SendAndWait on disconnected session to fail")
34+
}
35+
})
36+
37+
t.Run("should throw when getting messages from disconnected session", func(t *testing.T) {
38+
ctx.ConfigureForTest(t)
39+
40+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
41+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
42+
})
43+
if err != nil {
44+
t.Fatalf("Failed to create session: %v", err)
45+
}
46+
if err := session.Disconnect(); err != nil {
47+
t.Fatalf("Disconnect failed: %v", err)
48+
}
49+
50+
timeoutCtx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
51+
defer cancel()
52+
if _, err := session.GetMessages(timeoutCtx); err == nil {
53+
t.Fatal("Expected GetMessages on disconnected session to fail")
54+
}
55+
})
56+
57+
t.Run("should handle double abort without error", func(t *testing.T) {
58+
ctx.ConfigureForTest(t)
59+
60+
session, err := client.CreateSession(t.Context(), &copilot.SessionConfig{
61+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
62+
})
63+
if err != nil {
64+
t.Fatalf("Failed to create session: %v", err)
65+
}
66+
67+
if err := session.Abort(t.Context()); err != nil {
68+
t.Fatalf("First abort failed: %v", err)
69+
}
70+
if err := session.Abort(t.Context()); err != nil {
71+
t.Fatalf("Second abort failed: %v", err)
72+
}
73+
if err := session.Disconnect(); err != nil {
74+
t.Fatalf("Disconnect failed: %v", err)
75+
}
76+
})
77+
78+
t.Run("should throw when resuming non-existent session", func(t *testing.T) {
79+
ctx.ConfigureForTest(t)
80+
81+
timeoutCtx, cancel := context.WithTimeout(t.Context(), 10*time.Second)
82+
defer cancel()
83+
if _, err := client.ResumeSession(timeoutCtx, "non-existent-session-id-12345", &copilot.ResumeSessionConfig{
84+
OnPermissionRequest: copilot.PermissionHandler.ApproveAll,
85+
}); err == nil {
86+
t.Fatal("Expected ResumeSession for non-existent session to fail")
87+
}
88+
})
89+
}

0 commit comments

Comments
 (0)