返回 DeepSeek-Reasonix
toolresult_rich_test.go
根目录 / internal / plugin / toolresult_rich_test.go
1 package plugin
2
3 import (
4 "encoding/base64"
5 "encoding/json"
6 "strings"
7 "testing"
8 )
9
10 func TestParseToolResultProjectsStructuredContentDeterministically(t *testing.T) {
11 res := json.RawMessage(`{
12 "content":[{"type":"text","text":"summary"}],
13 "structuredContent":{"z":2,"a":{"second":2,"first":1}},
14 "_meta":{"private":"must-not-reach-model"}
15 }`)
16
17 text, images, err := parseToolResult(res)
18 if err != nil {
19 t.Fatalf("parseToolResult: %v", err)
20 }
21 want := "summary\n[MCP structured content]\n{\n \"a\": {\n \"first\": 1,\n \"second\": 2\n },\n \"z\": 2\n}"
22 if text != want {
23 t.Fatalf("text = %q, want %q", text, want)
24 }
25 if len(images) != 0 {
26 t.Fatalf("images = %v, want none", images)
27 }
28 if strings.Contains(text, "must-not-reach-model") {
29 t.Fatal("CallToolResult _meta leaked into the model projection")
30 }
31
32 again, _, err := parseToolResult(res)
33 if err != nil || again != text {
34 t.Fatalf("projection is not deterministic: text=%q again=%q err=%v", text, again, err)
35 }
36 }
37
38 func TestParseToolResultProjectsResourceLinkMetadataOnly(t *testing.T) {
39 res := json.RawMessage(`{"content":[{
40 "type":"resource_link",
41 "uri":"https://example.test/report.json",
42 "name":"report",
43 "title":"Quarterly report",
44 "description":"A compact report",
45 "mimeType":"application/json",
46 "size":42,
47 "_meta":{"token":"secret"}
48 }]}`)
49
50 text, images, err := parseToolResult(res)
51 if err != nil {
52 t.Fatalf("parseToolResult: %v", err)
53 }
54 want := `[MCP resource link] {"uri":"https://example.test/report.json","name":"report","title":"Quarterly report","description":"A compact report","mimeType":"application/json","size":42}`
55 if text != want {
56 t.Fatalf("text = %q, want %q", text, want)
57 }
58 if len(images) != 0 {
59 t.Fatalf("images = %v, want none", images)
60 }
61 if strings.Contains(text, "secret") {
62 t.Fatal("resource _meta leaked into the model projection")
63 }
64 }
65
66 func TestParseToolResultProjectsEmbeddedResources(t *testing.T) {
67 payload := base64.StdEncoding.EncodeToString([]byte("png-bytes"))
68 res := json.RawMessage(`{"content":[
69 {"type":"resource","resource":{"uri":"file:///notes.txt","mimeType":"text/plain","text":"hello from resource","_meta":{"private":"hidden"}}},
70 {"type":"resource","resource":{"uri":"file:///chart.png","mimeType":"image/png","blob":"` + payload + `"}},
71 {"type":"resource","resource":{"uri":"file:///archive.bin","mimeType":"application/octet-stream","blob":"AQID"}}
72 ]}`)
73
74 text, images, err := parseToolResult(res)
75 if err != nil {
76 t.Fatalf("parseToolResult: %v", err)
77 }
78 for _, want := range []string{
79 `[MCP embedded resource] {"uri":"file:///notes.txt","mimeType":"text/plain"}`,
80 "hello from resource",
81 `[MCP embedded resource] {"uri":"file:///chart.png","mimeType":"image/png"}`,
82 "[image: image/png]",
83 `[MCP binary resource] {"mimeType":"application/octet-stream","encodedBytes":4,"decodedBytes":3,"data":"omitted"}`,
84 } {
85 if !strings.Contains(text, want) {
86 t.Fatalf("text = %q, want substring %q", text, want)
87 }
88 }
89 if strings.Contains(text, payload) || strings.Contains(text, "hidden") {
90 t.Fatalf("embedded base64 or _meta leaked into text: %q", text)
91 }
92 if len(images) != 1 || images[0] != "data:image/png;base64,"+payload {
93 t.Fatalf("images = %v, want embedded png", images)
94 }
95 }
96
97 func TestParseToolResultProjectsAudioAsBoundedMetadata(t *testing.T) {
98 res := json.RawMessage(`{"content":[{"type":"audio","mimeType":"audio/wav","data":"AQID","_meta":{"private":"hidden"}}]}`)
99
100 text, images, err := parseToolResult(res)
101 if err != nil {
102 t.Fatalf("parseToolResult: %v", err)
103 }
104 want := `[MCP audio] {"mimeType":"audio/wav","encodedBytes":4,"decodedBytes":3,"data":"omitted: no audio provider channel"}`
105 if text != want {
106 t.Fatalf("text = %q, want %q", text, want)
107 }
108 if strings.Contains(text, "AQID") || strings.Contains(text, "hidden") {
109 t.Fatalf("audio base64 or _meta leaked into text: %q", text)
110 }
111 if len(images) != 0 {
112 t.Fatalf("images = %v, want none", images)
113 }
114 }
115
116 func TestParseToolResultUnknownContentIsVisibleWithoutInlinePayload(t *testing.T) {
117 res := json.RawMessage(`{"content":[{"type":"video","mimeType":"video/mp4","data":"private-inline-payload","_meta":{"private":"hidden"}}]}`)
118
119 text, images, err := parseToolResult(res)
120 if err != nil {
121 t.Fatalf("parseToolResult: %v", err)
122 }
123 if text != `[unsupported MCP content block] {"type":"video"}` {
124 t.Fatalf("text = %q", text)
125 }
126 if strings.Contains(text, "private-inline-payload") || strings.Contains(text, "hidden") {
127 t.Fatalf("unknown inline payload or _meta leaked into text: %q", text)
128 }
129 if len(images) != 0 {
130 t.Fatalf("images = %v, want none", images)
131 }
132 }
133
134 func TestParseToolResultRichProjectionIsBounded(t *testing.T) {
135 structured, err := json.Marshal(map[string]any{"payload": strings.Repeat("s", maxToolResultRichItemBytes+1)})
136 if err != nil {
137 t.Fatal(err)
138 }
139 embeddedText := strings.Repeat("r", maxToolResultRichItemBytes+1)
140 res := json.RawMessage(`{"content":[{"type":"resource","resource":{"uri":"file:///large.txt","mimeType":"text/plain","text":` + string(mustJSON(t, embeddedText)) + `}}],"structuredContent":` + string(structured) + `}`)
141
142 text, images, err := parseToolResult(res)
143 if err != nil {
144 t.Fatalf("parseToolResult: %v", err)
145 }
146 if len(text) > maxToolResultRichProjectionBytes {
147 t.Fatalf("projection = %d bytes, want at most %d", len(text), maxToolResultRichProjectionBytes)
148 }
149 if !strings.Contains(text, "projection truncated") {
150 t.Fatalf("text = %q, want embedded-resource truncation marker", text)
151 }
152 if !strings.Contains(text, "structured content omitted") {
153 t.Fatalf("text = %q, want structured-content omission marker", text)
154 }
155 if len(images) != 0 {
156 t.Fatalf("images = %v, want none", images)
157 }
158 }
159
160 func mustJSON(t *testing.T, value any) json.RawMessage {
161 t.Helper()
162 b, err := json.Marshal(value)
163 if err != nil {
164 t.Fatal(err)
165 }
166 return b
167 }
168
168 lines GO