返回 DeepSeek-Reasonix
subagent_model_test.go
根目录 / internal / boot / subagent_model_test.go
1 package boot
2
3 import (
4 "context"
5 "path/filepath"
6 "testing"
7 "time"
8
9 "reasonix/internal/ablation"
10 "reasonix/internal/agent"
11 "reasonix/internal/billing"
12 "reasonix/internal/config"
13 "reasonix/internal/event"
14 "reasonix/internal/provider"
15 "reasonix/internal/skill"
16 "reasonix/internal/tool"
17 )
18
19 type staticResolver struct {
20 catalog []provider.Descriptor
21 }
22
23 func (r *staticResolver) Catalog() []provider.Descriptor { return r.catalog }
24 func (r *staticResolver) Resolve(provider.Selection) (provider.Provider, error) {
25 return nil, nil
26 }
27
28 func TestSubagentModelRefUsesConfiguredDefault(t *testing.T) {
29 cfg := config.Default()
30 cfg.Agent.SubagentModel = "deepseek-pro"
31
32 got := subagentModelRef(cfg, skill.Skill{Name: "explore", RunAs: skill.RunSubagent})
33 if got != "deepseek-pro" {
34 t.Fatalf("subagent model = %q, want deepseek-pro", got)
35 }
36 }
37
38 func TestSubagentModelRefHonorsPrecedence(t *testing.T) {
39 cfg := config.Default()
40 cfg.Agent.SubagentModel = "mimo-pro"
41 cfg.Agent.SubagentModels = map[string]string{"review": "deepseek-pro"}
42
43 got := subagentModelRef(cfg, skill.Skill{
44 Name: "review",
45 RunAs: skill.RunSubagent,
46 Model: "mimo-flash",
47 })
48 if got != "deepseek-pro" {
49 t.Fatalf("per-skill config should override skill frontmatter and default, got %q", got)
50 }
51
52 got = subagentModelRef(cfg, skill.Skill{
53 Name: "custom",
54 RunAs: skill.RunSubagent,
55 Model: "mimo-flash",
56 })
57 if got != "mimo-flash" {
58 t.Fatalf("skill frontmatter should override default config, got %q", got)
59 }
60 }
61
62 func TestSubagentModelRefAcceptsToolNameAliases(t *testing.T) {
63 cfg := config.Default()
64 cfg.Agent.SubagentModels = map[string]string{"security_review": "deepseek-pro"}
65
66 got := subagentModelRef(cfg, skill.Skill{Name: "security-review", RunAs: skill.RunSubagent})
67 if got != "deepseek-pro" {
68 t.Fatalf("security_review alias should configure security-review, got %q", got)
69 }
70 }
71
72 func TestSubagentEffortRefHonorsPrecedence(t *testing.T) {
73 cfg := config.Default()
74 cfg.Agent.SubagentEffort = "high"
75 cfg.Agent.SubagentEfforts = map[string]string{"review": "max"}
76
77 got := subagentEffortRef(cfg, skill.Skill{
78 Name: "review",
79 RunAs: skill.RunSubagent,
80 Effort: "low",
81 })
82 if got != "max" {
83 t.Fatalf("per-skill effort config should override skill frontmatter and default, got %q", got)
84 }
85
86 got = subagentEffortRef(cfg, skill.Skill{
87 Name: "custom",
88 RunAs: skill.RunSubagent,
89 Effort: "medium",
90 })
91 if got != "medium" {
92 t.Fatalf("skill frontmatter effort should override default config, got %q", got)
93 }
94
95 got = subagentEffortRef(cfg, skill.Skill{Name: "other", RunAs: skill.RunSubagent})
96 if got != "high" {
97 t.Fatalf("default subagent effort = %q, want high", got)
98 }
99 }
100
101 func TestSubagentEffortRefAcceptsToolNameAliases(t *testing.T) {
102 cfg := config.Default()
103 cfg.Agent.SubagentEfforts = map[string]string{"security_review": "max"}
104
105 got := subagentEffortRef(cfg, skill.Skill{Name: "security-review", RunAs: skill.RunSubagent})
106 if got != "max" {
107 t.Fatalf("security_review alias should configure security-review effort, got %q", got)
108 }
109 }
110
111 func TestSubagentEffectiveIdentityUsesResolvedModelAndEffort(t *testing.T) {
112 cfg := config.Default()
113 cfg.Providers = []config.ProviderEntry{{
114 Name: "custom",
115 Kind: "openai",
116 Models: []string{"alpha", "beta"},
117 Default: "beta",
118 SupportedEfforts: []string{"low", "high"},
119 DefaultEffort: "high",
120 }}
121 base, ok := cfg.ResolveModel("custom")
122 if !ok {
123 t.Fatal("custom provider should resolve")
124 }
125
126 model, effort := subagentEffectiveIdentity(cfg, nil, "custom", base, "", "")
127 if model != "custom/beta" || effort != "high" {
128 t.Fatalf("identity = %q/%q, want custom/beta/high", model, effort)
129 }
130
131 model, effort = subagentEffectiveIdentity(cfg, nil, "custom", base, "alpha", "low")
132 if model != "custom/alpha" || effort != "low" {
133 t.Fatalf("override identity = %q/%q, want custom/alpha/low", model, effort)
134 }
135 }
136
137 func TestSubagentEffectiveIdentityUsesAuthoritativeExternalResolver(t *testing.T) {
138 cfg := config.Default()
139 base := &config.ProviderEntry{Name: "openai", Model: "gpt"}
140 resolver := &staticResolver{catalog: []provider.Descriptor{{
141 Ref: "anthropic/claude-sonnet", DisplayName: "anthropic", Model: "claude-sonnet",
142 Efforts: []string{"low", "high"}, DefaultEffort: "high",
143 }}}
144
145 model, effort := subagentEffectiveIdentity(cfg, resolver, "openai/gpt", base, "anthropic/claude-sonnet", "high")
146 if model != "anthropic/claude-sonnet" || effort != "high" {
147 t.Fatalf("identity = %q/%q, want anthropic/claude-sonnet/high", model, effort)
148 }
149 }
150
151 func TestSubagentSkillOptionsPreserveScheduledQuoteContext(t *testing.T) {
152 ratedAt := time.Date(2026, time.August, 17, 4, 0, 0, 0, time.UTC)
153 quoteCtx := &event.QuoteContext{
154 Now: func() time.Time { return ratedAt },
155 PricingContextForModel: func(modelRef string) billing.PricingContext {
156 if modelRef != "deepseek-pro/deepseek-v4-pro" {
157 t.Fatalf("pricing context model = %q", modelRef)
158 }
159 return billing.PricingContext{
160 ProviderKind: "deepseek",
161 ModelID: "deepseek-v4-pro",
162 BillingMode: billing.BillingModePAYG,
163 ScheduleID: billing.ScheduleDeepSeekV4August2026,
164 }
165 },
166 }
167 factory := newSubagentSkillOptionsFactory(config.AgentConfig{}, quoteCtx, nil, 0, 1, ablation.Set{}, nil, nil)
168 opts := factory(context.Background(), 5, &provider.Pricing{CacheHit: 0.30, Input: 9, Output: 27, Currency: "CNY"}, 1_000_000, 1)
169 if opts.QuoteContext != quoteCtx {
170 t.Fatal("skill subagent did not retain the host QuoteContext")
171 }
172
173 quote := event.EnsureCostQuote(event.Event{
174 Kind: event.Usage,
175 ModelRef: "deepseek-pro/deepseek-v4-pro",
176 Usage: &provider.Usage{PromptTokens: 1_000_000, TotalTokens: 1_000_000},
177 Pricing: opts.Pricing,
178 UsageSource: event.UsageSourceSubagent,
179 }, opts.QuoteContext)
180 if quote == nil || quote.RateBand != billing.RateBandOffPeak || quote.Original.Amount != "4.5" {
181 t.Fatalf("skill subagent quote = %+v, want CNY 4.5 off-peak", quote)
182 }
183 }
184
185 func TestNewSubagentStoreRequiresSessionDir(t *testing.T) {
186 if got, err := newSubagentStore("", nil); err != nil || got != nil {
187 if err != nil {
188 t.Fatalf("empty session dir error = %v", err)
189 }
190 t.Fatalf("empty session dir should disable subagent store, got %#v", got)
191 }
192 if got, err := newSubagentStore(t.TempDir(), nil); err != nil || got == nil {
193 if err != nil {
194 t.Fatalf("non-empty session dir error = %v", err)
195 }
196 t.Fatal("non-empty session dir should create subagent store")
197 }
198 }
199
200 func TestNewSubagentStoreCleansStaleRunningRefs(t *testing.T) {
201 sessionDir := t.TempDir()
202 store := agent.NewSubagentStore(filepath.Join(sessionDir, "subagents"))
203 spec := agent.SubagentSpec{
204 Kind: "task",
205 Name: "task",
206 WorkspaceRoot: t.TempDir(),
207 ParentSession: "parent-session",
208 SystemPrompt: "sys",
209 Registry: tool.NewRegistry(),
210 Model: "base-model",
211 }
212 run, err := store.PrepareFresh(spec)
213 if err != nil {
214 t.Fatalf("PrepareFresh: %v", err)
215 }
216 if err := store.MarkRunning(run); err != nil {
217 t.Fatalf("MarkRunning: %v", err)
218 }
219 ref := run.Ref
220 run.Release()
221
222 got, err := newSubagentStore(sessionDir, nil)
223 if err != nil {
224 t.Fatalf("newSubagentStore: %v", err)
225 }
226 if got == nil {
227 t.Fatal("newSubagentStore returned nil")
228 }
229 meta, err := got.LoadMeta(ref)
230 if err != nil {
231 t.Fatalf("LoadMeta: %v", err)
232 }
233 if meta.Status != agent.SubagentInterrupted {
234 t.Fatalf("status = %q, want interrupted", meta.Status)
235 }
236 }
237
237 lines GO