返回 DeepSeek-Reasonix
task_options_test.go
根目录 / internal / agent / task_options_test.go
1 package agent
2
3 import (
4 "reflect"
5 "strings"
6 "testing"
7
8 "reasonix/internal/provider"
9 "reasonix/internal/tool"
10 )
11
12 func TestNewTaskToolWithOptionsMatchesPositional(t *testing.T) {
13 prov := &mockProvider{name: "sub"}
14 pricing := &provider.Pricing{Input: 1.5, Output: 2.5}
15 reg := tool.NewRegistry()
16 gate := &stubGate{}
17 resolve := func(model, effort string) (provider.Provider, *provider.Pricing, int, error) {
18 return &mockProvider{name: "resolved-" + model}, pricing, 8192, nil
19 }
20
21 cases := []struct {
22 name string
23 opts TaskToolOptions
24 }{
25 {
26 name: "empty-sys-prompt-defaults",
27 opts: TaskToolOptions{
28 Provider: prov,
29 ParentRegistry: reg,
30 MaxSteps: 20,
31 },
32 },
33 {
34 name: "zero-value-config",
35 opts: TaskToolOptions{
36 Provider: prov,
37 ParentRegistry: reg,
38 },
39 },
40 {
41 name: "non-empty-gate-and-overrides",
42 opts: TaskToolOptions{
43 Provider: prov,
44 Pricing: pricing,
45 ParentRegistry: reg,
46 MaxSteps: 12,
47 ContextWindow: 64000,
48 RecentKeep: 7,
49 SoftCompactRatio: 0.55,
50 ToolResultSnipRatio: 0.4,
51 CompactRatio: 0.8,
52 CompactForceRatio: 0.95,
53 Temperature: 0.2,
54 ArchiveDir: t.TempDir(),
55 SysPrompt: "custom sub-agent prompt",
56 Gate: gate,
57 KeepPolicy: KeepErrors | KeepUserMarked,
58 SubagentModel: "deepseek-chat",
59 SubagentEffort: "high",
60 ResolveProvider: resolve,
61 },
62 },
63 }
64
65 for _, tc := range cases {
66 t.Run(tc.name, func(t *testing.T) {
67 legacy := NewTaskTool(
68 tc.opts.Provider,
69 tc.opts.Pricing,
70 tc.opts.ParentRegistry,
71 tc.opts.MaxSteps,
72 tc.opts.ContextWindow,
73 tc.opts.RecentKeep,
74 tc.opts.SoftCompactRatio,
75 tc.opts.ToolResultSnipRatio,
76 tc.opts.CompactRatio,
77 tc.opts.CompactForceRatio,
78 tc.opts.Temperature,
79 tc.opts.ArchiveDir,
80 tc.opts.SysPrompt,
81 tc.opts.Gate,
82 tc.opts.KeepPolicy,
83 tc.opts.SubagentModel,
84 tc.opts.SubagentEffort,
85 tc.opts.ResolveProvider,
86 )
87 modern := NewTaskToolWithOptions(tc.opts)
88 assertTaskToolConfigEqual(t, legacy, modern)
89 if !reflect.DeepEqual(legacy.Schema(), modern.Schema()) {
90 t.Fatalf("schema mismatch:\nlegacy=%s\nmodern=%s", legacy.Schema(), modern.Schema())
91 }
92 if legacy.Name() != modern.Name() || legacy.Description() != modern.Description() || legacy.ReadOnly() != modern.ReadOnly() {
93 t.Fatalf("tool identity mismatch: name=%q/%q desc-len=%d/%d readOnly=%v/%v",
94 legacy.Name(), modern.Name(), len(legacy.Description()), len(modern.Description()), legacy.ReadOnly(), modern.ReadOnly())
95 }
96 })
97 }
98 }
99
100 func TestNewTaskToolWithOptionsEmptySysPromptUsesDefault(t *testing.T) {
101 task := NewTaskToolWithOptions(TaskToolOptions{
102 Provider: &mockProvider{name: "sub"},
103 ParentRegistry: tool.NewRegistry(),
104 MaxSteps: 5,
105 SysPrompt: "",
106 })
107 if task.sysPrompt != DefaultTaskSystemPrompt {
108 t.Fatalf("sysPrompt = %q, want DefaultTaskSystemPrompt", task.sysPrompt)
109 }
110 }
111
112 func TestNewTaskToolWithOptionsAndLegacyExecuteEquivalence(t *testing.T) {
113 chunks := []provider.Chunk{
114 {Type: provider.ChunkText, Text: "options-equivalent-answer"},
115 {Type: provider.ChunkDone},
116 }
117 legacyProv := &mockProvider{name: "sub", chunks: chunks}
118 modernProv := &mockProvider{name: "sub", chunks: append([]provider.Chunk(nil), chunks...)}
119 reg := tool.NewRegistry()
120 sys := "sys-for-equivalence"
121 storeDir := t.TempDir()
122 workspace := t.TempDir()
123
124 legacy := NewTaskTool(legacyProv, nil, reg, 20, 0, 0, 0, 0, 0, 0, 0.0, "", sys, nil, 0, "", "", nil).
125 WithTranscripts(NewSubagentStore(storeDir), workspace, "base-model", "base-effort")
126 modern := NewTaskToolWithOptions(TaskToolOptions{
127 Provider: modernProv,
128 ParentRegistry: reg,
129 MaxSteps: 20,
130 SysPrompt: sys,
131 }).WithTranscripts(NewSubagentStore(storeDir), workspace, "base-model", "base-effort")
132
133 legacyOut, legacyErr := legacy.Execute(testTaskContext(), []byte(`{"prompt":"equivalence prompt"}`))
134 modernOut, modernErr := modern.Execute(testTaskContext(), []byte(`{"prompt":"equivalence prompt"}`))
135 if legacyErr != nil || modernErr != nil {
136 t.Fatalf("Execute errors: legacy=%v modern=%v", legacyErr, modernErr)
137 }
138 // Transcript refs differ by id; compare semantic body and system prompt routing.
139 if !strings.Contains(legacyOut, "options-equivalent-answer") || !strings.Contains(modernOut, "options-equivalent-answer") {
140 t.Fatalf("final answers missing:\nlegacy=%q\nmodern=%q", legacyOut, modernOut)
141 }
142 if legacySys := legacyProv.lastReq.Messages[0].Content; legacySys != sys {
143 t.Fatalf("legacy system prompt = %q, want %q", legacySys, sys)
144 }
145 if modernSys := modernProv.lastReq.Messages[0].Content; modernSys != sys {
146 t.Fatalf("modern system prompt = %q, want %q", modernSys, sys)
147 }
148 if !strings.Contains(lastUser(legacyProv.lastReq), "equivalence prompt") ||
149 !strings.Contains(lastUser(modernProv.lastReq), "equivalence prompt") {
150 t.Fatalf("user prompts not routed:\nlegacy=%q\nmodern=%q", lastUser(legacyProv.lastReq), lastUser(modernProv.lastReq))
151 }
152 }
153
154 func TestNewTaskToolWithOptionsProviderResolverAndOverrides(t *testing.T) {
155 base := &mockProvider{name: "base"}
156 resolved := &mockProvider{name: "resolved-child"}
157 pricing := &provider.Pricing{Input: 3}
158 var sawModel, sawEffort string
159 resolve := func(model, effort string) (provider.Provider, *provider.Pricing, int, error) {
160 sawModel, sawEffort = model, effort
161 return resolved, pricing, 4096, nil
162 }
163 task := NewTaskToolWithOptions(TaskToolOptions{
164 Provider: base,
165 ParentRegistry: tool.NewRegistry(),
166 MaxSteps: 8,
167 SubagentModel: "child-model",
168 SubagentEffort: "max",
169 ResolveProvider: resolve,
170 })
171 gotProv, gotPrice, gotWin, err := task.resolveSubSessionRuntime("child-model", "max")
172 if err != nil {
173 t.Fatalf("resolveSubSessionRuntime: %v", err)
174 }
175 if gotProv != resolved || gotPrice != pricing || gotWin != 4096 {
176 t.Fatalf("resolver result = (%v,%v,%d), want resolved pricing/window", gotProv.Name(), gotPrice, gotWin)
177 }
178 if sawModel != "child-model" || sawEffort != "max" {
179 t.Fatalf("resolver args = (%q,%q), want child-model/max", sawModel, sawEffort)
180 }
181 if task.subagentModel != "child-model" || task.subagentEffort != "max" {
182 t.Fatalf("stored overrides = (%q,%q)", task.subagentModel, task.subagentEffort)
183 }
184 }
185
186 func assertTaskToolConfigEqual(t *testing.T, a, b *TaskTool) {
187 t.Helper()
188 if a.prov != b.prov {
189 t.Fatalf("prov mismatch")
190 }
191 if a.pricing != b.pricing {
192 t.Fatalf("pricing mismatch")
193 }
194 if a.parentReg != b.parentReg {
195 t.Fatalf("parentReg mismatch")
196 }
197 if a.maxSteps != b.maxSteps || a.contextWindow != b.contextWindow || a.recentKeep != b.recentKeep {
198 t.Fatalf("step/window/keep mismatch: %+v vs %+v",
199 [3]int{a.maxSteps, a.contextWindow, a.recentKeep},
200 [3]int{b.maxSteps, b.contextWindow, b.recentKeep})
201 }
202 if a.softCompactRatio != b.softCompactRatio || a.toolResultSnipRatio != b.toolResultSnipRatio ||
203 a.compactRatio != b.compactRatio || a.compactForceRatio != b.compactForceRatio || a.temperature != b.temperature {
204 t.Fatalf("ratio/temp mismatch")
205 }
206 if a.archiveDir != b.archiveDir || a.sysPrompt != b.sysPrompt || a.keepPolicy != b.keepPolicy {
207 t.Fatalf("archive/sys/keep mismatch: archive=%q/%q sys=%q/%q keep=%v/%v",
208 a.archiveDir, b.archiveDir, a.sysPrompt, b.sysPrompt, a.keepPolicy, b.keepPolicy)
209 }
210 if a.gate != b.gate {
211 t.Fatalf("gate mismatch")
212 }
213 if a.subagentModel != b.subagentModel || a.subagentEffort != b.subagentEffort {
214 t.Fatalf("model/effort mismatch: %q/%q vs %q/%q", a.subagentModel, a.subagentEffort, b.subagentModel, b.subagentEffort)
215 }
216 // Function pointers are compared by identity for the same options value.
217 if reflect.ValueOf(a.resolveProvider).Pointer() != reflect.ValueOf(b.resolveProvider).Pointer() {
218 t.Fatalf("resolveProvider identity mismatch")
219 }
220 if a.maxSubagentDepth != b.maxSubagentDepth {
221 t.Fatalf("maxSubagentDepth = %d/%d", a.maxSubagentDepth, b.maxSubagentDepth)
222 }
223 }
224
224 lines GO