返回 DeepSeek-Reasonix
history_todo_test.go
根目录 / desktop / history_todo_test.go
1 package main
2
3 import (
4 "encoding/json"
5 "testing"
6
7 "reasonix/internal/provider"
8 )
9
10 func TestHistoryMessagesReplayCompleteStepsIntoTodoWrite(t *testing.T) {
11 msgs := []provider.Message{
12 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
13 ID: "todo-1", Name: "todo_write",
14 Arguments: `{"todos":[{"content":"Create the file","status":"in_progress"},{"content":"Update the file","status":"pending"}]}`,
15 }}},
16 {Role: provider.RoleTool, ToolCallID: "todo-1", Name: "todo_write", Content: "Todos updated"},
17 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
18 ID: "step-1", Name: "complete_step",
19 Arguments: `{"step":"Create the file"}`,
20 }}},
21 {Role: provider.RoleTool, ToolCallID: "step-1", Name: "complete_step", Content: "signed off"},
22 }
23
24 payload := restoredTodoPayload(t, msgs, "todo-1")
25 if got := payload.Todos[0].Status; got != "completed" {
26 t.Fatalf("first todo status = %q, want completed", got)
27 }
28 if got := payload.Todos[1].Status; got != "in_progress" {
29 t.Fatalf("second todo status = %q, want in_progress", got)
30 }
31 }
32
33 func TestHistoryMessagesRequireSuccessfulCompleteStepResult(t *testing.T) {
34 tests := []struct {
35 name string
36 toolResult *provider.Message
37 }{
38 {
39 name: "failed complete_step",
40 toolResult: &provider.Message{
41 Role: provider.RoleTool, ToolCallID: "step-1", Name: "complete_step", Content: "error: no evidence",
42 },
43 },
44 {name: "missing complete_step result"},
45 }
46 for _, tc := range tests {
47 t.Run(tc.name, func(t *testing.T) {
48 msgs := []provider.Message{
49 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
50 ID: "todo-1", Name: "todo_write",
51 Arguments: `{"todos":[{"content":"Create the file","status":"in_progress"},{"content":"Update the file","status":"pending"}]}`,
52 }}},
53 {Role: provider.RoleTool, ToolCallID: "todo-1", Name: "todo_write", Content: "Todos updated"},
54 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
55 ID: "step-1", Name: "complete_step",
56 Arguments: `{"step":"Create the file"}`,
57 }}},
58 }
59 if tc.toolResult != nil {
60 msgs = append(msgs, *tc.toolResult)
61 }
62
63 payload := restoredTodoPayload(t, msgs, "todo-1")
64 if got := payload.Todos[0].Status; got != "in_progress" {
65 t.Fatalf("complete_step without success changed first todo to %q", got)
66 }
67 if got := payload.Todos[1].Status; got != "pending" {
68 t.Fatalf("complete_step without success changed second todo to %q", got)
69 }
70 })
71 }
72 }
73
74 func TestHistoryMessagesIgnoreHistoricalPendingSignoff(t *testing.T) {
75 msgs := []provider.Message{
76 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
77 ID: "todo-1", Name: "todo_write",
78 Arguments: `{"todos":[{"content":"Create the file","status":"in_progress"},{"content":"Update the file","status":"pending"}]}`,
79 }}},
80 {Role: provider.RoleTool, ToolCallID: "todo-1", Name: "todo_write", Content: "Todos updated"},
81 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
82 ID: "step-2", Name: "complete_step", Arguments: `{"step":"Update the file"}`,
83 }}},
84 {Role: provider.RoleTool, ToolCallID: "step-2", Name: "complete_step", Content: "signed off"},
85 }
86
87 payload := restoredTodoPayload(t, msgs, "todo-1")
88 if got := payload.Todos[0].Status; got != "in_progress" {
89 t.Fatalf("current todo status = %q, want in_progress", got)
90 }
91 if got := payload.Todos[1].Status; got != "pending" {
92 t.Fatalf("historical pending signoff changed status to %q", got)
93 }
94 }
95
96 func TestHistoryMessagesNormalizeLegacyOutOfOrderTodoState(t *testing.T) {
97 msgs := []provider.Message{
98 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
99 ID: "todo-1", Name: "todo_write",
100 Arguments: `{"todos":[{"content":"Create the file","status":"in_progress"},{"content":"Update the file","status":"completed"}]}`,
101 }}},
102 {Role: provider.RoleTool, ToolCallID: "todo-1", Name: "todo_write", Content: "Todos updated"},
103 }
104
105 payload := restoredTodoPayload(t, msgs, "todo-1")
106 if got := payload.Todos[0].Status; got != "in_progress" {
107 t.Fatalf("legacy current todo status = %q, want in_progress", got)
108 }
109 if got := payload.Todos[1].Status; got != "pending" {
110 t.Fatalf("legacy out-of-order completion normalized to %q, want pending", got)
111 }
112 }
113
114 func TestHistoryMessagesIgnoreFailedTodoWriteAsReplayBase(t *testing.T) {
115 msgs := []provider.Message{
116 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
117 ID: "todo-1", Name: "todo_write",
118 Arguments: `{"todos":[{"content":"Create the file","status":"in_progress"}]}`,
119 }}},
120 {Role: provider.RoleTool, ToolCallID: "todo-1", Name: "todo_write", Content: "Todos updated"},
121 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
122 ID: "todo-2", Name: "todo_write",
123 Arguments: `{"todos":[{"content":"Bad replacement","status":"in_progress"}]}`,
124 }}},
125 {Role: provider.RoleTool, ToolCallID: "todo-2", Name: "todo_write", Content: "error: rejected todo transition"},
126 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
127 ID: "step-1", Name: "complete_step", Arguments: `{"step":"Create the file"}`,
128 }}},
129 {Role: provider.RoleTool, ToolCallID: "step-1", Name: "complete_step", Content: "signed off"},
130 }
131
132 good := restoredTodoPayload(t, msgs, "todo-1")
133 if got := good.Todos[0].Status; got != "completed" {
134 t.Fatalf("successful base was not replayed: %q", got)
135 }
136
137 bad := restoredTodoPayload(t, msgs, "todo-2")
138 if got := bad.Todos[0].Content; got != "Bad replacement" {
139 t.Fatalf("failed todo_write arguments should stay original, got %q", got)
140 }
141 if got := bad.Todos[0].Status; got != "in_progress" {
142 t.Fatalf("failed todo_write should not be replayed, status = %q", got)
143 }
144 }
145
146 func restoredTodoPayload(t *testing.T, msgs []provider.Message, todoID string) struct {
147 Todos []struct {
148 Content string `json:"content"`
149 Status string `json:"status"`
150 } `json:"todos"`
151 } {
152 t.Helper()
153 history := historyMessages(msgs, func(s string) string { return s })
154 var todoArgs string
155 for _, m := range history {
156 for _, tc := range m.ToolCalls {
157 if tc.ID == todoID {
158 todoArgs = tc.Arguments
159 }
160 }
161 }
162 if todoArgs == "" {
163 t.Fatalf("todo_write %q arguments missing from history", todoID)
164 }
165 var payload struct {
166 Todos []struct {
167 Content string `json:"content"`
168 Status string `json:"status"`
169 } `json:"todos"`
170 }
171 if err := json.Unmarshal([]byte(todoArgs), &payload); err != nil {
172 t.Fatalf("todo args are not JSON: %v", err)
173 }
174 return payload
175 }
176
177 func TestHistoryMessagesGatePhaseSignoffOnSubSteps(t *testing.T) {
178 msgs := []provider.Message{
179 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
180 ID: "todo-1", Name: "todo_write",
181 Arguments: `{"todos":[{"content":"Port the parser","status":"pending"},{"content":"move files","status":"in_progress","level":1},{"content":"fix imports","status":"pending","level":1}]}`,
182 }}},
183 {Role: provider.RoleTool, ToolCallID: "todo-1", Name: "todo_write", Content: "Todos updated"},
184 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
185 ID: "step-1", Name: "complete_step", Arguments: `{"step":"Port the parser"}`,
186 }}},
187 {Role: provider.RoleTool, ToolCallID: "step-1", Name: "complete_step", Content: "signed off"},
188 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
189 ID: "step-2", Name: "complete_step", Arguments: `{"step":"move files"}`,
190 }}},
191 {Role: provider.RoleTool, ToolCallID: "step-2", Name: "complete_step", Content: "signed off"},
192 }
193
194 payload := restoredTodoPayload(t, msgs, "todo-1")
195 if got := payload.Todos[0].Status; got != "pending" {
196 t.Fatalf("phase sign-off with unfinished sub-steps replayed to %q, want pending", got)
197 }
198 if got := payload.Todos[1].Status; got != "completed" {
199 t.Fatalf("signed-off sub-step replayed to %q, want completed", got)
200 }
201 if got := payload.Todos[2].Status; got != "in_progress" {
202 t.Fatalf("next sub-step should be promoted, got %q", got)
203 }
204
205 msgs = append(msgs,
206 provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{
207 ID: "step-3", Name: "complete_step", Arguments: `{"step":"fix imports"}`,
208 }}},
209 provider.Message{Role: provider.RoleTool, ToolCallID: "step-3", Name: "complete_step", Content: "signed off"},
210 )
211 payload = restoredTodoPayload(t, msgs, "todo-1")
212 if got := payload.Todos[0].Status; got != "in_progress" {
213 t.Fatalf("phase should become signable after its sub-steps, got %q", got)
214 }
215 }
216
216 lines GO