返回 DeepSeek-Reasonix
visible_final_test.go
根目录 / internal / agent / visible_final_test.go
1 package agent
2
3 import (
4 "context"
5 "reflect"
6 "strings"
7 "testing"
8
9 "reasonix/internal/event"
10 "reasonix/internal/provider"
11 "reasonix/internal/tool"
12 )
13
14 func TestRunSubAgentRetriesReasoningOnlyStopForVisibleFinal(t *testing.T) {
15 prov := &scriptedProvider{name: "sub", turns: [][]provider.Chunk{
16 {
17 {Type: provider.ChunkReasoning, Text: "The analysis is complete."},
18 {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "stop", TotalTokens: 10}},
19 {Type: provider.ChunkDone},
20 },
21 {
22 {Type: provider.ChunkText, Text: "visible result"},
23 {Type: provider.ChunkDone},
24 },
25 }}
26 sess := NewSession("sys")
27 sess.Add(provider.Message{Role: provider.RoleUser, Content: "previous task"})
28 sess.Add(provider.Message{Role: provider.RoleAssistant, Content: "previous result"})
29
30 answer, err := RunSubAgentWithSession(
31 testTaskContext(), deepseekThinkingProvider{prov}, tool.NewRegistry(), sess,
32 "analyze the code", Options{SubagentDepth: 1}, event.Discard,
33 )
34 if err != nil {
35 t.Fatalf("RunSubAgentWithSession: %v", err)
36 }
37 if answer != "visible result" {
38 t.Fatalf("answer = %q, want visible result", answer)
39 }
40 if prov.call != 2 {
41 t.Fatalf("provider calls = %d, want 2", prov.call)
42 }
43 if got := lastUser(prov.requests[1]); !strings.Contains(got, "visible answer") {
44 t.Fatalf("retry prompt = %q, want visible-answer nudge", got)
45 }
46 }
47
48 func TestRunSubAgentDoesNotReturnStalePreToolTextAfterReasoningOnlyStop(t *testing.T) {
49 prov := &scriptedProvider{name: "sub", turns: [][]provider.Chunk{
50 {
51 {Type: provider.ChunkReasoning, Text: "I need to inspect the input."},
52 {Type: provider.ChunkText, Text: "I'll inspect first."},
53 toolCallChunk("call-1", "echo", `{"text":"input"}`),
54 {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "tool_calls", TotalTokens: 10}},
55 {Type: provider.ChunkDone},
56 },
57 {
58 {Type: provider.ChunkReasoning, Text: "The inspection is complete."},
59 {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "stop", TotalTokens: 10}},
60 {Type: provider.ChunkDone},
61 },
62 {
63 {Type: provider.ChunkText, Text: "final findings"},
64 {Type: provider.ChunkDone},
65 },
66 }}
67 reg := tool.NewRegistry()
68 reg.Add(echoTool{})
69
70 answer, err := RunSubAgentWithSession(
71 testTaskContext(), deepseekThinkingProvider{prov}, reg, NewSession("sys"),
72 "inspect the input", Options{SubagentDepth: 1}, event.Discard,
73 )
74 if err != nil {
75 t.Fatalf("RunSubAgentWithSession: %v", err)
76 }
77 if answer != "final findings" {
78 t.Fatalf("answer = %q, want final findings (not stale tool preamble)", answer)
79 }
80 if prov.call != 3 {
81 t.Fatalf("provider calls = %d, want 3", prov.call)
82 }
83 if got := lastUser(prov.requests[2]); !strings.Contains(got, "visible answer") {
84 t.Fatalf("retry prompt = %q, want visible-answer nudge", got)
85 }
86 }
87
88 func TestRunSubAgentStopsAfterRepeatedReasoningOnlyStops(t *testing.T) {
89 prov := &scriptedProvider{name: "sub", turns: [][]provider.Chunk{
90 {{Type: provider.ChunkReasoning, Text: "thinking 1"}, {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "stop"}}, {Type: provider.ChunkDone}},
91 {{Type: provider.ChunkReasoning, Text: "thinking 2"}, {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "stop"}}, {Type: provider.ChunkDone}},
92 {{Type: provider.ChunkReasoning, Text: "thinking 3"}, {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "stop"}}, {Type: provider.ChunkDone}},
93 }}
94
95 _, err := RunSubAgentWithSession(
96 testTaskContext(), deepseekThinkingProvider{prov}, tool.NewRegistry(), NewSession("sys"),
97 "analyze the code", Options{SubagentDepth: 1}, event.Discard,
98 )
99 if err == nil || !strings.Contains(err.Error(), "visible final answer") {
100 t.Fatalf("error = %v, want bounded visible-final failure", err)
101 }
102 if prov.call != maxEmptyFinalBlocks {
103 t.Fatalf("provider calls = %d, want %d", prov.call, maxEmptyFinalBlocks)
104 }
105 }
106
107 func TestCoordinatorToolPlannerRetriesReasoningOnlyStopForVisiblePlan(t *testing.T) {
108 plannerScript := &scriptedProvider{name: "planner", turns: [][]provider.Chunk{
109 {
110 {Type: provider.ChunkReasoning, Text: "I need to inspect the input."},
111 {Type: provider.ChunkText, Text: "I'll inspect first."},
112 toolCallChunk("call-1", "echo", `{"text":"input"}`),
113 {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "tool_calls", TotalTokens: 10}},
114 {Type: provider.ChunkDone},
115 },
116 {
117 {Type: provider.ChunkReasoning, Text: "The plan is ready."},
118 {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "stop", TotalTokens: 10}},
119 {Type: provider.ChunkDone},
120 },
121 {
122 {Type: provider.ChunkReasoning, Text: "Submitting the plan now."},
123 toolCallChunk("call-2", "submit_plan", `{"objective":"apply the fix","steps":[{"title":"apply the verified fix"}]}`),
124 {Type: provider.ChunkUsage, Usage: &provider.Usage{FinishReason: "tool_calls", TotalTokens: 10}},
125 {Type: provider.ChunkDone},
126 },
127 }}
128 exec := &mockProvider{name: "executor", chunks: []provider.Chunk{
129 {Type: provider.ChunkText, Text: "Done."},
130 {Type: provider.ChunkDone},
131 }}
132 plannerTools := tool.NewRegistry()
133 plannerTools.Add(echoTool{})
134 executor := New(exec, tool.NewRegistry(), NewSession("exec-sys"), Options{}, event.Discard)
135 coord := NewCoordinator(
136 deepseekThinkingProvider{plannerScript}, NewSession("planner-sys"), nil,
137 PlannerToolRegistry(plannerTools), Options{}, executor, 0, event.Discard, nil,
138 )
139
140 if err := coord.Run(withNoClosedLoop(context.Background()), "fix the bug"); err != nil {
141 t.Fatalf("Run: %v", err)
142 }
143 if plannerScript.call != 3 {
144 t.Fatalf("planner calls = %d, want 3", plannerScript.call)
145 }
146 if len(exec.requests) == 0 {
147 t.Fatal("executor received no handoff request")
148 }
149 got := lastUser(exec.requests[0])
150 if !strings.Contains(got, "apply the verified fix") || !strings.Contains(got, executorHandoffMarker) {
151 t.Fatalf("executor input = %q, want visible plan and handoff marker", got)
152 }
153 if strings.Contains(got, "I'll inspect first.") {
154 t.Fatalf("executor input contains stale planner preamble: %q", got)
155 }
156 }
157
158 func TestCoordinatorRollbackAfterRewriteDropsReasoningOnlyRetryTail(t *testing.T) {
159 plannerSess := NewSession("planner-sys")
160 before := plannerSess.Snapshot()
161 rewriteBefore := plannerSess.RewriteVersion()
162
163 compactedWithEvidence := []provider.Message{
164 {Role: provider.RoleSystem, Content: "planner-sys"},
165 {Role: provider.RoleUser, Content: summaryTagOpen + "\ncompacted research\n" + summaryTagClose},
166 {Role: provider.RoleAssistant, Content: "Visible evidence collected before the current tool round."},
167 {Role: provider.RoleUser, Content: "Plan the current task."},
168 {Role: provider.RoleAssistant, ToolCalls: []provider.ToolCall{{ID: "read-1", Name: "read_file", Arguments: `{"path":"main.go"}`}}},
169 {Role: provider.RoleTool, ToolCallID: "read-1", Name: "read_file", Content: "package main"},
170 }
171 plannerSess.Replace(compactedWithEvidence)
172 plannerSess.IncrementRewrite()
173 plannerSess.Add(provider.Message{Role: provider.RoleAssistant, ReasoningContent: "first hidden-only plan"})
174 plannerSess.Add(provider.Message{Role: provider.RoleUser, Content: "provide a visible plan"})
175 plannerSess.Add(provider.Message{Role: provider.RoleAssistant, ReasoningContent: "second hidden-only plan"})
176 plannerSess.Add(provider.Message{Role: provider.RoleUser, Content: "provide a visible plan"})
177 plannerSess.Add(provider.Message{Role: provider.RoleAssistant, ReasoningContent: "third hidden-only plan"})
178
179 coord := &Coordinator{plannerSess: plannerSess}
180 coord.rollbackPlannerTurn(before, rewriteBefore)
181
182 got := plannerSess.Snapshot()
183 if !reflect.DeepEqual(got, compactedWithEvidence) {
184 t.Fatalf("rewrite-aware rollback changed compacted or completed tool evidence:\n got=%+v\nwant=%+v", got, compactedWithEvidence)
185 }
186 if normalized := provider.NormalizeMessages(got); !reflect.DeepEqual(normalized, got) {
187 t.Fatalf("preserved planner history is not provider-coherent:\n got=%+v\nnormalized=%+v", got, normalized)
188 }
189 }
190
190 lines GO