You've already forked agentic-coding-workflow
feat(fake-acp-agent): integration test agent (HANG/CRASH/FS_READ/FS_WRITE modes)
This commit is contained in:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user