mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-12 20:27:30 +00:00
三个模块新增 test/ 黑盒契约,测试仅通过导出 API 驱动。
## internal/plugins/mcp
TestListToolsThroughHTTP
TestCallToolSuccessAndEmptyContentHandling
TestCallToolSurfacesIsErrorAsFailure
TestCallToolSurfacesRPCError
TestCallToolSurfacesHTTPError
TestListToolsParseSSEStreamFrame
使用 httptest + NewSSETransport + NewServer,覆盖 JSON-RPC 工具列表、
成功调用、空内容失败、MCP isError、RPC 错误、HTTP 错误和 SSE data 帧。
## internal/plugin/proc
TestMajorVersionParsing
TestKnownCapabilitiesExcludesCoreAndIsStable
TestSharedRefSliceBoundary
TestPublicCapabilityConstantsStable
覆盖版本主段解析、能力清单排序/去重/core 排除、共享引用切片边界和能力常量。
## internal/plugins/cfgmgr
TestStartRegistersConfigToolsWithConcurrencyMarkers
TestStopIsSafe
通过公开 Start 注入 SDK 工具注册回调,观察七个配置工具及其
Serial/ParallelSafe 并发契约;未访问私有 handler。
## 变异验证
mcp 忽略 MCP isError → isError 未被判失败
proc MajorVersion 不截断 → 多组版本断言红
cfgmgr 写工具取消 Serial 标记 → 写工具未声明 Serial
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 51 个测试包 / 100 包无测试 / 0 FAIL
3 个新包 -race 全绿
源码目录 _test.go 0
121 lines
4.1 KiB
Go
121 lines
4.1 KiB
Go
package test
|
||
|
||
// mcp 的黑盒判据:httptest 提供真实 HTTP JSON-RPC 输入,
|
||
// 通过公开 NewSSETransport/NewServer/ListTools/CallTool 观察输出。
|
||
|
||
import (
|
||
"encoding/json"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"strings"
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/plugins/mcp"
|
||
)
|
||
|
||
func rpcServer(t *testing.T, handler func(method string, body []byte) (status int, ct string, resp string)) *httptest.Server {
|
||
t.Helper()
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
body, _ := io.ReadAll(r.Body)
|
||
var req struct{ Method string }
|
||
_ = json.Unmarshal(body, &req)
|
||
st, ct, resp := handler(req.Method, body)
|
||
w.Header().Set("Content-Type", ct)
|
||
w.WriteHeader(st)
|
||
_, _ = w.Write([]byte(resp))
|
||
}))
|
||
t.Cleanup(srv.Close)
|
||
return srv
|
||
}
|
||
|
||
func newClient(t *testing.T, url string) *mcp.Server {
|
||
t.Helper()
|
||
s := mcp.NewServer("contract-mcp", mcp.NewSSETransport(url))
|
||
t.Cleanup(func() { _ = s.Close() })
|
||
return s
|
||
}
|
||
|
||
func TestListToolsThroughHTTP(t *testing.T) {
|
||
srv := rpcServer(t, func(method string, body []byte) (int, string, string) {
|
||
if method != "tools/list" {
|
||
t.Errorf("method = %q,期望 tools/list", method)
|
||
}
|
||
return 200, "application/json", `{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"read_file","description":"读取文件"},{"name":"write_file","description":"写文件"}]}}`
|
||
})
|
||
tools, err := newClient(t, srv.URL).ListTools()
|
||
if err != nil {
|
||
t.Fatalf("ListTools: %v", err)
|
||
}
|
||
if len(tools) != 2 || tools[0].Name != "read_file" || tools[1].Name != "write_file" {
|
||
t.Fatalf("tools = %+v", tools)
|
||
}
|
||
}
|
||
|
||
func TestCallToolSuccessAndEmptyContentHandling(t *testing.T) {
|
||
srv := rpcServer(t, func(method string, body []byte) (int, string, string) {
|
||
if method != "tools/call" {
|
||
t.Errorf("method = %q,期望 tools/call", method)
|
||
}
|
||
return 200, "application/json", `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"world"}],"isError":false}}`
|
||
})
|
||
out, err := newClient(t, srv.URL).CallTool("greet", map[string]interface{}{"x": 1})
|
||
if err != nil {
|
||
t.Fatalf("CallTool: %v", err)
|
||
}
|
||
if out != "world" {
|
||
t.Fatalf("out = %q", out)
|
||
}
|
||
|
||
emptySrv := rpcServer(t, func(method string, body []byte) (int, string, string) {
|
||
return 200, "application/json", `{"jsonrpc":"2.0","id":1,"result":{"content":[]}}`
|
||
})
|
||
if _, err := newClient(t, emptySrv.URL).CallTool("x", nil); err == nil || !strings.Contains(err.Error(), "未返回任何文本") {
|
||
t.Fatalf("空内容未被判失败:%v", err)
|
||
}
|
||
}
|
||
|
||
func TestCallToolSurfacesIsErrorAsFailure(t *testing.T) {
|
||
srv := rpcServer(t, func(method string, body []byte) (int, string, string) {
|
||
return 200, "application/json", `{"jsonrpc":"2.0","id":1,"result":{"content":[{"type":"text","text":"id is required"}],"isError":true}}`
|
||
})
|
||
_, err := newClient(t, srv.URL).CallTool("x", nil)
|
||
if err == nil || !strings.Contains(err.Error(), "id is required") {
|
||
t.Fatalf("isError 未被判失败:%v", err)
|
||
}
|
||
}
|
||
|
||
func TestCallToolSurfacesRPCError(t *testing.T) {
|
||
srv := rpcServer(t, func(method string, body []byte) (int, string, string) {
|
||
return 200, "application/json", `{"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"bad params"}}`
|
||
})
|
||
_, err := newClient(t, srv.URL).CallTool("x", nil)
|
||
if err == nil || !strings.Contains(err.Error(), "bad params") {
|
||
t.Fatalf("RPC error 未被透出:%v", err)
|
||
}
|
||
}
|
||
|
||
func TestCallToolSurfacesHTTPError(t *testing.T) {
|
||
srv := rpcServer(t, func(method string, body []byte) (int, string, string) {
|
||
return 500, "text/plain", "boom"
|
||
})
|
||
_, err := newClient(t, srv.URL).CallTool("x", nil)
|
||
if err == nil || !strings.Contains(err.Error(), "http status 500") {
|
||
t.Fatalf("HTTP 错误未被透出:%v", err)
|
||
}
|
||
}
|
||
|
||
func TestListToolsParseSSEStreamFrame(t *testing.T) {
|
||
srv := rpcServer(t, func(method string, body []byte) (int, string, string) {
|
||
frame := `{"jsonrpc":"2.0","id":1,"result":{"tools":[{"name":"sse_tool","description":"d"}]}}`
|
||
return 200, "text/event-stream", "event: message\ndata: " + frame + "\n\n"
|
||
})
|
||
tools, err := newClient(t, srv.URL).ListTools()
|
||
if err != nil {
|
||
t.Fatalf("SSE LL: %v", err)
|
||
}
|
||
if len(tools) != 1 || tools[0].Name != "sse_tool" {
|
||
t.Fatalf("tools=%+v", tools)
|
||
}
|
||
}
|