返回 DeepSeek-Reasonix
tool_receipts_test.go
根目录 / internal / agent / tool_receipts_test.go
1 package agent
2
3 import (
4 "context"
5 "sync"
6 "testing"
7 "time"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 "reasonix/internal/tool"
12 )
13
14 type toolReceiptSignalSink struct {
15 mu sync.Mutex
16 events []event.Event
17 previews chan event.Event
18 }
19
20 func (s *toolReceiptSignalSink) Emit(e event.Event) {
21 s.mu.Lock()
22 s.events = append(s.events, e)
23 s.mu.Unlock()
24 if e.Kind == event.ToolResultPreview {
25 s.previews <- e
26 }
27 }
28
29 func (s *toolReceiptSignalSink) kinds(kind event.Kind) []event.Event {
30 s.mu.Lock()
31 defer s.mu.Unlock()
32 var out []event.Event
33 for _, e := range s.events {
34 if e.Kind == kind {
35 out = append(out, e)
36 }
37 }
38 return out
39 }
40
41 func TestTodoResultCommitsBeforeFollowingToolWithoutPreviewState(t *testing.T) {
42 started := make(chan struct{})
43 release := make(chan struct{})
44 reg := tool.NewRegistry()
45 reg.Add(fakeTool{name: "todo_write", readOnly: true})
46 reg.Add(blockingTool{name: "slow_read", started: started, release: release})
47 sink := &toolReceiptSignalSink{previews: make(chan event.Event, 1)}
48 a := New(nil, reg, NewSession(""), Options{}, sink)
49 done := make(chan struct{})
50 go func() {
51 defer close(done)
52 a.executeBatch(context.Background(), &a.turn, []provider.ToolCall{
53 {ID: "todo-1", Name: "todo_write", Arguments: `{"todos":[{"content":"Ship the fix","status":"in_progress"}]}`},
54 {ID: "read-1", Name: "slow_read", Arguments: `{}`},
55 })
56 }()
57
58 select {
59 case <-started:
60 case <-time.After(2 * time.Second):
61 t.Fatal("later tool did not start")
62 }
63 select {
64 case preview := <-sink.previews:
65 t.Fatalf("todo state leaked through a transient preview: %+v", preview.Tool)
66 default:
67 }
68 if results := sink.kinds(event.ToolResult); len(results) != 1 || results[0].Tool.Name != "todo_write" {
69 t.Fatalf("completed todo result must be checkpointed before the next tool: %+v", results)
70 }
71
72 close(release)
73 select {
74 case <-done:
75 case <-time.After(2 * time.Second):
76 t.Fatal("batch did not finish after releasing the later tool")
77 }
78 if previews := sink.kinds(event.ToolResultPreview); len(previews) != 0 {
79 t.Fatalf("ToolResultPreview events = %d, want 0", len(previews))
80 }
81 results := sink.kinds(event.ToolResult)
82 if len(results) != 2 || results[0].Tool.ID != "todo-1" || results[1].Tool.ID != "read-1" {
83 t.Fatalf("provider-ordered ToolResult events = %+v", results)
84 }
85 }
86
86 lines GO