返回 DeepSeek-Reasonix
readiness_manual_recovery_test.go
根目录 / internal / control / readiness_manual_recovery_test.go
1 package control
2
3 import (
4 "context"
5 "testing"
6
7 "reasonix/internal/agent"
8 "reasonix/internal/event"
9 "reasonix/internal/provider"
10 "reasonix/internal/session"
11 "reasonix/internal/tool"
12 )
13
14 func manualReadinessController(t *testing.T, turns [][]provider.Chunk) (*Controller, *scriptedTurns) {
15 t.Helper()
16 reg := tool.NewRegistry()
17 reg.Add(minimalFakeTool{name: "write_file"})
18 reg.Add(minimalFakeTool{name: "read_file", readOnly: true})
19 if todoWrite, ok := tool.LookupBuiltin("todo_write"); ok {
20 reg.Add(todoWrite)
21 }
22 prov := &scriptedTurns{turns: turns}
23 executor := agent.New(prov, reg, agent.NewSession("stable-system-prefix"), agent.Options{}, event.Discard)
24 c := newOwnedTestController(t, Options{Runner: executor, Executor: executor, Sink: event.Discard})
25 t.Cleanup(c.Close)
26 return c, prov
27 }
28
29 func syntheticUserTurnCount(messages []provider.Message) int {
30 count := 0
31 for _, message := range messages {
32 if message.Role == provider.RoleUser && IsSyntheticUserMessage(message.Content) {
33 count++
34 }
35 }
36 return count
37 }
38
39 func TestStandardStopsAfterOneVisibleTurn(t *testing.T) {
40 c, prov := manualReadinessController(t, [][]provider.Chunk{
41 textTurn("已完成学习说明。下一步可以由你决定。"),
42 textTurn("不应被隐藏续跑消费"),
43 })
44 if err := c.SetQualityFloor(QualityFloorStandard); err != nil {
45 t.Fatalf("SetQualityFloor: %v", err)
46 }
47
48 if err := newTurnOrchestrator(c).runGoalLoopWithRawDisplay(context.Background(), "学习并解释当前内容", "学习并解释当前内容", ""); err != nil {
49 t.Fatalf("standard turn returned %v", err)
50 }
51 if prov.call != 1 {
52 t.Fatalf("provider calls = %d, want one visible turn", prov.call)
53 }
54 if got := syntheticUserTurnCount(c.executor.Session().Snapshot()); got != 0 {
55 t.Fatalf("synthetic user turns = %d, want zero", got)
56 }
57 }
58
59 func TestStandardStartReplyContinuesCurrentTodoInsideVisibleTurn(t *testing.T) {
60 c, prov := manualReadinessController(t, [][]provider.Chunk{
61 {toolCallChunk("todo-1", "todo_write", `{"todos":[{"content":"重写第 4 节","status":"in_progress"}]}`), {Type: provider.ChunkDone}},
62 textTurn("让我先列出待办,接下来会重写。"),
63 {
64 toolCallChunk("write-1", "write_file", `{"path":"PRD.md"}`),
65 toolCallChunk("todo-2", "todo_write", `{"todos":[{"content":"重写第 4 节","status":"completed"}]}`),
66 {Type: provider.ChunkDone},
67 },
68 textTurn("第 4 节已经完成重写。"),
69 })
70 if err := c.SetQualityFloor(QualityFloorStandard); err != nil {
71 t.Fatalf("SetQualityFloor: %v", err)
72 }
73 c.executor.Session().Add(provider.Message{Role: provider.RoleAssistant, Content: "让我写完整新第 4 节。"})
74
75 if err := newTurnOrchestrator(c).runGoalLoopWithRawDisplay(context.Background(), "开始", "开始", "开始"); err != nil {
76 t.Fatalf("standard start reply returned %v", err)
77 }
78 if prov.call != 2 {
79 t.Fatalf("provider calls = %d, want a clean final with no ordinary todo continuation", prov.call)
80 }
81 if got := syntheticUserTurnCount(c.executor.Session().Snapshot()); got != 0 {
82 t.Fatalf("synthetic user turns = %d, want none on the ordinary Agent path", got)
83 }
84 }
85
86 func TestStandardStartWithoutConversationDoesNotArmTodoContinuation(t *testing.T) {
87 c, prov := manualReadinessController(t, [][]provider.Chunk{
88 {toolCallChunk("todo-1", "todo_write", `{"todos":[{"content":"重写第 4 节","status":"in_progress"}]}`), {Type: provider.ChunkDone}},
89 textTurn("等待下一步。"),
90 textTurn("不应被隐藏续跑消费"),
91 })
92
93 if err := newTurnOrchestrator(c).runGoalLoopWithRawDisplay(context.Background(), "开始", "开始", "开始"); err != nil {
94 t.Fatalf("standard start reply returned %v", err)
95 }
96 if prov.call != 2 {
97 t.Fatalf("provider calls = %d, want no continuation without prior context", prov.call)
98 }
99 }
100
101 func TestStandardTodoContinuationYieldsToPendingUserWork(t *testing.T) {
102 c, prov := manualReadinessController(t, [][]provider.Chunk{
103 {toolCallChunk("todo-1", "todo_write", `{"todos":[{"content":"重写第 4 节","status":"in_progress"}]}`), {Type: provider.ChunkDone}},
104 textTurn("等待下一步。"),
105 textTurn("不应被隐藏续跑消费"),
106 })
107 c.executor.Session().Add(provider.Message{Role: provider.RoleAssistant, Content: "让我写完整新第 4 节。"})
108 c.mu.Lock()
109 c.turns.cancelRequested = true
110 c.turns.phase = session.RuntimeCancelling
111 c.mu.Unlock()
112
113 if err := newTurnOrchestrator(c).runGoalLoopWithRawDisplay(context.Background(), "开始", "开始", "开始"); err != nil {
114 t.Fatalf("standard start reply returned %v", err)
115 }
116 if prov.call != 2 {
117 t.Fatalf("provider calls = %d, want pending user work to preempt continuation", prov.call)
118 }
119 }
120
121 func TestRetiredDeliverySettingEndsOrdinaryTurnWithoutRecoveryPause(t *testing.T) {
122 c, prov := manualReadinessController(t, [][]provider.Chunk{
123 {toolCallChunk("write", "write_file", `{"path":"main.go"}`), {Type: provider.ChunkDone}},
124 textTurn("已完成修改,但没有执行验证。"),
125 textTurn("显式恢复才允许消费这一轮"),
126 })
127 if err := c.SetQualityFloor(QualityFloorDelivery); err != nil {
128 t.Fatalf("SetQualityFloor: %v", err)
129 }
130
131 if err := newTurnOrchestrator(c).runGoalLoopWithRawDisplay(context.Background(), "修改 main.go", "修改 main.go", ""); err != nil {
132 t.Fatalf("ordinary turn returned %v", err)
133 }
134 if prov.call != 2 {
135 t.Fatalf("provider calls = %d, want one tool round plus one final answer", prov.call)
136 }
137 if got := syntheticUserTurnCount(c.executor.Session().Snapshot()); got != 0 {
138 t.Fatalf("synthetic user turns = %d, want zero before explicit recovery", got)
139 }
140 }
141
141 lines GO