返回 DeepSeek-Reasonix
tool_result_capability_test.go
根目录 / internal / agent / tool_result_capability_test.go
1 package agent
2
3 import (
4 "bytes"
5 "context"
6 "crypto/sha256"
7 "encoding/hex"
8 "encoding/json"
9 "reflect"
10 "strings"
11 "testing"
12 "unicode/utf8"
13
14 "reasonix/internal/event"
15 "reasonix/internal/provider"
16 "reasonix/internal/tool"
17 )
18
19 type toolResultPageHeader struct {
20 ResultRef string `json:"result_ref"`
21 Offset int `json:"offset"`
22 NextOffset int `json:"next_offset"`
23 TotalBytes int `json:"total_bytes"`
24 SHA256 string `json:"sha256"`
25 Complete bool `json:"complete"`
26 }
27
28 func executeToolResultPage(t *testing.T, proxy tool.Tool, callID, ref string, offset, limit int) (toolResultPageHeader, string, error) {
29 t.Helper()
30 args, _ := json.Marshal(map[string]any{
31 "action": "call", "capability_id": sessionToolResultCapabilityID,
32 "arguments": map[string]any{"tool_call_id": callID, "result_ref": ref, "offset": offset, "limit": limit},
33 })
34 out, err := proxy.Execute(context.Background(), args)
35 if err != nil {
36 return toolResultPageHeader{}, "", err
37 }
38 headerText, body, ok := strings.Cut(out, "\n")
39 if !ok {
40 t.Fatalf("page result has no metadata header: %.200q", out)
41 }
42 var header toolResultPageHeader
43 if err := json.Unmarshal([]byte(headerText), &header); err != nil {
44 t.Fatalf("decode page header %q: %v", headerText, err)
45 }
46 return header, body, nil
47 }
48
49 func newToolResultCapabilityAgent(t *testing.T, session *Session) (*Agent, *UseCapabilityTool) {
50 t.Helper()
51 reg := tool.NewRegistry()
52 proxy := NewUseCapabilityTool(context.Background(), nil, nil, reg, nil, nil, nil)
53 reg.Add(proxy)
54 a := New(nil, reg, session, Options{}, event.Discard)
55 return a, proxy
56 }
57
58 func TestSessionToolResultPagesReconstructCompleteUTF8Output(t *testing.T) {
59 full := strings.Repeat("ASCII-界-🧪\n", 6000)
60 bounded, notice := truncateToolOutputFor(full, "bash", "call-1")
61 if notice == "" || len(bounded) > maxToolOutputBytes {
62 t.Fatalf("fixture was not bounded: bytes=%d notice=%q", len(bounded), notice)
63 }
64 session := &Session{Messages: []provider.Message{{
65 Role: provider.RoleTool, Name: "bash", ToolCallID: "call-1", Content: bounded, RawContent: full,
66 }}}
67 _, proxy := newToolResultCapabilityAgent(t, session)
68 ref := toolResultRef("call-1", full)
69 if _, _, err := executeToolResultPage(t, proxy, "call-1", "", 0, 128); err == nil || !strings.Contains(err.Error(), "result_ref is required") {
70 t.Fatalf("new truncated result accepted no result_ref: %v", err)
71 }
72
73 var rebuilt strings.Builder
74 offset := 0
75 for pages := 0; ; pages++ {
76 if pages > 20 {
77 t.Fatal("pagination did not terminate")
78 }
79 header, page, err := executeToolResultPage(t, proxy, "call-1", ref, offset, 0)
80 if err != nil {
81 t.Fatal(err)
82 }
83 if header.Offset != offset || header.ResultRef != ref || header.TotalBytes != len(full) || header.SHA256 == "" {
84 t.Fatalf("unexpected page header: %+v", header)
85 }
86 if len(page) > toolResultPageDefaultBytes || len(page)+len(toolResultMustJSON(t, header))+1 > maxToolOutputBytes {
87 t.Fatalf("page exceeded bounded output: page=%d", len(page))
88 }
89 rebuilt.WriteString(page)
90 offset = header.NextOffset
91 if header.Complete {
92 break
93 }
94 }
95 if got := rebuilt.String(); got != full {
96 t.Fatalf("rebuilt result differs: got=%d want=%d", len(got), len(full))
97 }
98
99 header, page, err := executeToolResultPage(t, proxy, "call-1", ref, len(full), 1024)
100 if err != nil || !header.Complete || page != "" || header.NextOffset != len(full) {
101 t.Fatalf("terminal page = header=%+v page=%q err=%v", header, page, err)
102 }
103 }
104
105 func TestToolResultProviderBoundaryAndRecoveryMarker(t *testing.T) {
106 tests := []struct {
107 name string
108 body string
109 }{
110 {name: "below", body: strings.Repeat("a", maxToolOutputBytes-1)},
111 {name: "equal", body: strings.Repeat("a", maxToolOutputBytes)},
112 {name: "ascii", body: strings.Repeat("a", maxToolOutputBytes+1)},
113 {name: "chinese", body: strings.Repeat("界", maxToolOutputBytes)},
114 {name: "emoji", body: strings.Repeat("🧪", maxToolOutputBytes)},
115 {name: "error-tail", body: strings.Repeat("trace\n", 9000) + "fatal: unique error tail"},
116 }
117 for _, tc := range tests {
118 t.Run(tc.name, func(t *testing.T) {
119 callID := strings.Repeat("调用-🧪", 100)
120 got, notice := truncateToolOutputFor(tc.body, strings.Repeat("tool-界", 100), callID)
121 if len(tc.body) <= maxToolOutputBytes {
122 if got != tc.body || notice != "" {
123 t.Fatalf("under-bound result changed: bytes=%d notice=%q", len(got), notice)
124 }
125 return
126 }
127 if len(got) > maxToolOutputBytes || !utf8.ValidString(got) {
128 t.Fatalf("bounded result invalid: bytes=%d utf8=%v", len(got), utf8.ValidString(got))
129 }
130 ref := toolResultRef(callID, tc.body)
131 for _, want := range []string{"result_ref=" + ref, "original_bytes=", "kept_bytes=", "session:tool_result", "use_capability", "narrower arguments"} {
132 if !strings.Contains(got, want) {
133 t.Fatalf("recovery marker missing %q: %.1000s", want, got)
134 }
135 }
136 if tc.name == "error-tail" && !strings.Contains(got, "fatal: unique error tail") {
137 t.Fatal("error-oriented truncation lost the diagnostic tail")
138 }
139 })
140 }
141 }
142
143 func toolResultMustJSON(t *testing.T, value any) []byte {
144 t.Helper()
145 b, err := json.Marshal(value)
146 if err != nil {
147 t.Fatal(err)
148 }
149 return b
150 }
151
152 func TestSessionToolResultRequiresExactRefForDuplicateCallID(t *testing.T) {
153 first := strings.Repeat("first", 8000)
154 second := strings.Repeat("second", 8000)
155 session := &Session{Messages: []provider.Message{
156 {Role: provider.RoleTool, Name: "bash", ToolCallID: "repeat", Content: "bounded-1", RawContent: first},
157 {Role: provider.RoleTool, Name: "bash", ToolCallID: "repeat", Content: "bounded-2", RawContent: second},
158 }}
159 _, proxy := newToolResultCapabilityAgent(t, session)
160
161 if _, _, err := executeToolResultPage(t, proxy, "repeat", "", 0, 128); err == nil || !strings.Contains(err.Error(), "ambiguous") || !strings.Contains(err.Error(), toolResultRef("repeat", first)) {
162 t.Fatalf("missing deterministic ambiguity error: %v", err)
163 }
164 _, page, err := executeToolResultPage(t, proxy, "repeat", toolResultRef("repeat", first), 0, 128)
165 if err != nil || page != first[:128] {
166 t.Fatalf("exact ref selected wrong duplicate: page=%q err=%v", page, err)
167 }
168 }
169
170 func TestSessionToolResultValidatesLegacyAndPagingErrors(t *testing.T) {
171 fullLost := strings.Repeat("lost-new-result", 3000)
172 boundedLost, notice := truncateToolOutputFor(fullLost, "read_file", "lost-new")
173 if notice == "" {
174 t.Fatal("new lost-result fixture was not truncated")
175 }
176 session := &Session{Messages: []provider.Message{
177 {Role: provider.RoleTool, Name: "read_file", ToolCallID: "complete", Content: "完整结果"},
178 {Role: provider.RoleTool, Name: "read_file", ToolCallID: "lost", Content: "…[truncated tool=read_file call_id=lost]…"},
179 {Role: provider.RoleTool, Name: "read_file", ToolCallID: "lost-new", Content: boundedLost},
180 }}
181 _, proxy := newToolResultCapabilityAgent(t, session)
182
183 _, page, err := executeToolResultPage(t, proxy, "complete", "", 0, 1024)
184 if err != nil || page != "完整结果" {
185 t.Fatalf("legacy complete result = %q err=%v", page, err)
186 }
187 if _, _, err := executeToolResultPage(t, proxy, "lost", "", 0, 1024); err == nil || !strings.Contains(err.Error(), "full result is unavailable") {
188 t.Fatalf("legacy truncated result error = %v", err)
189 }
190 fullLostRef := toolResultRef("lost-new", fullLost)
191 if _, _, err := executeToolResultPage(t, proxy, "lost-new", "", 0, 1024); err == nil || !strings.Contains(err.Error(), fullLostRef) {
192 t.Fatalf("new truncated result did not request its marker ref: %v", err)
193 }
194 if _, _, err := executeToolResultPage(t, proxy, "lost-new", fullLostRef, 0, 1024); err == nil || !strings.Contains(err.Error(), "full result is unavailable") {
195 t.Fatalf("new truncated result with missing RawContent error = %v", err)
196 }
197 if _, _, err := executeToolResultPage(t, proxy, "complete", "", 1, 1024); err == nil || !strings.Contains(err.Error(), "UTF-8 character boundary") {
198 t.Fatalf("invalid UTF-8 offset error = %v", err)
199 }
200 if _, _, err := executeToolResultPage(t, proxy, "complete", "", 0, toolResultPageMaxBytes+1); err == nil || !strings.Contains(err.Error(), "limit must be") {
201 t.Fatalf("invalid limit error = %v", err)
202 }
203 if _, _, err := executeToolResultPage(t, proxy, "missing", "", 0, 10); err == nil || !strings.Contains(err.Error(), "was not found") {
204 t.Fatalf("missing result error = %v", err)
205 }
206 }
207
208 func TestSessionToolResultRejectsSubagentReferences(t *testing.T) {
209 _, proxy := newToolResultCapabilityAgent(t, &Session{})
210 if _, _, err := executeToolResultPage(t, proxy, "sa_child", "", 0, 32); err == nil || !strings.Contains(err.Error(), "read_subagent_result") {
211 t.Fatalf("subagent tool call id error = %v, want dedicated reader guidance", err)
212 }
213 if _, _, err := executeToolResultPage(t, proxy, "tool-call", "sa_child", 0, 32); err == nil || !strings.Contains(err.Error(), "read_subagent_result") {
214 t.Fatalf("subagent result ref error = %v, want dedicated reader guidance", err)
215 }
216 }
217
218 func TestSessionToolResultBindingTracksSetSessionAndCloneIsIsolated(t *testing.T) {
219 parent := &Session{Messages: []provider.Message{{Role: provider.RoleTool, ToolCallID: "call", Name: "bash", Content: "parent"}}}
220 a, proxy := newToolResultCapabilityAgent(t, parent)
221 clone := proxy.CloneForAgent(nil, nil)
222 if clone.currentToolResultTarget() != nil {
223 t.Fatal("CloneForAgent inherited the parent session reader")
224 }
225 childRegistry := tool.NewRegistry()
226 childRegistry.Add(clone)
227 _ = New(nil, childRegistry, &Session{Messages: []provider.Message{{Role: provider.RoleTool, ToolCallID: "call", Name: "bash", Content: "child"}}}, Options{}, event.Discard)
228
229 _, page, err := executeToolResultPage(t, proxy, "call", "", 0, 32)
230 if err != nil || page != "parent" {
231 t.Fatalf("parent page=%q err=%v", page, err)
232 }
233 a.SetSession(&Session{Messages: []provider.Message{{Role: provider.RoleTool, ToolCallID: "call", Name: "bash", Content: "replacement"}}})
234 _, page, err = executeToolResultPage(t, proxy, "call", "", 0, 32)
235 if err != nil || page != "replacement" {
236 t.Fatalf("replacement page=%q err=%v", page, err)
237 }
238 _, page, err = executeToolResultPage(t, clone, "call", "", 0, 32)
239 if err != nil || page != "child" {
240 t.Fatalf("child page=%q err=%v", page, err)
241 }
242 }
243
244 func TestSessionToolResultBindingDoesNotChangeUseCapabilitySchema(t *testing.T) {
245 reg := tool.NewRegistry()
246 proxy := NewUseCapabilityTool(context.Background(), nil, nil, reg, nil, nil, nil)
247 reg.Add(proxy)
248 beforeName, beforeDescription := proxy.Name(), proxy.Description()
249 beforeSchema := append([]byte(nil), proxy.Schema()...)
250 beforeProviderSchemas := reg.Schemas()
251
252 _ = New(nil, reg, NewSession("system"), Options{}, event.Discard)
253 if proxy.Name() != beforeName || proxy.Description() != beforeDescription || !reflect.DeepEqual(proxy.Schema(), json.RawMessage(beforeSchema)) {
254 t.Fatal("session reader binding changed the stable use_capability contract")
255 }
256 if !reflect.DeepEqual(reg.Schemas(), beforeProviderSchemas) {
257 t.Fatal("session reader binding changed provider tool schemas")
258 }
259 list, err := proxy.Execute(context.Background(), json.RawMessage(`{"action":"list"}`))
260 if err != nil || !strings.Contains(list, sessionToolResultCapabilityID) {
261 t.Fatalf("bound capability missing from list: err=%v list=%s", err, list)
262 }
263 inspectArgs := json.RawMessage(`{"action":"inspect","capability_id":"session:tool_result"}`)
264 inspect, err := proxy.Execute(context.Background(), inspectArgs)
265 if err != nil || !strings.Contains(inspect, `"limit_max": 24576`) {
266 t.Fatalf("inspect result: err=%v out=%s", err, inspect)
267 }
268 }
269
270 func TestRestrictedCapabilityProxyListsAndReadsOnlyOwnToolResults(t *testing.T) {
271 inner := NewUseCapabilityTool(context.Background(), nil, nil, tool.NewRegistry(), nil, nil, nil)
272 resolver := tool.CallResolver(inner)
273 proxy := &restrictedCapabilityProxy{
274 Tool: inner, resolver: resolver,
275 allowed: map[string]bool{"mcp-server:allowed": true}, servers: map[string]bool{"allowed": true},
276 }
277 reg := tool.NewRegistry()
278 reg.Add(proxy)
279 _ = New(nil, reg, &Session{Messages: []provider.Message{{Role: provider.RoleTool, Name: "read", ToolCallID: "own", Content: "own result"}}}, Options{}, event.Discard)
280
281 list, err := proxy.Execute(context.Background(), json.RawMessage(`{"action":"list"}`))
282 if err != nil || !strings.Contains(list, sessionToolResultCapabilityID) {
283 t.Fatalf("restricted list omitted session capability: err=%v list=%s", err, list)
284 }
285 args := json.RawMessage(`{"action":"call","capability_id":"session:tool_result","arguments":{"tool_call_id":"own"}}`)
286 out, err := proxy.Execute(context.Background(), args)
287 if err != nil || !strings.HasSuffix(out, "\nown result") {
288 t.Fatalf("restricted self read: err=%v out=%q", err, out)
289 }
290 }
291
292 func TestRestrictedCapabilityFrontendCloneKeepsAgentSessionsIsolated(t *testing.T) {
293 parentInner := NewUseCapabilityTool(context.Background(), nil, nil, tool.NewRegistry(), nil, nil, nil)
294 parentProxy := &restrictedCapabilityProxy{
295 Tool: parentInner, resolver: parentInner,
296 allowed: map[string]bool{"mcp-server:allowed": true}, servers: map[string]bool{"allowed": true},
297 }
298 parentRegistry := tool.NewRegistry()
299 parentRegistry.Add(parentProxy)
300 _ = New(nil, parentRegistry, &Session{Messages: []provider.Message{{
301 Role: provider.RoleTool, ToolCallID: "call", Name: "read", Content: "parent",
302 }}}, Options{}, event.Discard)
303
304 childTool := newSubagentCapabilityFrontend(parentRegistry, nil)
305 childProxy, ok := childTool.(*restrictedCapabilityProxy)
306 if !ok {
307 t.Fatalf("child frontend type = %T, want *restrictedCapabilityProxy", childTool)
308 }
309 if childProxy == parentProxy || childProxy.Tool == parentProxy.Tool {
310 t.Fatal("child restricted frontend shares the parent's session-bindable proxy")
311 }
312 childRegistry := tool.NewRegistry()
313 childRegistry.Add(childProxy)
314 _ = New(nil, childRegistry, &Session{Messages: []provider.Message{{
315 Role: provider.RoleTool, ToolCallID: "call", Name: "read", Content: "child",
316 }}}, Options{}, event.Discard)
317
318 _, parentPage, parentErr := executeToolResultPage(t, parentProxy, "call", "", 0, 32)
319 _, childPage, childErr := executeToolResultPage(t, childProxy, "call", "", 0, 32)
320 if parentErr != nil || parentPage != "parent" {
321 t.Fatalf("parent page=%q err=%v", parentPage, parentErr)
322 }
323 if childErr != nil || childPage != "child" {
324 t.Fatalf("child page=%q err=%v", childPage, childErr)
325 }
326 childProxy.allowed["mcp-server:child-only"] = true
327 if parentProxy.allowed["mcp-server:child-only"] {
328 t.Fatal("child clone shares the parent's mutable capability allowlist")
329 }
330 beforeRegistry := tool.NewRegistry()
331 beforeRegistry.Add(parentProxy)
332 afterRegistry := tool.NewRegistry()
333 afterRegistry.Add(childProxy)
334 if parentProxy.Name() != childProxy.Name() || parentProxy.Description() != childProxy.Description() ||
335 !reflect.DeepEqual(parentProxy.Schema(), childProxy.Schema()) ||
336 !reflect.DeepEqual(beforeRegistry.Schemas(), afterRegistry.Schemas()) {
337 t.Fatal("cloning or binding changed provider-visible use_capability bytes")
338 }
339 }
340
341 func TestPathBoundCapabilityFrontendBindsAndClonesAgentSessions(t *testing.T) {
342 for _, restricted := range []bool{false, true} {
343 name := "unrestricted"
344 if restricted {
345 name = "restricted"
346 }
347 t.Run(name, func(t *testing.T) {
348 root := t.TempDir()
349 claim, err := NormalizeWritePaths(root, []string{"frontend"})
350 if err != nil {
351 t.Fatal(err)
352 }
353 baseRegistry := tool.NewRegistry()
354 inner := NewUseCapabilityTool(context.Background(), nil, nil, baseRegistry, nil, nil, nil)
355 var frontend tool.Tool = inner
356 if restricted {
357 frontend = &restrictedCapabilityProxy{
358 Tool: inner, resolver: inner,
359 allowed: map[string]bool{"mcp-server:allowed": true}, servers: map[string]bool{"allowed": true},
360 }
361 }
362 baseRegistry.Add(frontend)
363 parentRegistry, removed := BindWritePaths(baseRegistry, claim, root, false)
364 if len(removed) != 0 {
365 t.Fatalf("path-bound registry removed tools: %v", removed)
366 }
367 parentTool, ok := parentRegistry.Get("use_capability")
368 if !ok {
369 t.Fatal("path-bound registry missing use_capability")
370 }
371 _ = New(nil, parentRegistry, &Session{Messages: []provider.Message{{
372 Role: provider.RoleTool, ToolCallID: "call", Name: "read", Content: "parent",
373 }}}, Options{}, event.Discard)
374
375 childTool := newSubagentCapabilityFrontend(parentRegistry, nil)
376 if childTool == nil {
377 t.Fatal("path-bound child frontend was dropped during clone")
378 }
379 childRegistry := tool.NewRegistry()
380 childRegistry.Add(childTool)
381 _ = New(nil, childRegistry, &Session{Messages: []provider.Message{{
382 Role: provider.RoleTool, ToolCallID: "call", Name: "read", Content: "child",
383 }}}, Options{}, event.Discard)
384
385 _, parentPage, parentErr := executeToolResultPage(t, parentTool, "call", "", 0, 32)
386 _, childPage, childErr := executeToolResultPage(t, childTool, "call", "", 0, 32)
387 if parentErr != nil || parentPage != "parent" {
388 t.Fatalf("path-bound parent page=%q err=%v", parentPage, parentErr)
389 }
390 if childErr != nil || childPage != "child" {
391 t.Fatalf("path-bound child page=%q err=%v", childPage, childErr)
392 }
393 if parentTool.Name() != childTool.Name() || parentTool.Description() != childTool.Description() ||
394 !reflect.DeepEqual(parentTool.Schema(), childTool.Schema()) ||
395 !reflect.DeepEqual(parentRegistry.Schemas(), childRegistry.Schemas()) {
396 t.Fatal("path-bound cloning or binding changed provider-visible use_capability bytes")
397 }
398 })
399 }
400 }
401
402 func TestPlannerRestrictedCapabilityFrontendCloneKeepsExecutorSession(t *testing.T) {
403 inner := NewUseCapabilityTool(context.Background(), nil, nil, tool.NewRegistry(), nil, nil, nil)
404 executorProxy := &restrictedCapabilityProxy{
405 Tool: inner, resolver: inner,
406 allowed: map[string]bool{"mcp-server:allowed": true}, servers: map[string]bool{"allowed": true},
407 }
408 parent := tool.NewRegistry()
409 parent.Add(executorProxy)
410 _ = New(nil, parent, &Session{Messages: []provider.Message{{
411 Role: provider.RoleTool, ToolCallID: "call", Name: "read", Content: "executor",
412 }}}, Options{}, event.Discard)
413
414 plannerRegistry := PlannerToolRegistry(parent)
415 plannerTool, ok := plannerRegistry.Get("use_capability")
416 if !ok {
417 t.Fatal("planner missing use_capability")
418 }
419 plannerProxy, ok := plannerTool.(*restrictedCapabilityProxy)
420 if !ok || plannerProxy == executorProxy || plannerProxy.Tool == executorProxy.Tool {
421 t.Fatalf("planner frontend was not deeply cloned: %T", plannerTool)
422 }
423 _ = New(nil, plannerRegistry, &Session{Messages: []provider.Message{{
424 Role: provider.RoleTool, ToolCallID: "call", Name: "read", Content: "planner",
425 }}}, Options{}, event.Discard)
426
427 _, executorPage, executorErr := executeToolResultPage(t, executorProxy, "call", "", 0, 32)
428 _, plannerPage, plannerErr := executeToolResultPage(t, plannerProxy, "call", "", 0, 32)
429 if executorErr != nil || executorPage != "executor" {
430 t.Fatalf("executor page=%q err=%v", executorPage, executorErr)
431 }
432 if plannerErr != nil || plannerPage != "planner" {
433 t.Fatalf("planner page=%q err=%v", plannerPage, plannerErr)
434 }
435 }
436
437 func TestProviderRequestsNeverUploadToolRawContent(t *testing.T) {
438 const rawSentinel = "RAW-SENTINEL-MUST-STAY-LOCAL"
439 msgs := []provider.Message{
440 {Role: provider.RoleSystem, Content: "system"},
441 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "call-1", Name: "read", Arguments: `{}`}}},
442 {Role: provider.RoleTool, ToolCallID: "call-1", Name: "read", Content: "bounded", RawContent: rawSentinel},
443 }
444 a := New(nil, tool.NewRegistry(), &Session{Messages: msgs}, Options{}, event.Discard)
445 req := a.summaryRequest(msgs, "")
446 b, err := json.Marshal(req)
447 if err != nil {
448 t.Fatal(err)
449 }
450 if strings.Contains(string(b), rawSentinel) || !strings.Contains(string(b), "bounded") {
451 t.Fatalf("summary request leaked RawContent: %s", b)
452 }
453 projected := buildVisibleCompressionProjection(msgs, visibleCompressionPlan{foldMask: make([]bool, len(msgs)), firstFold: -1}, "")
454 b, _ = json.Marshal(projected)
455 if strings.Contains(string(b), rawSentinel) || !strings.Contains(string(b), "bounded") {
456 t.Fatalf("projection leaked RawContent: %s", b)
457 }
458 }
459
460 func TestSessionToolResultRejectsPageThatSplitsUTF8Rune(t *testing.T) {
461 session := &Session{Messages: []provider.Message{{Role: provider.RoleTool, Name: "read", ToolCallID: "utf8", Content: "界"}}}
462 _, proxy := newToolResultCapabilityAgent(t, session)
463 if _, _, err := executeToolResultPage(t, proxy, "utf8", "", 0, 1); err == nil || !strings.Contains(err.Error(), "increase limit") {
464 t.Fatalf("split-rune limit error = %v", err)
465 }
466 }
467
468 func TestSessionToolResultSHA256MatchesCompleteBody(t *testing.T) {
469 body := strings.Repeat("hash-界", 100)
470 session := &Session{Messages: []provider.Message{{Role: provider.RoleTool, Name: "read", ToolCallID: "hash", Content: body}}}
471 _, proxy := newToolResultCapabilityAgent(t, session)
472 header, page, err := executeToolResultPage(t, proxy, "hash", "", 0, toolResultPageMaxBytes)
473 if err != nil || page != body {
474 t.Fatalf("page=%q err=%v", page, err)
475 }
476 digest := sha256.Sum256([]byte(body))
477 if !bytes.Equal(mustDecodeHex(t, header.SHA256), digest[:]) {
478 t.Fatalf("sha256=%q does not match complete body", header.SHA256)
479 }
480 }
481
482 func mustDecodeHex(t *testing.T, value string) []byte {
483 t.Helper()
484 b, err := hex.DecodeString(value)
485 if err != nil {
486 t.Fatal(err)
487 }
488 return b
489 }
490
490 lines GO