返回 DeepSeek-Reasonix
attachment_routing_test.go
根目录 / internal / boot / attachment_routing_test.go
1 package boot
2
3 import (
4 "context"
5 "encoding/base64"
6 "encoding/json"
7 "net/http"
8 "net/http/httptest"
9 "os"
10 "path/filepath"
11 "reasonix/internal/config"
12 "reasonix/internal/event"
13 "reasonix/internal/plugin"
14 "reasonix/internal/provider"
15 "strings"
16 "sync/atomic"
17 "testing"
18 )
19
20 func TestBuildDeepSeekTextParentCanUseImageReturningMCPAndVisionSubagent(t *testing.T) {
21 isolateConfigHome(t)
22 dir := robustTempDir(t)
23 t.Chdir(dir)
24
25 registerBootSubagentTestProvider()
26 prov := &bootSubagentTestProvider{combinedVision: true}
27 setBootSubagentTestProvider(t, prov)
28 if _, err := config.SetCredential("BOOT_DEEPSEEK_TEST_KEY", "test-key"); err != nil {
29 t.Fatalf("store test DeepSeek credential: %v", err)
30 }
31 writeFile(t, dir, "reasonix.toml", `
32 default_model = "parent"
33
34 [agent]
35 system_prompt = "BASE"
36 subagent_model = "vision-model"
37 vision_model = "vision-model"
38
39 [[providers]]
40 name = "parent"
41 kind = "boot-subagent-test"
42 base_url = "https://api.deepseek.com/anthropic"
43 model = "x"
44 api_key_env = "BOOT_DEEPSEEK_TEST_KEY"
45
46 [[providers]]
47 name = "vision-model"
48 kind = "boot-subagent-test"
49 base_url = "https://vision.example.invalid"
50 model = "x"
51 vision = true
52 `)
53 png, err := base64.StdEncoding.DecodeString("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==")
54 if err != nil {
55 t.Fatalf("decode test png: %v", err)
56 }
57 if err := os.MkdirAll(filepath.Join(dir, ".reasonix", "attachments"), 0o755); err != nil {
58 t.Fatal(err)
59 }
60 if err := os.WriteFile(filepath.Join(dir, ".reasonix", "attachments", "shot.png"), png, 0o644); err != nil {
61 t.Fatal(err)
62 }
63 mcpImage := base64.StdEncoding.EncodeToString(png)
64 var mcpCalls atomic.Int32
65 mcpServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
66 var request struct {
67 ID *int `json:"id"`
68 Method string `json:"method"`
69 Params json.RawMessage `json:"params"`
70 }
71 if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
72 http.Error(w, "bad request", http.StatusBadRequest)
73 return
74 }
75 if request.ID == nil {
76 w.WriteHeader(http.StatusAccepted)
77 return
78 }
79 var result any
80 switch request.Method {
81 case "initialize":
82 result = map[string]any{
83 "protocolVersion": "2024-11-05",
84 "serverInfo": map[string]any{"name": "vision-reader", "version": "1"},
85 "capabilities": map[string]any{"tools": map[string]any{}},
86 }
87 case "tools/list":
88 result = map[string]any{"tools": []map[string]any{{
89 "name": "inspect",
90 "description": "Inspect an image file by path.",
91 "inputSchema": map[string]any{
92 "type": "object",
93 "properties": map[string]any{"path": map[string]any{"type": "string"}},
94 "required": []string{"path"},
95 },
96 "annotations": map[string]any{"readOnlyHint": true},
97 }}}
98 case "tools/call":
99 mcpCalls.Add(1)
100 result = map[string]any{"content": []map[string]any{
101 {"type": "text", "text": "vision-mcp-ok"},
102 {"type": "image", "mimeType": "image/png", "data": mcpImage},
103 }}
104 default:
105 http.Error(w, "unsupported method", http.StatusBadRequest)
106 return
107 }
108 w.Header().Set("Content-Type", "application/json")
109 _ = json.NewEncoder(w).Encode(map[string]any{"jsonrpc": "2.0", "id": *request.ID, "result": result})
110 }))
111 defer mcpServer.Close()
112
113 ctrl, err := Build(context.Background(), Options{
114 Sink: event.Discard,
115 ExtraPlugins: []plugin.Spec{{
116 Name: "vision-reader",
117 Type: "http",
118 URL: mcpServer.URL,
119 Authorized: true,
120 }},
121 })
122 if err != nil {
123 t.Fatalf("Build: %v", err)
124 }
125 defer ctrl.Close()
126 if ctrl.ImageInputEnabled() {
127 loaded, loadErr := config.LoadForRoot(dir)
128 resolved, _ := loaded.ResolveModel(ctrl.ModelRef())
129 t.Fatalf("official DeepSeek parent unexpectedly enables direct images: ref=%q entry=%+v load_err=%v", ctrl.ModelRef(), resolved, loadErr)
130 }
131 if err := ctrl.Run(context.Background(), "review @.reasonix/attachments/shot.png"); err != nil {
132 t.Fatalf("Run: %v", err)
133 }
134 reqs := prov.requestsSnapshot()
135 if len(reqs) < 4 {
136 t.Fatalf("provider requests = %d, want parent MCP call, parent subagent call, vision child, and parent final", len(reqs))
137 }
138 if got := mcpCalls.Load(); got != 1 {
139 t.Fatalf("vision MCP calls = %d, want 1", got)
140 }
141 // MCP tools stay behind use_capability; review is registered for dispatch.
142 if !requestHasTool(reqs[0], "use_capability") {
143 t.Fatalf("parent tools = %v, want use_capability for MCP/review dispatch", toolSchemaNames(reqs[0].Tools))
144 }
145 registered := map[string]bool{}
146 for _, e := range ctrl.AllToolContractEntries() {
147 registered[e.Name] = true
148 }
149 if !registered["review"] && !registered["run_skill"] {
150 t.Fatalf("capability registry missing review/run_skill: %v", registered)
151 }
152 if got := bootLastUser(reqs[0]); !strings.Contains(got, "@.reasonix/attachments/shot.png") {
153 t.Fatalf("text-only parent lost the attachment reference needed by MCP: %q", got)
154 }
155 // The text parent receives summaries; native images remain available to
156 // the independently routed vision child.
157 for _, requestIndex := range []int{0, 1, len(reqs) - 1} {
158 for _, msg := range reqs[requestIndex].Messages {
159 if msg.Role == provider.RoleUser && len(msg.Images) != 0 {
160 t.Fatalf("text-only parent request %d embedded %d direct attachment(s): %+v", requestIndex, len(msg.Images), reqs[requestIndex].Messages)
161 }
162 }
163 }
164 if !requestMessageContains(reqs[1].Messages, provider.RoleTool, "vision-mcp-ok") {
165 t.Fatalf("parent did not receive the vision MCP text result: %+v", reqs[1].Messages)
166 }
167 var parentMCPImageCount int
168 for _, msg := range reqs[1].Messages {
169 if msg.Role == provider.RoleTool && msg.Name == "mcp__vision-reader__inspect" {
170 parentMCPImageCount += len(msg.Images)
171 }
172 }
173 if parentMCPImageCount != 0 {
174 t.Fatalf("text parent MCP result images = %d, want summary only", parentMCPImageCount)
175 }
176 var childImageCount int
177 for _, msg := range reqs[2].Messages {
178 if msg.Role == provider.RoleUser {
179 childImageCount = len(msg.Images)
180 }
181 }
182 if childImageCount != 1 {
183 t.Fatalf("vision child user images = %d, want one attachment; request = %+v", childImageCount, reqs[2].Messages)
184 }
185 }
186
186 lines GO