task_test.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. package kit
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "net/http"
  7. "net/http/httptest"
  8. "strings"
  9. "testing"
  10. "time"
  11. "github.com/mark3labs/mcp-go/mcp"
  12. "go.uber.org/zap"
  13. )
  14. type fakeTaskArgs struct {
  15. Name string `json:"name"`
  16. }
  17. // 业务数据:key -> 状态;cancelled 记录被真正取消的 key
  18. type fakeJobs struct {
  19. status map[string]mcp.TaskStatus
  20. cancelled []string
  21. }
  22. func newFakeTaskTool(jobs *fakeJobs) TaskDef[fakeTaskArgs] {
  23. return TaskDef[fakeTaskArgs]{
  24. Def: Def[fakeTaskArgs]{Name: "job_run", Title: "运行作业", Description: "测试", Handler: func(context.Context, fakeTaskArgs) (*mcp.CallToolResult, error) {
  25. return mcp.NewToolResultText("sync"), nil
  26. }},
  27. Poll: time.Minute,
  28. Start: func(_ context.Context, a fakeTaskArgs) (TaskStart, *mcp.CallToolResult) {
  29. if a.Name == "" {
  30. return TaskStart{}, Failf("参数 name 必填")
  31. }
  32. jobs.status[a.Name] = mcp.TaskStatusWorking
  33. return TaskStart{Key: a.Name, Flag: "owner", Note: "已开始"}, nil
  34. },
  35. Status: func(_ context.Context, ref TaskRef) (TaskState, error) {
  36. st, ok := jobs.status[ref.Key]
  37. if !ok {
  38. return TaskState{}, ErrTaskNotFound
  39. }
  40. s := TaskState{Status: st, Message: string(st)}
  41. if st == mcp.TaskStatusCompleted {
  42. s.Result = mcp.NewToolResultText("done " + ref.Key)
  43. }
  44. if st == mcp.TaskStatusFailed {
  45. s.Result = Failf("failed %s", ref.Key)
  46. }
  47. return s, nil
  48. },
  49. Cancel: func(_ context.Context, ref TaskRef) error {
  50. if ref.Flag == "owner" {
  51. jobs.cancelled = append(jobs.cancelled, ref.Key)
  52. }
  53. return nil
  54. },
  55. }
  56. }
  57. type rpcReply struct {
  58. Result json.RawMessage `json:"result"`
  59. Error *struct {
  60. Code int `json:"code"`
  61. Message string `json:"message"`
  62. } `json:"error"`
  63. }
  64. func TestTaskRouter(t *testing.T) {
  65. jobs := &fakeJobs{status: map[string]mcp.TaskStatus{}}
  66. passed := 0
  67. next := http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
  68. passed++
  69. _, _ = w.Write([]byte(`{"jsonrpc":"2.0","id":1,"result":{}}`))
  70. })
  71. router := NewTaskRouter(next, []TaskTool{newFakeTaskTool(jobs)}, "secret", zap.NewNop())
  72. call := func(method string, params any) rpcReply {
  73. t.Helper()
  74. body, _ := json.Marshal(map[string]any{"jsonrpc": "2.0", "id": 7, "method": method, "params": params})
  75. rec := httptest.NewRecorder()
  76. router.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/mcp", bytes.NewReader(body)))
  77. var r rpcReply
  78. if err := json.Unmarshal(rec.Body.Bytes(), &r); err != nil {
  79. t.Fatalf("%s: 响应不是 JSON-RPC:%s", method, rec.Body.String())
  80. }
  81. return r
  82. }
  83. // 以任务方式调用:立即返回任务与即时说明
  84. r := call("tools/call", map[string]any{"name": "job_run", "arguments": map[string]any{"name": "a"}, "task": map[string]any{"ttl": 1000}})
  85. var created struct {
  86. Task mcp.Task `json:"task"`
  87. Meta map[string]any `json:"_meta"`
  88. }
  89. if r.Error != nil || json.Unmarshal(r.Result, &created) != nil {
  90. t.Fatalf("创建任务失败:%+v", r)
  91. }
  92. id := created.Task.TaskId
  93. if !strings.HasPrefix(id, taskIDPrefix+".job_run.a.owner.") || created.Task.Status != mcp.TaskStatusWorking ||
  94. *created.Task.TTL != 1000 || *created.Task.PollInterval != 60000 || created.Meta[immediateMeta] != "已开始" {
  95. t.Errorf("创建任务的返回不符合预期:%+v", created)
  96. }
  97. // 参数错误:返回 JSON-RPC 错误
  98. if r := call("tools/call", map[string]any{"name": "job_run", "arguments": map[string]any{}, "task": map[string]any{}}); r.Error == nil || !strings.Contains(r.Error.Message, "name 必填") {
  99. t.Errorf("参数错误应返回错误:%+v", r)
  100. }
  101. // 查询状态
  102. var got mcp.Task
  103. if r := call("tasks/get", map[string]any{"taskId": id}); r.Error != nil || json.Unmarshal(r.Result, &got) != nil || got.Status != mcp.TaskStatusWorking {
  104. t.Errorf("tasks/get 应为 working:%+v", r)
  105. }
  106. // 篡改任务 id:找不到任务
  107. tampered := strings.Replace(id, ".a.", ".b.", 1)
  108. if r := call("tasks/get", map[string]any{"taskId": tampered}); r.Error == nil || r.Error.Code != mcp.INVALID_PARAMS {
  109. t.Errorf("篡改的任务 id 应返回 INVALID_PARAMS:%+v", r)
  110. }
  111. // 完成后取结果:带 related-task
  112. jobs.status["a"] = mcp.TaskStatusCompleted
  113. var res struct {
  114. Content []mcp.TextContent `json:"content"`
  115. IsError bool `json:"isError"`
  116. Meta map[string]any `json:"_meta"`
  117. }
  118. if r := call("tasks/result", map[string]any{"taskId": id}); r.Error != nil || json.Unmarshal(r.Result, &res) != nil ||
  119. res.Content[0].Text != "done a" || res.IsError || res.Meta[relatedMeta] == nil {
  120. t.Errorf("tasks/result 不符合预期:%+v %+v", r, res)
  121. }
  122. // 已结束的任务不能取消
  123. if r := call("tasks/cancel", map[string]any{"taskId": id}); r.Error == nil {
  124. t.Errorf("已完成的任务取消应报错:%+v", r)
  125. }
  126. // 取消进行中的任务:调用业务取消
  127. r = call("tools/call", map[string]any{"name": "job_run", "arguments": map[string]any{"name": "b"}, "task": map[string]any{}})
  128. _ = json.Unmarshal(r.Result, &created)
  129. if r := call("tasks/cancel", map[string]any{"taskId": created.Task.TaskId}); r.Error != nil || json.Unmarshal(r.Result, &got) != nil || got.Status != mcp.TaskStatusCancelled {
  130. t.Errorf("取消应返回 cancelled:%+v", r)
  131. }
  132. if len(jobs.cancelled) != 1 || jobs.cancelled[0] != "b" {
  133. t.Errorf("应取消作业 b,实际 %v", jobs.cancelled)
  134. }
  135. // 失败的任务:结果为 isError
  136. jobs.status["b"] = mcp.TaskStatusFailed
  137. if r := call("tasks/result", map[string]any{"taskId": created.Task.TaskId}); json.Unmarshal(r.Result, &res) != nil || !res.IsError {
  138. t.Errorf("失败任务的结果应为 isError:%+v", r)
  139. }
  140. // 不归本服务处理的请求交给 mcp-go:普通调用、别的任务 id、其他方法
  141. passed = 0
  142. call("tools/call", map[string]any{"name": "job_run", "arguments": map[string]any{"name": "c"}})
  143. call("tasks/get", map[string]any{"taskId": "other-task"})
  144. call("tools/list", map[string]any{})
  145. if passed != 3 {
  146. t.Errorf("应有 3 个请求交给 mcp-go,实际 %d", passed)
  147. }
  148. }
  149. func TestTaskDefDeclaresTaskSupport(t *testing.T) {
  150. tool := newFakeTaskTool(&fakeJobs{}).Tool()
  151. if tool.Execution == nil || tool.Execution.TaskSupport != mcp.TaskSupportOptional {
  152. t.Errorf("TaskDef 应声明 taskSupport=optional:%+v", tool.Execution)
  153. }
  154. }