返回 DeepSeek-Reasonix
todo_test.go
根目录 / internal / tool / builtin / todo_test.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "strings"
7 "testing"
8 )
9
10 func executeTodos(t *testing.T, raw string) (todoWriteResponse, error) {
11 t.Helper()
12 out, err := (todoWrite{}).Execute(context.Background(), json.RawMessage(raw))
13 if err != nil {
14 return todoWriteResponse{}, err
15 }
16 var response todoWriteResponse
17 if err := json.Unmarshal([]byte(out), &response); err != nil {
18 t.Fatalf("decode result %q: %v", out, err)
19 }
20 return response, nil
21 }
22
23 func TestTodoWriteAcceptsWholeListReplacementShapes(t *testing.T) {
24 for _, tc := range []struct {
25 name string
26 raw string
27 }{
28 {"empty", `{"todos":[]}`},
29 {"only pending", `{"todos":[{"content":"one","status":"pending"},{"content":"two","status":"pending"}]}`},
30 {"parallel", `{"todos":[{"content":"one","status":"in_progress"},{"content":"two","status":"in_progress"}]}`},
31 {"out of order", `{"todos":[{"content":"later","status":"completed"},{"content":"earlier","status":"pending"}]}`},
32 } {
33 t.Run(tc.name, func(t *testing.T) {
34 if _, err := executeTodos(t, tc.raw); err != nil {
35 t.Fatalf("replacement rejected: %v", err)
36 }
37 })
38 }
39 }
40
41 func TestTodoWriteReturnsCanonicalListAndCounts(t *testing.T) {
42 got, err := executeTodos(t, `{"todos":[{"content":" inspect ","status":"completed"},{"content":"ship","status":"in_progress"}]}`)
43 if err != nil {
44 t.Fatal(err)
45 }
46 if len(got.Todos) != 2 || got.Todos[0].Content != "inspect" || got.Counts.Total != 2 || got.Counts.Completed != 1 || got.Counts.InProgress != 1 {
47 t.Fatalf("canonical response = %+v", got)
48 }
49 }
50
51 func TestTodoWriteValidationIsStrictAndNonMutating(t *testing.T) {
52 for _, tc := range []struct {
53 name string
54 raw string
55 want string
56 }{
57 {"unknown item field", `{"todos":[{"content":"one","status":"pending","step_id":"old"}]}`, "unknown field"},
58 {"unknown root field", `{"todos":[],"activeForm":"old"}`, "unknown field"},
59 {"null item", `{"todos":[null]}`, "todos[0] must be an object"},
60 {"missing content", `{"todos":[{"status":"pending"}]}`, "todos[0].content"},
61 {"null content", `{"todos":[{"content":null,"status":"pending"}]}`, "todos[0].content"},
62 {"missing status", `{"todos":[{"content":"one"}]}`, "todos[0].status"},
63 {"null status", `{"todos":[{"content":"one","status":null}]}`, "todos[0].status"},
64 {"blank", `{"todos":[{"content":" ","status":"pending"}]}`, "todos[0].content"},
65 {"duplicate after trim", `{"todos":[{"content":"one","status":"pending"},{"content":" one ","status":"completed"}]}`, "duplicates"},
66 {"bad status", `{"todos":[{"content":"one","status":"running"}]}`, "todos[0].status"},
67 {"trailing value", `{"todos":[]} {}`, "multiple JSON values"},
68 } {
69 t.Run(tc.name, func(t *testing.T) {
70 _, err := executeTodos(t, tc.raw)
71 if err == nil || !strings.Contains(err.Error(), tc.want) {
72 t.Fatalf("error = %v, want %q", err, tc.want)
73 }
74 })
75 }
76 }
77
78 func TestCompleteStepTombstoneIsActionable(t *testing.T) {
79 _, err := (completeStep{}).Execute(context.Background(), json.RawMessage(`{"result":"done"}`))
80 if err == nil || !strings.Contains(err.Error(), "retired") || !strings.Contains(err.Error(), "todo_write") {
81 t.Fatalf("retirement error = %v", err)
82 }
83 }
84
84 lines GO