返回 DeepSeek-Reasonix
todo_state_test.go
根目录 / internal / turnevent / todo_state_test.go
1 package turnevent
2
3 import (
4 "errors"
5 "os"
6 "testing"
7
8 "reasonix/internal/event"
9 )
10
11 func TestTodoStateIsCommittedWithToolResultAndClearedByCommittedStart(t *testing.T) {
12 path := testSessionPath(t)
13 ledger, err := Open(path, "session")
14 if err != nil {
15 t.Fatal(err)
16 }
17 turnID, err := ledger.Begin()
18 if err != nil {
19 t.Fatal(err)
20 }
21 if _, ok, err := ledger.Append(event.Event{Kind: event.TurnStarted}, event.TurnInProgress); err != nil || !ok {
22 t.Fatalf("append start: ok=%v err=%v", ok, err)
23 }
24 todos := []event.Todo{{Content: "inspect", Status: "completed"}, {Content: "ship", Status: "in_progress"}}
25 if _, ok, err := ledger.Append(event.Event{Kind: event.ToolResult, Tool: event.Tool{ID: "todo-1", Name: "todo_write", TodoWritten: true, Todos: todos}}, event.TurnInProgress); err != nil || !ok {
26 t.Fatalf("append todo result: ok=%v err=%v", ok, err)
27 }
28 if got, written := ledger.TodoState(); !written || len(got) != 2 || got[1] != todos[1] {
29 t.Fatalf("live todo state = %+v written=%v", got, written)
30 }
31 if _, ok, err := ledger.Append(event.Event{Kind: event.TurnDone}, event.TurnCompleted); err != nil || !ok {
32 t.Fatalf("append terminal: ok=%v err=%v", ok, err)
33 }
34 if err := ledger.AcknowledgeProjection(turnID); err != nil {
35 t.Fatal(err)
36 }
37 if err := ledger.Compact(); err != nil {
38 t.Fatal(err)
39 }
40 if err := ledger.Close(); err != nil {
41 t.Fatal(err)
42 }
43
44 reopened, err := Open(path, "session")
45 if err != nil {
46 t.Fatal(err)
47 }
48 defer reopened.Close()
49 if got, written := reopened.TodoState(); !written || len(got) != 2 || got[0] != todos[0] {
50 t.Fatalf("reopened todo state = %+v written=%v", got, written)
51 }
52 if _, err := reopened.Begin(); err != nil {
53 t.Fatal(err)
54 }
55 if got, written := reopened.TodoState(); !written || len(got) != 2 {
56 t.Fatalf("turn allocation changed todo state before commit = %+v written=%v", got, written)
57 }
58 if _, ok, err := reopened.Append(event.Event{Kind: event.TurnStarted}, event.TurnInProgress); err != nil || !ok {
59 t.Fatalf("append new start: ok=%v err=%v", ok, err)
60 }
61 if got, written := reopened.TodoState(); written || len(got) != 0 {
62 t.Fatalf("new turn retained todo state = %+v written=%v", got, written)
63 }
64 }
65
66 func TestFailedTurnStartCommitKeepsPreviousTodoState(t *testing.T) {
67 ledger, err := Open("", "session")
68 if err != nil {
69 t.Fatal(err)
70 }
71 if _, err := ledger.Begin(); err != nil {
72 t.Fatal(err)
73 }
74 _, _, _ = ledger.Append(event.Event{Kind: event.TurnStarted}, event.TurnInProgress)
75 want := []event.Todo{{Content: "previous", Status: "completed"}}
76 _, _, _ = ledger.Append(event.Event{Kind: event.ToolResult, Tool: event.Tool{Name: "todo_write", TodoWritten: true, Todos: want}}, event.TurnInProgress)
77 _, _, _ = ledger.Append(event.Event{Kind: event.TurnDone}, event.TurnCompleted)
78
79 if _, err := ledger.Begin(); err != nil {
80 t.Fatal(err)
81 }
82 // Force the first write attempt to fail before it can publish the new-turn
83 // projection. A directory cannot be opened as the append-only ledger file.
84 ledger.path = t.TempDir()
85 if _, _, err := ledger.Append(event.Event{Kind: event.TurnStarted}, event.TurnInProgress); err == nil || !errors.Is(err, ErrTurnLedgerUnavailable) {
86 t.Fatalf("failed start append error = %v", err)
87 }
88 if got, written := ledger.TodoState(); !written || len(got) != 1 || got[0] != want[0] {
89 t.Fatalf("failed start commit changed todo state = %+v written=%v", got, written)
90 }
91 if ledger.writer != nil {
92 t.Fatal("failed start retained a writer")
93 }
94 if _, err := os.Stat(ledger.path); err != nil {
95 t.Fatal(err)
96 }
97 }
98
99 func TestExplicitEmptyTodoStateSurvivesReplay(t *testing.T) {
100 path := testSessionPath(t)
101 ledger, err := Open(path, "session")
102 if err != nil {
103 t.Fatal(err)
104 }
105 if _, err := ledger.Begin(); err != nil {
106 t.Fatal(err)
107 }
108 _, _, _ = ledger.Append(event.Event{Kind: event.TurnStarted}, event.TurnInProgress)
109 _, _, _ = ledger.Append(event.Event{Kind: event.ToolResult, Tool: event.Tool{Name: "todo_write", TodoWritten: true, Todos: []event.Todo{}}}, event.TurnInProgress)
110 _, _, _ = ledger.Append(event.Event{Kind: event.TurnDone}, event.TurnCompleted)
111 if err := ledger.Close(); err != nil {
112 t.Fatal(err)
113 }
114 reopened, err := Open(path, "session")
115 if err != nil {
116 t.Fatal(err)
117 }
118 defer reopened.Close()
119 if got, written := reopened.TodoState(); !written || len(got) != 0 {
120 t.Fatalf("empty todo state = %+v written=%v", got, written)
121 }
122 }
123
124 func TestRecoveryStatusPreservesExactDurableReason(t *testing.T) {
125 path := testSessionPath(t)
126 ledger, err := Open(path, "session")
127 if err != nil {
128 t.Fatal(err)
129 }
130 if _, err := ledger.Begin(); err != nil {
131 t.Fatal(err)
132 }
133 want := &event.RecoveryStatus{State: "recovery_required", Reason: "cancellation_grace_expired", Phase: "tool_execution", RequiresUserDecision: true}
134 if _, ok, err := ledger.Append(event.Event{Kind: event.TurnDone, Recovery: want}, event.TurnRecoveryRequired); err != nil || !ok {
135 t.Fatalf("append recovery = (%v, %v)", ok, err)
136 }
137 if err := ledger.Close(); err != nil {
138 t.Fatal(err)
139 }
140
141 reopened, err := Open(path, "session")
142 if err != nil {
143 t.Fatal(err)
144 }
145 defer reopened.Close()
146 got := reopened.RecoveryStatus()
147 if got == nil || got.Reason != want.Reason || got.Phase != want.Phase || !got.RequiresUserDecision {
148 t.Fatalf("recovery = %#v, want %#v", got, want)
149 }
150 }
151
151 lines GO