返回 DeepSeek-Reasonix
tool_dispatch_test.go
根目录 / internal / agent / tool_dispatch_test.go
1 package agent
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "sync/atomic"
8 "testing"
9
10 "reasonix/internal/evidence"
11 "reasonix/internal/tool"
12 )
13
14 type transientOnceTool struct {
15 calls atomic.Int32
16 }
17
18 func (t *transientOnceTool) Name() string { return "read_file" }
19 func (t *transientOnceTool) Description() string { return "" }
20 func (t *transientOnceTool) Schema() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
21 func (t *transientOnceTool) ReadOnly() bool { return true }
22 func (t *transientOnceTool) Execute(context.Context, json.RawMessage) (string, error) {
23 if t.calls.Add(1) == 1 {
24 return "", errors.New("connection reset by peer")
25 }
26 return "ok", nil
27 }
28
29 func TestDispatchResolvedToolRetriesReadOnlyTransientOnce(t *testing.T) {
30 target := &transientOnceTool{}
31 a := &Agent{}
32 plan := &toolCallPlan{runTool: target, runArgs: json.RawMessage(`{}`), readOnly: true}
33 result, _, _, err := a.dispatchResolvedTool(context.Background(), plan)
34 if err != nil {
35 t.Fatalf("retry: %v", err)
36 }
37 if result != "ok" || target.calls.Load() != 2 {
38 t.Fatalf("result=%q calls=%d", result, target.calls.Load())
39 }
40 }
41
42 func TestDispatchResolvedToolDoesNotRetryWriterTransient(t *testing.T) {
43 target := &transientOnceTool{}
44 a := &Agent{}
45 plan := &toolCallPlan{
46 runTool: target, runArgs: json.RawMessage(`{}`), readOnly: true,
47 effects: evidence.ToolEffects{StateMutation: true},
48 }
49 _, _, _, err := a.dispatchResolvedTool(context.Background(), plan)
50 if err == nil || target.calls.Load() != 1 {
51 t.Fatalf("writer retry: err=%v calls=%d", err, target.calls.Load())
52 }
53 }
54
55 type fixedErrorTool struct {
56 err error
57 calls atomic.Int32
58 }
59
60 type deterministicApplicationError string
61
62 func (e deterministicApplicationError) Error() string { return string(e) }
63 func (deterministicApplicationError) RetryableToolError() bool { return false }
64
65 func (t *fixedErrorTool) Name() string { return "read_file" }
66 func (t *fixedErrorTool) Description() string { return "" }
67 func (t *fixedErrorTool) Schema() json.RawMessage { return json.RawMessage(`{"type":"object"}`) }
68 func (t *fixedErrorTool) ReadOnly() bool { return true }
69 func (t *fixedErrorTool) Execute(context.Context, json.RawMessage) (string, error) {
70 t.calls.Add(1)
71 return "", t.err
72 }
73
74 func TestDispatchResolvedToolNeverRetriesCancellationOrAmbiguousDispatch(t *testing.T) {
75 for _, tc := range []struct {
76 name string
77 err error
78 }{
79 {name: "cancelled", err: context.Canceled},
80 {name: "deadline", err: context.DeadlineExceeded},
81 {name: "may have completed", err: errors.New("connection reset after dispatch; execution may have completed and was not retried")},
82 {name: "unknown result", err: errors.New("connection closed after dispatch; execution result is unknown")},
83 {name: "typed application unavailable", err: deterministicApplicationError("resource unavailable")},
84 } {
85 t.Run(tc.name, func(t *testing.T) {
86 target := &fixedErrorTool{err: tc.err}
87 plan := &toolCallPlan{runTool: target, runArgs: json.RawMessage(`{}`), readOnly: true}
88 _, _, _, _ = (&Agent{}).dispatchResolvedTool(context.Background(), plan)
89 if got := target.calls.Load(); got != 1 {
90 t.Fatalf("calls = %d, want 1", got)
91 }
92 })
93 }
94 }
95
96 var _ tool.Tool = (*transientOnceTool)(nil)
97
97 lines GO