Why: the completion doc requires the client to recognize resultType, treat input_required as a protocol-level result whose answer retries the original tools/call, and expose tasks/get|update|cancel; today only a content-block special case existed and the advertised version was never checked against the server's. Evidence: GOWORK=off go vet + go test -count=1 ./... green; TestInputRequiredResultTypeRetriesOriginalCall (ask → tasks/update → retry, call order and final content), TestNegotiatedVersionFallbackKeepsLegacyPath (byte-level legacy behavior on 2024-11-05), TestTasksGetAndCancelRoundTrip, TestRunEndCancelsSeenTasks, TestDefaultSchemaDeclares202012; the acceptance-③ regression TestStdioSlowCancelThenEcho passes unchanged. Impact: advertise 2026-07-28 and branch on the negotiated version (older servers keep byte-identical behavior); synthesized default tool schemas now declare JSON Schema 2020-12; runs cancel the MCP tasks they saw via a per-run TaskTracker (other runs on the shared manager are unaffected). Refs: Agent-System-Documents#42 (N2 plan WP3)
534 lines
16 KiB
Go
534 lines
16 KiB
Go
package mcp
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"agent-native/runtime/tools"
|
||
)
|
||
|
||
// TestMain:环境变量置位时,本测试二进制化身为 stdio MCP server(真实子进程测试)。
|
||
func TestMain(m *testing.M) {
|
||
if os.Getenv(FakeServerEnv) == "1" {
|
||
RunFakeServer()
|
||
os.Exit(0)
|
||
}
|
||
os.Exit(m.Run())
|
||
}
|
||
|
||
func fakeStdioConfig(t *testing.T) ServerConfig {
|
||
t.Helper()
|
||
exe, err := os.Executable()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return ServerConfig{
|
||
Name: "fake",
|
||
Command: exe,
|
||
Env: append(os.Environ(), FakeServerEnv+"=1"),
|
||
}
|
||
}
|
||
|
||
func TestStdioClientHandshakeListAndCall(t *testing.T) {
|
||
ctx := context.Background()
|
||
c, err := DialStdio(ctx, fakeStdioConfig(t))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer c.Close()
|
||
|
||
list, err := c.ListTools(ctx)
|
||
if err != nil || len(list) != 5 || list[0].Name != "echo" {
|
||
t.Fatalf("list failed: %v %+v", err, list)
|
||
}
|
||
res, err := c.CallTool(ctx, "echo", json.RawMessage(`{"text":"hi"}`))
|
||
if err != nil || len(res.Content) != 1 || res.Content[0].Text != "echo: hi" {
|
||
t.Fatalf("call failed: %v %+v", err, res)
|
||
}
|
||
_, err = c.CallTool(ctx, "no_such_tool", nil)
|
||
if err == nil || !strings.Contains(err.Error(), "unknown tool") {
|
||
t.Fatalf("unknown-tool call must fail via rpc error, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestStdioClientServerRejectsBadSchema(t *testing.T) {
|
||
ctx := context.Background()
|
||
c, err := DialStdio(ctx, fakeStdioConfig(t))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer c.Close()
|
||
// server 端 tool 报 isError:作为 callToolResult 返回而非 rpc 错误。
|
||
res, err := c.CallTool(ctx, "boom", nil)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !res.IsError || res.Content[0].Text != "boom failed" {
|
||
t.Fatalf("want isError result, got %+v", res)
|
||
}
|
||
}
|
||
|
||
func TestStdioSlowCancelThenEcho(t *testing.T) {
|
||
ctx := context.Background()
|
||
c, err := DialStdio(ctx, fakeStdioConfig(t))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer c.Close()
|
||
|
||
slowCtx, cancel := context.WithTimeout(ctx, 80*time.Millisecond)
|
||
defer cancel()
|
||
_, err = c.CallTool(slowCtx, "slow", nil)
|
||
if err == nil {
|
||
t.Fatal("slow call should time out")
|
||
}
|
||
|
||
echoCtx, echoCancel := context.WithTimeout(ctx, 3*time.Second)
|
||
defer echoCancel()
|
||
res, err := c.CallTool(echoCtx, "echo", json.RawMessage(`{"text":"after-slow"}`))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(res.Content) != 1 || res.Content[0].Text != "echo: after-slow" {
|
||
t.Fatalf("echo must receive its own result, got %+v", res)
|
||
}
|
||
if c.Readers() != 1 {
|
||
t.Fatalf("cancel must leave a single reader, got %d", c.Readers())
|
||
}
|
||
}
|
||
|
||
func fakeHTTPHandler(w http.ResponseWriter, r *http.Request) {
|
||
var req rpcRequest
|
||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||
w.WriteHeader(400)
|
||
return
|
||
}
|
||
resp := fakeServerHandle(req)
|
||
if sid := r.Header.Get("Mcp-Session-Id"); sid != "" {
|
||
w.Header().Set("Mcp-Session-Id", sid)
|
||
} else {
|
||
w.Header().Set("Mcp-Session-Id", "sess-1")
|
||
}
|
||
w.Header().Set("Content-Type", "application/json")
|
||
_ = json.NewEncoder(w).Encode(resp)
|
||
}
|
||
|
||
func TestHTTPClientJSONMode(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(fakeHTTPHandler))
|
||
defer srv.Close()
|
||
ctx := context.Background()
|
||
c, err := DialHTTP(ctx, ServerConfig{Name: "fakehttp", URL: srv.URL})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer c.Close()
|
||
if c.sessionID != "sess-1" {
|
||
t.Fatalf("session id must be captured, got %q", c.sessionID)
|
||
}
|
||
list, err := c.ListTools(ctx)
|
||
if err != nil || len(list) != 5 {
|
||
t.Fatalf("list failed: %v %+v", err, list)
|
||
}
|
||
res, err := c.CallTool(ctx, "echo", json.RawMessage(`{"text":"net"}`))
|
||
if err != nil || res.Content[0].Text != "echo: net" {
|
||
t.Fatalf("call failed: %v %+v", err, res)
|
||
}
|
||
}
|
||
|
||
func TestHTTPClientSSEMode(t *testing.T) {
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
var req rpcRequest
|
||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||
resp := fakeServerHandle(req)
|
||
w.Header().Set("Content-Type", "text/event-stream")
|
||
body, _ := json.Marshal(resp)
|
||
fmt.Fprintf(w, "event: message\ndata: %s\n\n", body)
|
||
}))
|
||
defer srv.Close()
|
||
ctx := context.Background()
|
||
c, err := DialHTTP(ctx, ServerConfig{Name: "fakesse", URL: srv.URL})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer c.Close()
|
||
list, err := c.ListTools(ctx)
|
||
if err != nil || len(list) != 5 {
|
||
t.Fatalf("sse list failed: %v %+v", err, list)
|
||
}
|
||
}
|
||
|
||
func TestManagerRegisterIntoAndCallThroughRegistry(t *testing.T) {
|
||
mgr, err := NewManager([]ServerConfig{fakeStdioConfig(t)})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry(t.TempDir())
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var names []string
|
||
for _, d := range reg.Decls() {
|
||
names = append(names, d.Name)
|
||
}
|
||
joined := strings.Join(names, ",")
|
||
if !strings.Contains(joined, "mcp__fake__echo") || !strings.Contains(joined, "mcp__fake__boom") {
|
||
t.Fatalf("mcp tools not registered: %v", names)
|
||
}
|
||
res := reg.Execute(context.Background(), "mcp__fake__echo", json.RawMessage(`{"text":"bridge"}`))
|
||
if res.IsError || res.Content != "echo: bridge\n" {
|
||
t.Fatalf("bridge call failed: %+v", res)
|
||
}
|
||
res = reg.Execute(context.Background(), "mcp__fake__boom", nil)
|
||
if !res.IsError || !strings.Contains(res.Content, "boom failed") {
|
||
t.Fatalf("bridge error propagation failed: %+v", res)
|
||
}
|
||
}
|
||
|
||
func TestManagerNamespaceWildcardRestrict(t *testing.T) {
|
||
mgr, _ := NewManager([]ServerConfig{fakeStdioConfig(t)})
|
||
defer mgr.Close()
|
||
reg := tools.WorkspaceTools(t.TempDir())
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := reg.Restrict([]string{"mcp__fake__*"}); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, d := range reg.Decls() {
|
||
if !strings.HasPrefix(d.Name, "mcp__fake__") {
|
||
t.Fatalf("wildcard restrict must keep only mcp namespace, got %s", d.Name)
|
||
}
|
||
}
|
||
// 通配未命中任何工具 → fail-closed。
|
||
err := reg.Restrict([]string{"mcp__nope__*"})
|
||
if err == nil || !strings.Contains(err.Error(), "mcp__nope__*") {
|
||
t.Fatalf("unknown namespace wildcard must fail-closed, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestManagerFailClosedOnUnreachableServer(t *testing.T) {
|
||
mgr, err := NewManager([]ServerConfig{{Name: "dead", Command: "/nonexistent/mcp-server-bin"}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry(t.TempDir())
|
||
err = mgr.RegisterInto(context.Background(), reg)
|
||
if err == nil || !strings.Contains(err.Error(), "unreachable") {
|
||
t.Fatalf("unreachable server must fail-closed, got %v", err)
|
||
}
|
||
}
|
||
|
||
func TestManagerConfigValidation(t *testing.T) {
|
||
if _, err := NewManager([]ServerConfig{{Name: "bad name", Command: "x"}}); err == nil {
|
||
t.Fatal("invalid name must be rejected")
|
||
}
|
||
if _, err := NewManager([]ServerConfig{{Name: "both", Command: "x", URL: "http://y"}}); err == nil {
|
||
t.Fatal("dual transport must be rejected")
|
||
}
|
||
if _, err := NewManager([]ServerConfig{{Name: "none"}}); err == nil {
|
||
t.Fatal("missing transport must be rejected")
|
||
}
|
||
_, err := NewManager([]ServerConfig{{Name: "a", Command: "x"}, {Name: "a", URL: "http://y"}})
|
||
if err == nil || !strings.Contains(err.Error(), "twice") {
|
||
t.Fatal("duplicate names must be rejected")
|
||
}
|
||
}
|
||
|
||
func TestManagerRegisterTimeoutBounded(t *testing.T) {
|
||
// server 挂起不响应:initialize 必须在超时内 fail-closed,而不是永久阻塞。
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
select {
|
||
case <-r.Context().Done():
|
||
case <-time.After(10 * time.Second):
|
||
}
|
||
}))
|
||
defer srv.Close()
|
||
mgr, err := NewManager([]ServerConfig{{Name: "slow", URL: srv.URL, Timeout: 300 * time.Millisecond}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry(t.TempDir())
|
||
start := time.Now()
|
||
err = mgr.RegisterInto(context.Background(), reg)
|
||
elapsed := time.Since(start)
|
||
if err == nil || !strings.Contains(err.Error(), "unreachable") {
|
||
t.Fatalf("hung server must fail-closed, got %v", err)
|
||
}
|
||
if elapsed > 5*time.Second {
|
||
t.Fatalf("timeout must bound the dial, took %s", elapsed)
|
||
}
|
||
}
|
||
|
||
func TestBackgroundConnectDoesNotBlockTheCaller(t *testing.T) {
|
||
started := make(chan struct{})
|
||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
select {
|
||
case <-started:
|
||
default:
|
||
close(started)
|
||
}
|
||
time.Sleep(400 * time.Millisecond)
|
||
fakeHTTPHandler(w, r)
|
||
}))
|
||
defer srv.Close()
|
||
mgr, err := NewManager([]ServerConfig{{Name: "late", URL: srv.URL}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry(t.TempDir())
|
||
t0 := time.Now()
|
||
mgr.StartBackground(context.Background(), reg)
|
||
if time.Since(t0) > 150*time.Millisecond {
|
||
t.Fatalf("background connect blocked the caller for %s", time.Since(t0))
|
||
}
|
||
deadline := time.Now().Add(3 * time.Second)
|
||
for time.Now().Before(deadline) {
|
||
if reg.Has("mcp__late__echo") {
|
||
return
|
||
}
|
||
time.Sleep(20 * time.Millisecond)
|
||
}
|
||
t.Fatal("tools did not appear after the server became ready")
|
||
}
|
||
|
||
func TestDeferredMCPToolsUseSearchAndCall(t *testing.T) {
|
||
mgr, err := NewManager([]ServerConfig{{Name: "fake", Command: fakeStdioConfig(t).Command, Args: fakeStdioConfig(t).Args, Env: fakeStdioConfig(t).Env, Exposure: "deferred"}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry(t.TempDir())
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, d := range reg.Decls() {
|
||
if strings.HasPrefix(d.Name, "mcp__fake__") {
|
||
t.Fatalf("deferred mcp tool listed: %s", d.Name)
|
||
}
|
||
}
|
||
if !strings.Contains(reg.Search("echo"), "mcp__fake__echo") {
|
||
t.Fatalf("search %q", reg.Search("echo"))
|
||
}
|
||
res := reg.Execute(context.Background(), "call_tool", json.RawMessage(`{"name":"mcp__fake__echo","arguments":{"text":"via"}}`))
|
||
if res.IsError || !strings.Contains(res.Content, "echo: via") {
|
||
t.Fatalf("call %+v", res)
|
||
}
|
||
}
|
||
|
||
func TestDisabledServerIsSkipped(t *testing.T) {
|
||
off := false
|
||
mgr, err := NewManager([]ServerConfig{{Name: "fake", Command: "x", Enabled: &off}})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
reg := tools.NewRegistry(t.TempDir())
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if reg.Has("mcp__fake__echo") {
|
||
t.Fatal("disabled server registered a tool")
|
||
}
|
||
if err := mgr.ApplyOverrides(`{"fake":{"enabled":true,"exposure":"direct"}}`); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !mgr.Configs()[0].IsEnabled() || mgr.Configs()[0].Exposure != "direct" {
|
||
t.Fatalf("override %+v", mgr.Configs()[0])
|
||
}
|
||
}
|
||
|
||
func TestManyToolsDefaultToDeferred(t *testing.T) {
|
||
if chooseExposure("", 9) != tools.ExposureDeferred {
|
||
t.Fatal("many tools should default to deferred")
|
||
}
|
||
if chooseExposure("direct", 9) != tools.ExposureDirect {
|
||
t.Fatal("explicit direct must win")
|
||
}
|
||
}
|
||
|
||
type askClient struct{}
|
||
|
||
func (askClient) Initialize(context.Context) (initializeResult, error) {
|
||
return initializeResult{}, nil
|
||
}
|
||
func (askClient) NegotiatedVersion() string { return LegacyProtocolVersion }
|
||
func (askClient) ListTools(context.Context) ([]toolInfo, error) { return nil, nil }
|
||
func (askClient) Close() error { return nil }
|
||
func (askClient) TasksGet(context.Context, string) (taskResult, error) {
|
||
return taskResult{}, nil
|
||
}
|
||
func (askClient) TasksUpdate(context.Context, string, string) error { return nil }
|
||
func (askClient) TasksCancel(context.Context, string) error { return nil }
|
||
func (askClient) CallTool(context.Context, string, json.RawMessage) (callToolResult, error) {
|
||
return callToolResult{Content: []contentBlock{{Type: "input_required", Text: "选一个颜色"}}}, nil
|
||
}
|
||
|
||
func TestInputRequiredUsesAsk(t *testing.T) {
|
||
tool := &bridgeTool{
|
||
client: askClient{}, declName: "mcp__x__ask", toolName: "ask",
|
||
schema: json.RawMessage(`{"type":"object","properties":{}}`),
|
||
}
|
||
ctx := WithAsk(context.Background(), func(context.Context, string) (string, error) { return "蓝色", nil })
|
||
res, err := tool.Execute(ctx, json.RawMessage(`{}`))
|
||
if err != nil || res.IsError || res.Content != "蓝色" {
|
||
t.Fatalf("ask result %+v %v", res, err)
|
||
}
|
||
}
|
||
|
||
// ---- N2 WP3:2026-07-28 对齐 ----
|
||
|
||
// 协议级 input_required:ask 取应答 → tasks/update 投递 → 原参数重试得终结果。
|
||
func TestInputRequiredResultTypeRetriesOriginalCall(t *testing.T) {
|
||
mgr, err := NewManager([]ServerConfig{fakeStdioConfig(t)})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry("")
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
var questions []string
|
||
ctx := WithAsk(context.Background(), func(_ context.Context, q string) (string, error) {
|
||
questions = append(questions, q)
|
||
return "蓝色", nil
|
||
})
|
||
res := reg.Execute(ctx, "mcp__fake__need_input", json.RawMessage(`{}`))
|
||
if res.IsError || res.Content != "答案已收到: 蓝色" {
|
||
t.Fatalf("final result: %+v", res)
|
||
}
|
||
if len(questions) != 1 || questions[0] != "要答案" {
|
||
t.Fatalf("ask questions: %v", questions)
|
||
}
|
||
}
|
||
|
||
// 协商回落:服务端只讲 2024-11-05 时,协议级路径不启用,旧内容块路径不变。
|
||
func TestNegotiatedVersionFallbackKeepsLegacyPath(t *testing.T) {
|
||
cfg := fakeStdioConfig(t)
|
||
cfg.Env = append(cfg.Env, "AGENT_NATIVE_TEST_MCP_VERSION="+LegacyProtocolVersion)
|
||
mgr, err := NewManager([]ServerConfig{cfg})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry("")
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ctx := WithAsk(context.Background(), func(_ context.Context, q string) (string, error) {
|
||
return "蓝色", nil
|
||
})
|
||
res := reg.Execute(ctx, "mcp__fake__legacy_ask", json.RawMessage(`{}`))
|
||
if res.IsError || res.Content != "蓝色" {
|
||
t.Fatalf("legacy path: %+v", res)
|
||
}
|
||
}
|
||
|
||
// tasks 客户端面:get 回状态、cancel 清除任务输入。
|
||
func TestTasksGetAndCancelRoundTrip(t *testing.T) {
|
||
cfg := fakeStdioConfig(t)
|
||
ctx := context.Background()
|
||
c, err := DialStdio(ctx, cfg)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer c.Close()
|
||
if got := c.NegotiatedVersion(); !SupportsResultType(got) {
|
||
t.Fatalf("negotiated = %q, want >= 2026-07-28", got)
|
||
}
|
||
if err := c.TasksUpdate(ctx, "task-x", "输入"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
tr, err := c.TasksGet(ctx, "task-x")
|
||
if err != nil || tr.TaskID != "task-x" || tr.Status != "running" {
|
||
t.Fatalf("tasks/get: %+v %v", tr, err)
|
||
}
|
||
if err := c.TasksCancel(ctx, "task-x"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
|
||
// 合成默认 schema 声明 JSON Schema 2020-12。
|
||
func TestDefaultSchemaDeclares202012(t *testing.T) {
|
||
mgr, err := NewManager([]ServerConfig{fakeStdioConfig(t)})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry("")
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
for _, d := range reg.Decls() {
|
||
if d.Name == "mcp__fake__legacy_ask" {
|
||
// legacy_ask 在 fake 里无 InputSchema → 走合成默认
|
||
if !strings.Contains(string(d.Parameters), "2020-12") {
|
||
t.Fatalf("默认 schema 未声明 2020-12:%s", d.Parameters)
|
||
}
|
||
return
|
||
}
|
||
}
|
||
t.Fatal("mcp__fake__legacy_ask not registered")
|
||
}
|
||
|
||
// 任务跟踪器:记录 → 按服务端分组取消 → 清空。
|
||
func TestTaskTrackerCancelAllPerRun(t *testing.T) {
|
||
cfg := fakeStdioConfig(t)
|
||
ctx := context.Background()
|
||
c, err := DialStdio(ctx, cfg)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer c.Close()
|
||
tr := &TaskTracker{seen: map[Client]map[string]struct{}{}}
|
||
if err := c.TasksUpdate(ctx, "task-a", "1"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := c.TasksUpdate(ctx, "task-b", "2"); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
tr.note(c, "task-a")
|
||
tr.note(c, "task-b")
|
||
tr.note(c, "task-a") // 幂等
|
||
if n := tr.CancelAll(ctx); n != 2 {
|
||
t.Fatalf("CancelAll = %d, want 2", n)
|
||
}
|
||
if n := tr.CancelAll(ctx); n != 0 {
|
||
t.Fatalf("第二次 CancelAll 应为 0,得到 %d", n)
|
||
}
|
||
}
|
||
|
||
// 端到端:经注册表执行 input_required 工具的 run 结束时,task-1 被取消。
|
||
func TestRunEndCancelsSeenTasks(t *testing.T) {
|
||
cfg := fakeStdioConfig(t)
|
||
mgr, err := NewManager([]ServerConfig{cfg})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer mgr.Close()
|
||
reg := tools.NewRegistry("")
|
||
if err := mgr.RegisterInto(context.Background(), reg); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
ctx, tr := WithTaskTracker(context.Background())
|
||
ctx = WithAsk(ctx, func(_ context.Context, q string) (string, error) { return "蓝", nil })
|
||
res := reg.Execute(ctx, "mcp__fake__need_input", json.RawMessage(`{}`))
|
||
if res.IsError {
|
||
t.Fatalf("execute: %+v", res)
|
||
}
|
||
// 模仿 adapter run 结束:取消本 run 见过的任务(task-1 已被记录)。
|
||
if n := tr.CancelAll(context.Background()); n != 1 {
|
||
t.Fatalf("CancelAll = %d, want 1(task-1)", n)
|
||
}
|
||
}
|