返回 DeepSeek-Reasonix
context_maintenance_restart_test.go
根目录 / internal / agent / context_maintenance_restart_test.go
1 package agent
2
3 import (
4 "context"
5 "fmt"
6 "path/filepath"
7 "strings"
8 "testing"
9
10 "reasonix/internal/event"
11 "reasonix/internal/provider"
12 "reasonix/internal/tool"
13 )
14
15 const restartMaintenanceWindow = 1_000_000
16
17 // largeRestartToolHistory builds a realistic long coding session from capped
18 // tool results. Reusing the same immutable string keeps the test allocation
19 // small while every message still contributes to the provider request size.
20 func largeRestartToolHistory(results int) []provider.Message {
21 result := strings.Repeat("x", 32*1024)
22 messages := []provider.Message{
23 {Role: provider.RoleSystem, Content: "system"},
24 {Role: provider.RoleUser, Content: "long-running task"},
25 }
26 for i := range results {
27 id := fmt.Sprintf("tool-%d", i)
28 messages = append(messages,
29 provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: id, Name: "read_file", Arguments: "{}"}}},
30 provider.Message{Role: provider.RoleTool, ToolCallID: id, Name: "read_file", Content: result},
31 )
32 }
33 return append(messages,
34 provider.Message{Role: provider.RoleUser, Content: "write the handover now"},
35 provider.Message{Role: provider.RoleAssistant, Content: "working"},
36 )
37 }
38
39 func newRestartMaintenanceAgent(path string, messages []provider.Message, sink event.Sink) *Agent {
40 // Explicit 0.80 matches the v1.22.0 user scenario and the plan's regression
41 // fixture. New configs default to 0.80; explicit legacy ratios remain unchanged.
42 return New(nil, tool.NewRegistry(), &Session{Messages: append([]provider.Message(nil), messages...)}, Options{
43 ContextWindow: restartMaintenanceWindow,
44 CompactRatio: 0.80,
45 RecentKeep: 2,
46 SessionPath: path,
47 WorkspaceID: "workspace",
48 ModelRef: "provider/model",
49 }, sink)
50 }
51
52 func appliedMaintenanceEvents(sink *recordSink) int {
53 count := 0
54 for _, got := range sink.kinds(event.ContextMaintenanceEvent) {
55 if got.Maintenance != nil && got.Maintenance.Status == "applied" {
56 count++
57 }
58 }
59 return count
60 }
61
62 // 80 capped tool results put the request just above 60% of a one-million-token
63 // window. In v1.22.0 that crossed tool_result_snip_ratio, rewrote the prompt,
64 // and did so again after reopening. compact_ratio=80% must be the sole trigger.
65 func TestContextMaintenanceWaitsForCompactRatioAcrossRestart(t *testing.T) {
66 messages := largeRestartToolHistory(80)
67 path := filepath.Join(t.TempDir(), "session.jsonl")
68
69 firstSink := &recordSink{}
70 first := newRestartMaintenanceAgent(path, messages, firstSink)
71 before := first.ContextMaintenanceSnapshot()
72 if before.ProjectedTokens < 600_000 || before.ProjectedTokens >= before.FoldTrigger {
73 t.Fatalf("fixture prompt/trigger = %d/%d, want prompt in [600000, fold)", before.ProjectedTokens, before.FoldTrigger)
74 }
75 if _, err := first.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
76 t.Fatal(err)
77 }
78 if got := first.currentProjectionVersion(); got != 0 {
79 t.Fatalf("60%% prompt installed projection version %d below compact_ratio", got)
80 }
81 if got := appliedMaintenanceEvents(firstSink); got != 0 {
82 t.Fatalf("60%% prompt emitted %d applied maintenance events", got)
83 }
84
85 reopenedSink := &recordSink{}
86 reopened := newRestartMaintenanceAgent(path, messages, reopenedSink)
87 if _, err := reopened.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
88 t.Fatal(err)
89 }
90 if got := reopened.currentProjectionVersion(); got != 0 {
91 t.Fatalf("reopened 60%% prompt installed projection version %d", got)
92 }
93 if got := appliedMaintenanceEvents(reopenedSink); got != 0 {
94 t.Fatalf("reopened 60%% prompt emitted %d applied maintenance events", got)
95 }
96 }
97
98 // Once the prompt really crosses 80%, one summary checkpoint is installed.
99 // Reopening must restore that sidecar without re-summarizing or replaying events.
100 func TestContextMaintenanceRestoresAppliedProjectionWithoutReapplying(t *testing.T) {
101 messages := largeRestartToolHistory(110)
102 path := filepath.Join(t.TempDir(), "session.jsonl")
103
104 // A deterministic fake provider so the single summary transaction can land.
105 firstSink := &recordSink{}
106 first := New(&fakeProvider{reply: "structured resume briefing"}, tool.NewRegistry(),
107 &Session{Messages: append([]provider.Message(nil), messages...)}, Options{
108 ContextWindow: restartMaintenanceWindow,
109 CompactRatio: 0.80,
110 RecentKeep: 2,
111 SessionPath: path,
112 WorkspaceID: "workspace",
113 ModelRef: "provider/model",
114 }, firstSink)
115 before := first.ContextMaintenanceSnapshot()
116 if before.ProjectedTokens < before.FoldTrigger {
117 t.Fatalf("fixture prompt/trigger = %d/%d, want prompt at or above fold", before.ProjectedTokens, before.FoldTrigger)
118 }
119 if _, err := first.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
120 t.Fatal(err)
121 }
122 if got := first.currentProjectionVersion(); got != 1 {
123 t.Fatalf("first maintenance projection version = %d, want 1", got)
124 }
125 if got := appliedMaintenanceEvents(firstSink); got != 1 {
126 t.Fatalf("first maintenance applied events = %d, want 1", got)
127 }
128 // Content-driven checkpoint should materially shrink the visible request.
129 after := first.ContextMaintenanceSnapshot()
130 if after.ProjectedTokens > after.HardInputCeiling/2 && after.ProjectedTokens > 300_000 {
131 t.Fatalf("checkpoint projected tokens = %d, want content-driven low occupancy", after.ProjectedTokens)
132 }
133
134 reopenedSink := &recordSink{}
135 reopened := New(nil, tool.NewRegistry(),
136 &Session{Messages: append([]provider.Message(nil), messages...)}, Options{
137 ContextWindow: restartMaintenanceWindow,
138 CompactRatio: 0.80,
139 RecentKeep: 2,
140 SessionPath: path,
141 WorkspaceID: "workspace",
142 ModelRef: "provider/model",
143 }, reopenedSink)
144 if got := reopened.currentProjectionVersion(); got != 1 {
145 t.Fatalf("restored projection version = %d, want 1", got)
146 }
147 if got := reopened.ContextMaintenanceSnapshot().CheckpointState; got != "restored" {
148 t.Fatalf("checkpoint state = %q, want restored", got)
149 }
150 if _, err := reopened.contextManager().Prepare(context.Background(), ContextPreparePolicy{Trigger: CompactionTriggerPressure}); err != nil {
151 t.Fatal(err)
152 }
153 if got := reopened.currentProjectionVersion(); got != 1 {
154 t.Fatalf("reopen advanced unchanged projection to version %d", got)
155 }
156 if got := appliedMaintenanceEvents(reopenedSink); got != 0 {
157 t.Fatalf("reopen reapplied maintenance %d times", got)
158 }
159 }
160
160 lines GO