返回 DeepSeek-Reasonix
context_admission_test.go
根目录 / internal / agent / context_admission_test.go
1 package agent
2
3 import (
4 "encoding/json"
5 "errors"
6 "strings"
7 "testing"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 "reasonix/internal/tool"
12 )
13
14 type policyWindowProvider struct {
15 sharedWindowTestProvider
16 policy provider.ContextBudgetPolicy
17 }
18
19 func (p *policyWindowProvider) ContextBudgetPolicy() provider.ContextBudgetPolicy { return p.policy }
20
21 func TestAdmitOutputBudgetClipsIssue8909AndScreenshot(t *testing.T) {
22 cases := []struct {
23 name string
24 prompt int
25 requested int
26 want int
27 }{
28 {name: "issue8909", prompt: 810_882, requested: 354_469, want: 229_502},
29 {name: "screenshot", prompt: 917_189, requested: 245_760, want: 123_195},
30 }
31 for _, tc := range cases {
32 t.Run(tc.name, func(t *testing.T) {
33 prov := &sharedWindowTestProvider{budget: tc.requested, shared: true}
34 a := &Agent{
35 agentConfig: agentConfig{contextWindow: 1_048_576},
36 svc: agentServices{prov: prov},
37 sess: sessionRuntime{output: outputBudgetState{outputBudget: tc.requested}},
38 }
39 msgs := []provider.Message{{Role: provider.RoleUser, Content: strings.Repeat("x", 3_000_000)}}
40 a.setPromptTokenCalibration(tc.prompt, requestCalibrationShapeOf(provider.Request{Messages: msgs}))
41 adm, err := a.admitOutputBudget(provider.Request{Messages: msgs, MaxTokens: tc.requested})
42 if err != nil {
43 t.Fatal(err)
44 }
45 if !adm.Clipped || adm.EffectiveOutputTokens != tc.want || adm.PhysicalRemaining != tc.want {
46 t.Fatalf("adm=%+v, want clipped %d", adm, tc.want)
47 }
48 })
49 }
50 }
51
52 func TestAdmitOutputBudgetUsesOfficialAutoWhenConfigIsZero(t *testing.T) {
53 prov := &policyWindowProvider{sharedWindowTestProvider: sharedWindowTestProvider{shared: true}, policy: provider.ContextBudgetPolicy{
54 WindowMode: provider.ContextWindowShared,
55 AutoOutputTokens: provider.DeepSeekMaxOutputTokens,
56 MaxOutputTokens: provider.DeepSeekMaxOutputTokens,
57 LimitMode: provider.OutputLimitOmitWhenSafe,
58 }}
59 a := &Agent{agentConfig: agentConfig{contextWindow: 1_048_576}, svc: agentServices{prov: prov}}
60 msgs := []provider.Message{{Role: provider.RoleUser, Content: strings.Repeat("x", 3_000_000)}}
61 a.setPromptTokenCalibration(810_882, requestCalibrationShapeOf(provider.Request{Messages: msgs}))
62 req := provider.Request{Messages: msgs, MaxTokens: 0}
63 if err := a.applyAdmissionToRequest(&req); err != nil {
64 t.Fatal(err)
65 }
66 if req.MaxTokens != 229_502 {
67 t.Fatalf("auto official clip = %d, want 229502", req.MaxTokens)
68 }
69 }
70
71 func TestAdmitOutputBudgetOmitsWhenSafeHasRoom(t *testing.T) {
72 prov := &policyWindowProvider{policy: provider.ContextBudgetPolicy{
73 WindowMode: provider.ContextWindowShared,
74 AutoOutputTokens: provider.DeepSeekMaxOutputTokens,
75 MaxOutputTokens: provider.DeepSeekMaxOutputTokens,
76 LimitMode: provider.OutputLimitOmitWhenSafe,
77 }}
78 a := &Agent{agentConfig: agentConfig{contextWindow: 1_048_576}, svc: agentServices{prov: prov}}
79 req := provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}}
80 if err := a.applyAdmissionToRequest(&req); err != nil {
81 t.Fatal(err)
82 }
83 if req.MaxTokens != 0 {
84 t.Fatalf("safe omit injected %d", req.MaxTokens)
85 }
86 }
87
88 func TestAdmitOutputBudgetAlwaysSendsOpenCodeLimit(t *testing.T) {
89 prov := &policyWindowProvider{policy: provider.ContextBudgetPolicy{
90 WindowMode: provider.ContextWindowShared,
91 AutoOutputTokens: 131_072,
92 MaxOutputTokens: 131_072,
93 LimitMode: provider.OutputLimitAlways,
94 }}
95 a := &Agent{agentConfig: agentConfig{contextWindow: 1_048_576}, svc: agentServices{prov: prov}}
96 req := provider.Request{Messages: []provider.Message{{Role: provider.RoleUser, Content: "hi"}}, MaxTokens: 0}
97 if err := a.applyAdmissionToRequest(&req); err != nil {
98 t.Fatal(err)
99 }
100 if req.MaxTokens != 131_072 {
101 t.Fatalf("OpenCode always send = %d, want 131072", req.MaxTokens)
102 }
103 }
104
105 func TestAdmitOutputBudgetNegativeOmitsInsteadOfInject(t *testing.T) {
106 prov := &policyWindowProvider{policy: provider.ContextBudgetPolicy{
107 WindowMode: provider.ContextWindowShared,
108 AutoOutputTokens: provider.DeepSeekMaxOutputTokens,
109 LimitMode: provider.OutputLimitOmitWhenSafe,
110 }}
111 a := &Agent{agentConfig: agentConfig{contextWindow: 1_048_576}, svc: agentServices{prov: prov}}
112 msgs := []provider.Message{{Role: provider.RoleUser, Content: strings.Repeat("x", 3_000_000)}}
113 a.setPromptTokenCalibration(810_882, requestCalibrationShapeOf(provider.Request{Messages: msgs}))
114 _, err := a.admitOutputBudget(provider.Request{Messages: msgs, MaxTokens: -1})
115 if !errors.Is(err, ErrCompactionRequired) {
116 t.Fatalf("negative omit err = %v, want compaction", err)
117 }
118 }
119
120 func TestCompactRatioIndependentOfOutputBudgetAndLearnedWindow(t *testing.T) {
121 a := &Agent{
122 svc: agentServices{prov: &sharedWindowTestProvider{budget: 131_072, shared: true}},
123 agentConfig: agentConfig{contextWindow: 128_000, compactRatio: defaultCompactRatio},
124 }
125 want := int(float64(128_000) * defaultCompactRatio)
126 if got := a.compactTrigger(); got != want {
127 t.Fatalf("trigger = %d, want %d", got, want)
128 }
129 a.sess.output.learned.Store(&learnedContextBudget{windowTokens: 64_000})
130 if got := a.compactTrigger(); got != int(float64(64_000)*defaultCompactRatio) {
131 t.Fatalf("learned trigger = %d", got)
132 }
133 a.sess.output.learned.Store(&learnedContextBudget{windowTokens: 64_000})
134 if a.compactRatio != defaultCompactRatio {
135 t.Fatal("compact_ratio must stay user-owned")
136 }
137 }
138
139 func TestGuardedSummaryUsesSharedPolicyWhenAutoBudgetIsZero(t *testing.T) {
140 prov := &policyWindowProvider{policy: provider.ContextBudgetPolicy{WindowMode: provider.ContextWindowShared, LimitMode: provider.OutputLimitOmitWhenSafe}}
141 a := &Agent{agentConfig: agentConfig{contextWindow: 100_000}, svc: agentServices{prov: prov, sink: event.Discard}}
142 fold := []provider.Message{{Role: provider.RoleUser, Content: strings.Repeat("字", 80_000)}}
143 if budget := a.summaryInputBudget(""); budget <= 0 {
144 t.Fatalf("shared auto-zero summary budget = %d", budget)
145 }
146 if got := a.guardedSummaryInputTokens(fold); got <= 0 {
147 t.Fatalf("guarded tokens = %d", got)
148 }
149 }
150
151 func TestLearnedWindowIsolatedPerAgent(t *testing.T) {
152 first := &Agent{agentConfig: agentConfig{contextWindow: 1_048_576}}
153 second := &Agent{agentConfig: agentConfig{contextWindow: 1_048_576}}
154 first.learnContextBudget(200_000, 0, false)
155 if first.effectiveContextWindow() != 200_000 {
156 t.Fatalf("first window = %d", first.effectiveContextWindow())
157 }
158 if second.effectiveContextWindow() != 1_048_576 {
159 t.Fatalf("second agent inherited learned window %d", second.effectiveContextWindow())
160 }
161 }
162
163 func TestZeroConfigWindowUsesLearned(t *testing.T) {
164 a := &Agent{}
165 a.learnContextBudget(262_144, 0, false)
166 if a.effectiveContextWindow() != 262_144 {
167 t.Fatalf("zero config window = %d", a.effectiveContextWindow())
168 }
169 }
170
171 func TestForkCaptureForwardsContextBudgetPolicy(t *testing.T) {
172 t.Setenv("REASONIX_EXPERIMENT_FORK_CAPTURE_DIR", t.TempDir())
173 inner := &policyWindowProvider{policy: provider.ContextBudgetPolicy{
174 WindowMode: provider.ContextWindowShared, AutoOutputTokens: 384_000, LimitMode: provider.OutputLimitOmitWhenSafe,
175 }}
176 a := New(inner, tool.NewRegistry(), NewSession(""), Options{}, event.Discard)
177 got := provider.ResolveContextBudgetPolicy(a.svc.prov)
178 if got.AutoOutputTokens != 384_000 || got.WindowMode != provider.ContextWindowShared {
179 t.Fatalf("wrapped policy = %+v", got)
180 }
181 }
182
183 func TestFreezeProviderRequestOwnsNestedServerSearchData(t *testing.T) {
184 req := provider.Request{Messages: []provider.Message{{
185 Role: provider.RoleAssistant,
186 ServerSearch: []provider.ServerSearchCall{{
187 ID: "search-1",
188 Results: []provider.ServerSearchHit{{Title: "original", URL: "https://example.test/original"}},
189 Raw: json.RawMessage(`{"value":"original"}`),
190 }},
191 }}}
192 frozen := freezeProviderRequest(req)
193 req.Messages[0].ServerSearch[0].Results[0].Title = "mutated"
194 req.Messages[0].ServerSearch[0].Raw[0] = '['
195
196 got := frozen.Messages[0].ServerSearch[0]
197 if got.Results[0].Title != "original" || string(got.Raw) != `{"value":"original"}` {
198 t.Fatalf("frozen server search shared mutable data: %+v", got)
199 }
200 }
201
201 lines GO