feat(fake-acp-agent): integration test agent (HANG/CRASH/FS_READ/FS_WRITE modes)

This commit is contained in:
2026-05-07 13:48:30 +08:00
parent 85a8bc0df7
commit b4694e113b
+159
View File
@@ -0,0 +1,159 @@
// cmd/fake-acp-agent 是集成测试用的 fake ACP agent,stdin/stdout 走 JSON-RPC。
//
// 行为通过 env 控制:
//
// FAKE_ACP_HANG=true 永不响应任何请求(测 handshake 超时)
// FAKE_ACP_CRASH_AFTER_PROMPTS=N 处理完 N 个 prompt 后 os.Exit(1)
// FAKE_ACP_FS_READ=path prompt 时发起 fs/read_text_file 请求该路径
// FAKE_ACP_FS_WRITE=path:content prompt 时发起 fs/write_text_file
// FAKE_ACP_PROMPT_UPDATES=N 每 prompt 发 N 个 update(默认 3)
package main
import (
"bufio"
"encoding/json"
"fmt"
"os"
"strconv"
"strings"
"sync/atomic"
"time"
)
type Message struct {
JSONRPC string `json:"jsonrpc"`
ID json.RawMessage `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Params json.RawMessage `json:"params,omitempty"`
Result json.RawMessage `json:"result,omitempty"`
Error *RPCError `json:"error,omitempty"`
}
type RPCError struct {
Code int `json:"code"`
Message string `json:"message"`
}
var (
promptsHandled atomic.Int32
nextServerID atomic.Int64
)
func main() {
if os.Getenv("FAKE_ACP_HANG") == "true" {
select {} // 永不退出
}
in := bufio.NewReader(os.Stdin)
out := bufio.NewWriter(os.Stdout)
defer out.Flush()
for {
line, err := in.ReadBytes('\n')
if err != nil {
return
}
var msg Message
if err := json.Unmarshal(line, &msg); err != nil {
continue
}
switch msg.Method {
case "initialize":
respond(out, msg.ID, map[string]any{
"protocolVersion": 1,
"agentInfo": map[string]any{"name": "fake", "version": "0.0.0"},
})
case "session/new":
respond(out, msg.ID, map[string]any{"sessionId": "fake-session-id"})
case "session/prompt":
handlePrompt(out, msg.ID)
case "session/cancel":
// ignore
}
}
}
func handlePrompt(out *bufio.Writer, id json.RawMessage) {
updates := 3
if v := os.Getenv("FAKE_ACP_PROMPT_UPDATES"); v != "" {
if n, _ := strconv.Atoi(v); n > 0 {
updates = n
}
}
for i := 0; i < updates; i++ {
notify(out, "session/update", map[string]any{
"sessionId": "fake-session-id",
"update": map[string]any{
"sessionUpdate": "agent_message_chunk",
"content": []map[string]any{{"type": "text", "text": fmt.Sprintf("chunk %d", i)}},
},
})
time.Sleep(10 * time.Millisecond)
}
if path := os.Getenv("FAKE_ACP_FS_READ"); path != "" {
_ = doFsRead(out, path)
}
if spec := os.Getenv("FAKE_ACP_FS_WRITE"); spec != "" {
parts := strings.SplitN(spec, ":", 2)
if len(parts) == 2 {
_ = doFsWrite(out, parts[0], parts[1])
}
}
respond(out, id, map[string]any{"stopReason": "end_turn"})
if maxStr := os.Getenv("FAKE_ACP_CRASH_AFTER_PROMPTS"); maxStr != "" {
n, _ := strconv.Atoi(maxStr)
count := promptsHandled.Add(1)
if int(count) >= n {
os.Exit(1)
}
}
}
func doFsRead(out *bufio.Writer, path string) error {
id := nextServerID.Add(1)
idJSON, _ := json.Marshal(id)
body := mustJSON(Message{
JSONRPC: "2.0", ID: idJSON, Method: "fs/read_text_file",
Params: mustJSON(map[string]any{"sessionId": "fake-session-id", "path": path}),
})
_, _ = out.Write(body)
_, _ = out.Write([]byte{'\n'})
_ = out.Flush()
return nil
}
func doFsWrite(out *bufio.Writer, path, content string) error {
id := nextServerID.Add(1)
idJSON, _ := json.Marshal(id)
body := mustJSON(Message{
JSONRPC: "2.0", ID: idJSON, Method: "fs/write_text_file",
Params: mustJSON(map[string]any{"sessionId": "fake-session-id", "path": path, "content": content}),
})
_, _ = out.Write(body)
_, _ = out.Write([]byte{'\n'})
_ = out.Flush()
return nil
}
func respond(out *bufio.Writer, id json.RawMessage, result any) {
body := mustJSON(Message{JSONRPC: "2.0", ID: id, Result: mustJSON(result)})
_, _ = out.Write(body)
_, _ = out.Write([]byte{'\n'})
_ = out.Flush()
}
func notify(out *bufio.Writer, method string, params any) {
body := mustJSON(Message{JSONRPC: "2.0", Method: method, Params: mustJSON(params)})
_, _ = out.Write(body)
_, _ = out.Write([]byte{'\n'})
_ = out.Flush()
}
func mustJSON(v any) json.RawMessage {
b, _ := json.Marshal(v)
return b
}