llm(三协议 Provider)/ session(JSONL 持久化)/ tools(Registry、围栏、 builtin、computer)/ cdp(browser)/ skills / mcp / agent(循环、两级压缩、 编排、审批门控)+ 底座 jsonl/textx/execx/diag/scratch/spill/imagecache。 产品装配代码(config 桥接、schedule 工具、memory/plan/retro/prompt) 留在 AutoGo,宿主接入方式见 MIGRATION.md。
114 lines
3.5 KiB
Go
114 lines
3.5 KiB
Go
package cdp
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/coder/websocket"
|
|
)
|
|
|
|
// tinyPNG 是 1x1 红色 PNG 的 base64(截图测试用)。
|
|
const tinyPNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
|
|
|
// fakeCDPHandler 处理一条 CDP 命令并回响应;navigate 先发事件再回响应,
|
|
// 用于验证"先订阅后触发"的事件时序。
|
|
func fakeCDPHandler(t *testing.T, title string) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
c, err := websocket.Accept(w, r, nil)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close(websocket.StatusInternalError, "")
|
|
ctx := r.Context()
|
|
for {
|
|
_, data, err := c.Read(ctx)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var req struct {
|
|
ID int64 `json:"id"`
|
|
Method string `json:"method"`
|
|
Params json.RawMessage `json:"params"`
|
|
}
|
|
if err := json.Unmarshal(data, &req); err != nil {
|
|
continue
|
|
}
|
|
reply := func(v map[string]any) {
|
|
b, _ := json.Marshal(map[string]any{"id": req.ID, "result": v})
|
|
_ = c.Write(ctx, websocket.MessageText, b)
|
|
}
|
|
switch req.Method {
|
|
case "Page.enable":
|
|
reply(map[string]any{})
|
|
case "Page.navigate":
|
|
// 事件先于响应到达:验证 Subscribe 必须先行
|
|
ev, _ := json.Marshal(map[string]any{"method": "Page.loadEventFired", "params": map[string]any{}})
|
|
_ = c.Write(ctx, websocket.MessageText, ev)
|
|
reply(map[string]any{"frameId": "f1"})
|
|
case "Runtime.evaluate":
|
|
reply(map[string]any{"result": map[string]any{"type": "string", "value": title}})
|
|
case "Page.captureScreenshot":
|
|
reply(map[string]any{"data": tinyPNG})
|
|
default:
|
|
b, _ := json.Marshal(map[string]any{"id": req.ID, "error": map[string]any{"code": -32601, "message": "unknown method"}})
|
|
_ = c.Write(ctx, websocket.MessageText, b)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestConnCallAndEventTiming:事件先到时 Call 仍能配对响应、Subscribe 先行能等到事件。
|
|
func TestConnCallAndEventTiming(t *testing.T) {
|
|
srv := httptest.NewServer(fakeCDPHandler(t, "Fake Page"))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/devtools/page/t1"
|
|
|
|
ctx := context.Background()
|
|
conn, err := Dial(ctx, wsURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer conn.Close()
|
|
|
|
loaded := conn.Subscribe("Page.loadEventFired")
|
|
if _, err := conn.Call(ctx, "Page.enable", nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := conn.Call(ctx, "Page.navigate", map[string]any{"url": "https://example.com"}); err != nil {
|
|
t.Fatalf("navigate: %v", err)
|
|
}
|
|
// 事件在响应前已发出:订阅先行则必然能等到
|
|
if _, err := loaded.Wait(ctx, 2*time.Second); err != nil {
|
|
t.Fatalf("event wait: %v", err)
|
|
}
|
|
title, err := evaluateString(ctx, conn, "document.title")
|
|
if err != nil || title != "Fake Page" {
|
|
t.Fatalf("evaluate: %q %v", title, err)
|
|
}
|
|
// 服务端错误响应 → Go error
|
|
if _, err := conn.Call(ctx, "NoSuch.Method", nil); err == nil || !strings.Contains(err.Error(), "unknown method") {
|
|
t.Fatalf("expected rpc error, got %v", err)
|
|
}
|
|
}
|
|
|
|
// TestConnClosed:连接断开后 Call 立即失败。
|
|
func TestConnClosed(t *testing.T) {
|
|
srv := httptest.NewServer(fakeCDPHandler(t, "x"))
|
|
defer srv.Close()
|
|
wsURL := "ws" + strings.TrimPrefix(srv.URL, "http") + "/devtools/page/t1"
|
|
ctx := context.Background()
|
|
conn, err := Dial(ctx, wsURL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
conn.Close()
|
|
if _, err := conn.Call(ctx, "Page.enable", nil); err == nil {
|
|
t.Fatal("call on closed connection should fail")
|
|
}
|
|
}
|