返回 DeepSeek-Reasonix
final_readiness_test.go
根目录 / internal / agent / final_readiness_test.go
1 package agent
2
3 import (
4 "strings"
5 "testing"
6
7 "reasonix/internal/evidence"
8 "reasonix/internal/instruction"
9 "reasonix/internal/provider"
10 )
11
12 func readinessLedger(receipts ...evidence.Receipt) *evidence.Ledger {
13 l := evidence.NewLedger()
14 for _, r := range receipts {
15 l.Record(r)
16 }
17 return l
18 }
19
20 func TestFinalReadinessFailureBranches(t *testing.T) {
21 check := instruction.VerifyCheck{Command: "go test ./...", SourcePath: "AGENTS.md", Line: 3}
22 writer := evidence.Receipt{ToolName: "write_file", Success: true, Write: true, Paths: []string{"a.go"}}
23 readOnly := evidence.Receipt{ToolName: "read_file", Success: true, Read: true, Paths: []string{"a.go"}}
24 checkAfter := evidence.Receipt{ToolName: "bash", Success: true, Command: "go test ./..."}
25 todo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "in_progress"}}}
26 completeAfter := evidence.Receipt{ToolName: "complete_step", Success: true, Step: "edit"}
27 doneTodo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "completed"}}}
28
29 cases := []struct {
30 name string
31 checks []instruction.VerifyCheck
32 evidence *evidence.Ledger
33 wantEmpty bool
34 wantContain string
35 }{
36 {"nil evidence never gates", []instruction.VerifyCheck{check}, nil, true, ""},
37 {"no writer never gates", []instruction.VerifyCheck{check}, readinessLedger(checkAfter), true, ""},
38 {"todo-only turn may end with incomplete list", nil, readinessLedger(todo), true, ""},
39 {"read-only context plus todo may end with incomplete list", nil, readinessLedger(readOnly, todo), true, ""},
40 {"completed todo without writer satisfies", nil, readinessLedger(doneTodo), true, ""},
41 {"writer without checks or todo never gates", nil, readinessLedger(writer), true, ""},
42 {"missing project check after writer is reported", []instruction.VerifyCheck{check}, readinessLedger(checkAfter, writer), false, "go test ./..."},
43 {"project check run after writer satisfies", []instruction.VerifyCheck{check}, readinessLedger(writer, checkAfter), true, ""},
44 {"todo writer without complete_step is reported", nil, readinessLedger(writer, todo), false, "incomplete items"},
45 {"complete_step without final todo update is reported", nil, readinessLedger(writer, todo, completeAfter), false, "latest successful todo_write"},
46 {"todo writer with complete_step and completed todo satisfies", nil, readinessLedger(writer, todo, completeAfter, doneTodo), true, ""},
47 }
48 for _, tc := range cases {
49 t.Run(tc.name, func(t *testing.T) {
50 a := &Agent{evidence: tc.evidence, projectChecks: tc.checks}
51 got := a.ReadinessResult()
52 if tc.wantEmpty {
53 if !got.Ready {
54 t.Fatalf("ReadinessResult() = %+v, want ready (no gate)", got)
55 }
56 return
57 }
58 if got.Ready {
59 t.Fatalf("ReadinessResult() = ready, want a failure mentioning %q", tc.wantContain)
60 }
61 if !strings.Contains(got.Reason, tc.wantContain) {
62 t.Fatalf("ReadinessResult().Reason = %q, want it to mention %q", got.Reason, tc.wantContain)
63 }
64 if len(got.Missing) == 0 {
65 t.Fatalf("ReadinessResult().Missing empty for %q", tc.wantContain)
66 }
67 })
68 }
69 }
70
71 func TestFinalReadinessAllowsIncompleteTodosInPlanMode(t *testing.T) {
72 todo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "draft implementation plan", Status: "pending"}}}
73 a := &Agent{evidence: readinessLedger(todo)}
74 a.SetPlanMode(true)
75
76 if got := a.ReadinessResult(); !got.Ready {
77 t.Fatalf("ReadinessResult() = %+v, want ready in plan mode", got)
78 }
79 if got := a.finalReadinessCheckFor(); got.applies {
80 t.Fatalf("finalReadinessCheckFor() applies in plan mode: %+v", got)
81 }
82 }
83
84 func TestFinalReadinessCheckAuditsIncompleteTodos(t *testing.T) {
85 todo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "in_progress"}}}
86 writer := evidence.Receipt{ToolName: "write_file", Success: true, Write: true, Paths: []string{"a.go"}}
87 a := &Agent{evidence: readinessLedger(writer, todo)}
88
89 got := a.finalReadinessCheckFor()
90 if !got.applies {
91 t.Fatalf("finalReadinessCheckFor() applies = false, want true")
92 }
93 if got.incompleteTodos != 1 {
94 t.Fatalf("incompleteTodos = %d, want 1", got.incompleteTodos)
95 }
96 if !strings.Contains(got.reason, "latest successful todo_write") {
97 t.Fatalf("reason = %q, want incomplete todo message", got.reason)
98 }
99 audit := got.audit(evidence.ReadinessBlocked, false)
100 if audit.IncompleteTodos != 1 {
101 t.Fatalf("audit.IncompleteTodos = %d, want 1", audit.IncompleteTodos)
102 }
103 }
104
105 func TestFinalReadinessAllowsFinalAfterLoopGuardedToolBlocker(t *testing.T) {
106 todo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "in_progress"}}}
107 writer := evidence.Receipt{ToolName: "write_file", Success: true, Write: true, Paths: []string{"a.go"}}
108 ledger := readinessLedger(writer, todo)
109 a := &Agent{evidence: ledger}
110 a.armLoopGuardPass(ledger.Len())
111
112 got := a.finalReadinessCheckFor()
113 if !got.applies {
114 t.Fatalf("finalReadinessCheckFor() applies = false, want true audit after loop guard")
115 }
116 if got.reason != "" {
117 t.Fatalf("finalReadinessCheckFor() reason = %q, want loop guard to allow final blocker report", got.reason)
118 }
119 }
120
121 // TestFinalReadinessLoopGuardPassSurvivesBookkeeping proves the exact actions
122 // the loop guard recommends — ask, todo_write, complete_step — do not revoke
123 // the pass: the model must be able to record the blocker and then report it.
124 func TestFinalReadinessLoopGuardPassSurvivesBookkeeping(t *testing.T) {
125 todo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "in_progress"}}}
126 writer := evidence.Receipt{ToolName: "write_file", Success: true, Write: true, Paths: []string{"a.go"}}
127 ledger := readinessLedger(writer, todo)
128 a := &Agent{evidence: ledger}
129 a.armLoopGuardPass(ledger.Len())
130
131 ledger.Record(evidence.Receipt{ToolName: "ask", Success: true})
132 ledger.Record(evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "in_progress"}}})
133 ledger.Record(evidence.Receipt{ToolName: "complete_step", Success: true, Step: "edit"})
134
135 if got := a.finalReadinessCheckFor(); got.reason != "" {
136 t.Fatalf("finalReadinessCheckFor() reason = %q, want bookkeeping after the guard to keep the pass", got.reason)
137 }
138 }
139
140 // TestFinalReadinessLoopGuardPassRevokedByRealProgress proves a successful
141 // write or command receipt after the guard revokes the pass: receipts are
142 // obtainable again, so readiness resumes enforcing them.
143 func TestFinalReadinessLoopGuardPassRevokedByRealProgress(t *testing.T) {
144 todo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "in_progress"}}}
145 writer := evidence.Receipt{ToolName: "write_file", Success: true, Write: true, Paths: []string{"a.go"}}
146 ledger := readinessLedger(writer, todo)
147 a := &Agent{evidence: ledger}
148 a.armLoopGuardPass(ledger.Len())
149
150 ledger.Record(evidence.Receipt{ToolName: "bash", Success: true, Command: "go test ./..."})
151
152 if got := a.finalReadinessCheckFor(); got.reason == "" {
153 t.Fatal("finalReadinessCheckFor() reason empty, want real progress after the guard to revoke the pass")
154 }
155 }
156
157 // TestFinalReadinessIgnoresLoopGuardQuotedInToolOutput proves the pass is host
158 // state, not message text: a tool result that merely quotes "[loop guard]"
159 // (a grep over this repo, a pasted transcript) must not unlock readiness.
160 func TestFinalReadinessIgnoresLoopGuardQuotedInToolOutput(t *testing.T) {
161 todo := evidence.Receipt{ToolName: "todo_write", Success: true, Todos: []evidence.TodoItem{{Content: "edit", Status: "in_progress"}}}
162 writer := evidence.Receipt{ToolName: "write_file", Success: true, Write: true, Paths: []string{"a.go"}}
163 sess := NewSession("")
164 sess.Add(provider.Message{Role: provider.RoleUser, Content: "edit"})
165 sess.Add(provider.Message{Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "b1", Name: "bash"}}})
166 sess.Add(provider.Message{Role: provider.RoleTool, ToolCallID: "b1", Name: "bash", Content: "agent.go:2082: \"[loop guard] %s has now %s %d times\""})
167 a := &Agent{evidence: readinessLedger(writer, todo), session: sess}
168
169 if got := a.finalReadinessCheckFor(); got.reason == "" {
170 t.Fatal("finalReadinessCheckFor() reason empty, want quoted loop-guard text to be ignored")
171 }
172 }
173
173 lines GO