// 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 }