feat: AutoZero 通用 Agent 框架(从 AutoGo 五阶段拆分完成)

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。
This commit is contained in:
Sakurasan committed 2026-10-26 00:54:46 +08:00
commit d5dac5a6fc
140 files changed
+26497

No files matched your search

+327
View File
@@ -0,0 +1,327 @@
package mcp
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
)
// 本文件用 httptest 起真的 HTTP MCP server,端到端验证 Streamable HTTP 传输:
// modern(2026-07-28 无会话)/ SSE 响应 / legacy 回退(initialize + Mcp-Session-Id)/ 401 鉴权。
// stdio 的用例在 mcp_test.go,不受本文件影响。
// httpRecorder 记录服务端看到的请求(httptest 的 handler 在另一个 goroutine,需要加锁)。
type httpRecorder struct {
mu sync.Mutex
reqs []recordedReq
sessionSeen bool
}
type recordedReq struct {
method string
header http.Header
}
func (r *httpRecorder) add(method string, h http.Header) {
r.mu.Lock()
defer r.mu.Unlock()
r.reqs = append(r.reqs, recordedReq{method: method, header: h.Clone()})
if h.Get("Mcp-Session-Id") != "" {
r.sessionSeen = true
}
}
// every 检查是否所有请求都满足 fn。
func (r *httpRecorder) every(fn func(recordedReq) bool) bool {
r.mu.Lock()
defer r.mu.Unlock()
if len(r.reqs) == 0 {
return false
}
for _, q := range r.reqs {
if !fn(q) {
return false
}
}
return true
}
func (r *httpRecorder) count() int {
r.mu.Lock()
defer r.mu.Unlock()
return len(r.reqs)
}
func (r *httpRecorder) sawSession() bool {
r.mu.Lock()
defer r.mu.Unlock()
return r.sessionSeen
}
// hasMethodName 检查某次请求是否带了正确的 Mcp-Name 头。
func (r *httpRecorder) hasMethodName(method, name string) bool {
r.mu.Lock()
defer r.mu.Unlock()
for _, q := range r.reqs {
if q.method == method {
return q.header.Get("Mcp-Name") == name
}
}
return false
}
func mustJSON(v any) string {
b, _ := json.Marshal(v)
return string(b)
}
// modernServer 起一个现代(无会话)MCP HTTP server。mode 决定响应形式:json 或 sse。
func modernServer(rec *httpRecorder, mode string) *httptest.Server {
write := func(w http.ResponseWriter, resp map[string]any) {
if mode == "sse" {
w.Header().Set("Content-Type", "text/event-stream")
// 先来 keep-alive 注释与一条与本次请求相关的通知,客户端应跳过它们,只取最后的响应。
fmt.Fprintf(w, ": keep-alive\r\n\r\n")
fmt.Fprintf(w, "data: %s\r\n\r\n", mustJSON(map[string]any{
"jsonrpc": "2.0",
"method": "notifications/progress",
"params": map[string]any{"progress": 10},
}))
fmt.Fprintf(w, "data: %s\r\n\r\n", mustJSON(resp))
if f, ok := w.(http.Flusher); ok {
f.Flush()
}
return
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(resp)
}
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID json.RawMessage `json:"id"`
Method string `json:"method"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
rec.add(req.Method, r.Header)
resp := map[string]any{"jsonrpc": "2.0", "id": req.ID}
switch req.Method {
case "server/discover":
resp["result"] = map[string]any{
"resultType": "complete",
"supportedVersions": []string{"2026-07-28"},
"capabilities": map[string]any{"tools": map[string]any{}},
"_meta": map[string]any{metaServerInfo: map[string]any{"name": "fake-http", "version": "3.0.0"}},
}
case "tools/list":
resp["result"] = map[string]any{
"resultType": "complete",
"tools": []map[string]any{{
"name": "echo",
"description": "Echo",
"inputSchema": map[string]any{"type": "object"},
}},
}
case "tools/call":
resp["result"] = map[string]any{
"resultType": "complete",
"content": []map[string]any{{"type": "text", "text": "http echo"}},
"isError": false,
}
default:
resp["error"] = map[string]any{"code": -32601, "message": "no such method"}
}
write(w, resp)
}))
}
// TestHTTPModernProtocol:现代 Streamable HTTP。
// 核心断言:每个请求都带 MCP-Protocol-Version / 与 body 一致的 Mcp-Method,
// tools/call 带 Mcp-Name=echo,Accept 同时接受两种 content type,且不用 Mcp-Session-Id。
func TestHTTPModernProtocol(t *testing.T) {
rec := &httpRecorder{}
srv := modernServer(rec, "json")
defer srv.Close()
cli, err := Start("httptest", Config{URL: srv.URL})
if err != nil {
t.Fatalf("start: %v", err)
}
defer cli.Close()
if cli.Era() != eraModern {
t.Fatalf("era = %q, want modern", cli.Era())
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
got, err := cli.CallTool(ctx, "echo", `{"text":"x"}`)
if err != nil {
t.Fatalf("call: %v", err)
}
if got != "http echo" {
t.Fatalf("call result: %q", got)
}
if !rec.every(func(q recordedReq) bool {
return q.header.Get("MCP-Protocol-Version") == protocolVersion
}) {
t.Errorf("每个请求都必须带 MCP-Protocol-Version %s", protocolVersion)
}
if !rec.every(func(q recordedReq) bool {
return q.header.Get("Mcp-Method") == q.method
}) {
t.Errorf("每个请求都必须带与 body 一致的 Mcp-Method 头(%d 个请求)", rec.count())
}
if !rec.every(func(q recordedReq) bool {
a := q.header.Get("Accept")
return strings.Contains(a, "application/json") && strings.Contains(a, "text/event-stream")
}) {
t.Error("Accept 必须同时列 application/json 与 text/event-stream")
}
if !rec.hasMethodName("tools/call", "echo") {
t.Error("tools/call 必须带 Mcp-Name 头(值为 params.name)")
}
if rec.sawSession() {
t.Error("现代传输不应使用 Mcp-Session-Id")
}
}
// TestHTTPSSEResponse:服务端用 text/event-stream 回:流上先来注释与通知,最后才是响应。
func TestHTTPSSEResponse(t *testing.T) {
rec := &httpRecorder{}
srv := modernServer(rec, "sse")
defer srv.Close()
cli, err := Start("httptest", Config{URL: srv.URL})
if err != nil {
t.Fatalf("start: %v", err)
}
defer cli.Close()
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
got, err := cli.CallTool(ctx, "echo", `{"text":"x"}`)
if err != nil {
t.Fatalf("call: %v", err)
}
if got != "http echo" {
t.Fatalf("call result: %q", got)
}
}
// TestHTTPLegacyFallback:服务端不认识 server/discover(404 + 非现代 body)→ 回退
// initialize,之后每个请求都要带上服务端下发的 Mcp-Session-Id。
func TestHTTPLegacyFallback(t *testing.T) {
rec := &httpRecorder{}
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var req struct {
ID json.RawMessage `json:"id"`
Method string `json:"method"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
rec.add(req.Method, r.Header)
if req.Method == "server/discover" {
// 旧服务端:404 且 body 不是 JSON-RPC 错误 → 客户端应判为 legacy。
w.WriteHeader(http.StatusNotFound)
_, _ = w.Write([]byte("not found"))
return
}
w.Header().Set("Content-Type", "application/json")
resp := map[string]any{"jsonrpc": "2.0", "id": req.ID}
switch req.Method {
case "initialize":
// 旧版语义:initialize 响应里下发会话 id。
w.Header().Set("Mcp-Session-Id", "sess-abc")
resp["result"] = map[string]any{
"protocolVersion": legacyVersion,
"capabilities": map[string]any{"tools": map[string]any{}},
"serverInfo": map[string]any{"name": "fake-http-legacy", "version": "1.2.3"},
}
case "tools/list":
resp["result"] = map[string]any{
"tools": []map[string]any{{"name": "echo", "inputSchema": map[string]any{"type": "object"}}},
}
case "tools/call":
resp["result"] = map[string]any{
"content": []map[string]any{{"type": "text", "text": "legacy http echo"}},
}
default:
resp["error"] = map[string]any{"code": -32601, "message": "no such method"}
}
_ = json.NewEncoder(w).Encode(resp)
}))
defer srv.Close()
cli, err := Start("httptest", Config{URL: srv.URL})
if err != nil {
t.Fatalf("start: %v", err)
}
defer cli.Close()
if cli.Era() != eraLegacy {
t.Fatalf("era = %q, want legacy(discover 404 应回退握手)", cli.Era())
}
if cli.Version() != legacyVersion {
t.Errorf("version = %q, want %q", cli.Version(), legacyVersion)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
got, err := cli.CallTool(ctx, "echo", `{"text":"x"}`)
if err != nil {
t.Fatalf("call: %v", err)
}
if got != "legacy http echo" {
t.Fatalf("call result: %q", got)
}
if !rec.sawSession() {
t.Error("legacy 传输的后续请求必须带上 Mcp-Session-Id")
}
}
// TestHTTPAuthRequired:远程 MCP 要求鉴权时给出可操作的错误(用户的 westock-mcp 正是这种情况)。
func TestHTTPAuthRequired(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("WWW-Authenticate", "Bearer resource_metadata=\"https://example.com/.well-known/oauth-protected-resource\"")
w.WriteHeader(http.StatusUnauthorized)
_, _ = w.Write([]byte("{\"error\":\"unauthorized\"}"))
}))
defer srv.Close()
_, err := Start("httptest", Config{URL: srv.URL})
if err == nil {
t.Fatal("401 应报错")
}
if !strings.Contains(err.Error(), "鉴权") {
t.Errorf("错误信息应说明是鉴权问题:%v", err)
}
}
// TestHTTPHeadersPassedThrough:配置里的 headers(例如 Authorization)要真的发出去。
func TestHTTPHeadersPassedThrough(t *testing.T) {
rec := &httpRecorder{}
srv := modernServer(rec, "json")
defer srv.Close()
cli, err := Start("httptest", Config{URL: srv.URL, Headers: map[string]string{"Authorization": "Bearer tok-123"}})
if err != nil {
t.Fatalf("start: %v", err)
}
defer cli.Close()
// 配置里的 headers 要随每个请求发出(远程 MCP 常用 Authorization 传 token)。
if !rec.every(func(q recordedReq) bool {
return q.header.Get("Authorization") == "Bearer tok-123"
}) {
t.Errorf("配置的 headers 必须随每个请求发出(%d 个请求)", rec.count())
}
}