Skip to content

Commit 7ee60ac

Browse files
committed
feat: add e2e test for ACP
1 parent 5d6b259 commit 7ee60ac

File tree

3 files changed

+179
-0
lines changed

3 files changed

+179
-0
lines changed

e2e/acp_echo.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
//go:build ignore
2+
3+
package main
4+
5+
import (
6+
"context"
7+
"encoding/json"
8+
"fmt"
9+
"os"
10+
"os/signal"
11+
"strings"
12+
13+
acp "github.com/coder/acp-go-sdk"
14+
)
15+
16+
// ScriptEntry defines a single entry in the test script.
17+
type ScriptEntry struct {
18+
ExpectMessage string `json:"expectMessage"`
19+
ThinkDurationMS int64 `json:"thinkDurationMS"`
20+
ResponseMessage string `json:"responseMessage"`
21+
}
22+
23+
// acpEchoAgent implements the ACP Agent interface for testing.
24+
type acpEchoAgent struct {
25+
script []ScriptEntry
26+
scriptIndex int
27+
conn *acp.AgentSideConnection
28+
sessionID acp.SessionId
29+
}
30+
31+
var _ acp.Agent = (*acpEchoAgent)(nil)
32+
33+
func main() {
34+
if len(os.Args) != 2 {
35+
fmt.Fprintln(os.Stderr, "Usage: acp_echo <script.json>")
36+
os.Exit(1)
37+
}
38+
39+
script, err := loadScript(os.Args[1])
40+
if err != nil {
41+
fmt.Fprintf(os.Stderr, "Error loading script: %v\n", err)
42+
os.Exit(1)
43+
}
44+
45+
if len(script) == 0 {
46+
fmt.Fprintln(os.Stderr, "Script is empty")
47+
os.Exit(1)
48+
}
49+
50+
sigCh := make(chan os.Signal, 1)
51+
signal.Notify(sigCh, os.Interrupt)
52+
go func() {
53+
<-sigCh
54+
os.Exit(0)
55+
}()
56+
57+
agent := &acpEchoAgent{
58+
script: script,
59+
}
60+
61+
conn := acp.NewAgentSideConnection(agent, os.Stdout, os.Stdin)
62+
agent.conn = conn
63+
64+
<-conn.Done()
65+
}
66+
67+
func (a *acpEchoAgent) Initialize(_ context.Context, _ acp.InitializeRequest) (acp.InitializeResponse, error) {
68+
return acp.InitializeResponse{
69+
ProtocolVersion: acp.ProtocolVersionNumber,
70+
AgentCapabilities: acp.AgentCapabilities{},
71+
}, nil
72+
}
73+
74+
func (a *acpEchoAgent) Authenticate(_ context.Context, _ acp.AuthenticateRequest) (acp.AuthenticateResponse, error) {
75+
return acp.AuthenticateResponse{}, nil
76+
}
77+
78+
func (a *acpEchoAgent) Cancel(_ context.Context, _ acp.CancelNotification) error {
79+
return nil
80+
}
81+
82+
func (a *acpEchoAgent) NewSession(_ context.Context, _ acp.NewSessionRequest) (acp.NewSessionResponse, error) {
83+
a.sessionID = "test-session"
84+
return acp.NewSessionResponse{
85+
SessionId: a.sessionID,
86+
}, nil
87+
}
88+
89+
func (a *acpEchoAgent) Prompt(ctx context.Context, params acp.PromptRequest) (acp.PromptResponse, error) {
90+
// Extract text from prompt
91+
var promptText string
92+
for _, block := range params.Prompt {
93+
if block.Text != nil {
94+
promptText = block.Text.Text
95+
break
96+
}
97+
}
98+
promptText = strings.TrimSpace(promptText)
99+
100+
if a.scriptIndex >= len(a.script) {
101+
return acp.PromptResponse{
102+
StopReason: acp.StopReasonEndTurn,
103+
}, nil
104+
}
105+
106+
entry := a.script[a.scriptIndex]
107+
expected := strings.TrimSpace(entry.ExpectMessage)
108+
109+
// Empty ExpectMessage matches any prompt
110+
if expected != "" && expected != promptText {
111+
return acp.PromptResponse{}, fmt.Errorf("expected message %q but got %q", expected, promptText)
112+
}
113+
114+
a.scriptIndex++
115+
116+
// Send response via session update
117+
if err := a.conn.SessionUpdate(ctx, acp.SessionNotification{
118+
SessionId: params.SessionId,
119+
Update: acp.UpdateAgentMessageText(entry.ResponseMessage),
120+
}); err != nil {
121+
return acp.PromptResponse{}, err
122+
}
123+
124+
return acp.PromptResponse{
125+
StopReason: acp.StopReasonEndTurn,
126+
}, nil
127+
}
128+
129+
func (a *acpEchoAgent) SetSessionMode(_ context.Context, _ acp.SetSessionModeRequest) (acp.SetSessionModeResponse, error) {
130+
return acp.SetSessionModeResponse{}, nil
131+
}
132+
133+
func loadScript(scriptPath string) ([]ScriptEntry, error) {
134+
data, err := os.ReadFile(scriptPath)
135+
if err != nil {
136+
return nil, fmt.Errorf("failed to read script file: %w", err)
137+
}
138+
139+
var script []ScriptEntry
140+
if err := json.Unmarshal(data, &script); err != nil {
141+
return nil, fmt.Errorf("failed to parse script JSON: %w", err)
142+
}
143+
144+
return script, nil
145+
}

e2e/echo_test.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,34 @@ func TestE2E(t *testing.T) {
100100
require.Equal(t, script[0].ExpectMessage, strings.TrimSpace(msgResp.Messages[1].Content))
101101
require.Equal(t, script[0].ResponseMessage, strings.TrimSpace(msgResp.Messages[2].Content))
102102
})
103+
104+
t.Run("acp_basic", func(t *testing.T) {
105+
ctx, cancel := context.WithTimeout(context.Background(), testTimeout)
106+
defer cancel()
107+
108+
script, apiClient := setup(ctx, t, &params{
109+
cmdFn: func(ctx context.Context, t testing.TB, serverPort int, binaryPath, cwd, scriptFilePath string) (string, []string) {
110+
return binaryPath, []string{
111+
"server",
112+
fmt.Sprintf("--port=%d", serverPort),
113+
"--experimental-acp",
114+
"--", "go", "run", filepath.Join(cwd, "acp_echo.go"), scriptFilePath,
115+
}
116+
},
117+
})
118+
messageReq := agentapisdk.PostMessageParams{
119+
Content: "This is a test message.",
120+
Type: agentapisdk.MessageTypeUser,
121+
}
122+
_, err := apiClient.PostMessage(ctx, messageReq)
123+
require.NoError(t, err, "Failed to send message via SDK")
124+
require.NoError(t, waitAgentAPIStable(ctx, t, apiClient, operationTimeout, "post message"))
125+
msgResp, err := apiClient.GetMessages(ctx)
126+
require.NoError(t, err, "Failed to get messages via SDK")
127+
require.Len(t, msgResp.Messages, 2)
128+
require.Equal(t, script[0].ExpectMessage, strings.TrimSpace(msgResp.Messages[0].Content))
129+
require.Equal(t, script[0].ResponseMessage, strings.TrimSpace(msgResp.Messages[1].Content))
130+
})
103131
}
104132

105133
type params struct {

e2e/testdata/acp_basic.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
[
2+
{
3+
"expectMessage": "This is a test message.",
4+
"responseMessage": "Echo: This is a test message."
5+
}
6+
]

0 commit comments

Comments
 (0)