返回 DeepSeek-Reasonix
planner_gate_test.go
根目录 / internal / control / planner_gate_test.go
1 package control
2
3 import (
4 "context"
5 "testing"
6
7 "reasonix/internal/agent"
8 )
9
10 func TestTaskWarrantsPlanner(t *testing.T) {
11 cases := []struct {
12 input string
13 want bool
14 }{
15 {"", false},
16 {" ", false},
17 {"/init", false},
18 {"1", false},
19 {"好的", false},
20 {"what does this function do?", false},
21 {"解释一下这段代码", false},
22 {"fix the bug", false},
23 {"复杂重构认证迁移", false},
24 {"直接修改 parser.go", false},
25 {"先规划这个认证迁移,不要执行", true},
26 {"plan first then implement the cache", true},
27 {"give me a plan only", true},
28 {"fix auth and wait for my approval", true},
29 }
30 for _, c := range cases {
31 if got := TaskWarrantsPlanner(c.input); got != c.want {
32 t.Errorf("TaskWarrantsPlanner(%q) = %v, want %v", c.input, got, c.want)
33 }
34 }
35 }
36
37 func TestNewPlannerGateIsExplicitOnly(t *testing.T) {
38 gate := NewPlannerGate()
39 if gate == nil {
40 t.Fatal("NewPlannerGate returned nil")
41 }
42 if got := gate(context.Background(), "what is this?"); got {
43 t.Error("planner gate should skip questions")
44 }
45 if got := gate(context.Background(), "fix the bug"); got {
46 t.Error("ordinary work must stay executor-only")
47 }
48 if got := gate(context.Background(), "先规划再执行认证迁移"); !got {
49 t.Error("explicit plan-then-execute must call planner")
50 }
51 }
52
53 func TestDecidePlannerRouteExplicitOnly(t *testing.T) {
54 cases := []struct {
55 name string
56 input string
57 meta plannerTurnMetadata
58 route agent.PlannerRoute
59 reason string
60 }{
61 {
62 name: "explicit plan mode bypasses dual planner",
63 input: "fix the bug",
64 meta: plannerTurnMetadata{ExplicitPlanMode: true},
65 route: agent.PlannerRouteExecutorOnly,
66 reason: plannerReasonExplicitPlanMode,
67 },
68 {
69 name: "trusted synthetic turn bypasses dual planner",
70 input: "perform a brand new implementation",
71 meta: plannerTurnMetadata{Synthetic: true},
72 route: agent.PlannerRouteExecutorOnly,
73 reason: plannerReasonSynthetic,
74 },
75 {
76 name: "user text matching a legacy host prefix stays user authored",
77 input: agent.CompletionValidationContinuationPrefix + " give me a plan only",
78 meta: plannerTurnMetadata{UserText: agent.CompletionValidationContinuationPrefix + " give me a plan only"},
79 route: agent.PlannerRoutePlanOnly,
80 reason: plannerReasonUserPlanOnly,
81 },
82 {
83 name: "user asks for plan only",
84 input: "先规划这个认证迁移,不要执行",
85 route: agent.PlannerRoutePlanOnly,
86 reason: plannerReasonUserPlanOnly,
87 },
88 {
89 name: "user asks for plan then execute",
90 input: "先规划再执行这个缓存改造",
91 route: agent.PlannerRoutePlanAndExecute,
92 reason: plannerReasonUserPlanAndExecute,
93 },
94 {
95 name: "user asks for approval",
96 input: "plan the migration and wait for my approval",
97 route: agent.PlannerRoutePlanForApproval,
98 reason: plannerReasonUserPlanApproval,
99 },
100 {
101 name: "direct request stays executor only",
102 input: "直接修改 parser.go",
103 route: agent.PlannerRouteExecutorOnly,
104 reason: plannerReasonUserDirect,
105 },
106 {
107 name: "ordinary long multi-file request stays executor only",
108 input: "refactor the parser across reader.py and writer.py and add tests",
109 route: agent.PlannerRouteExecutorOnly,
110 reason: plannerReasonDefault,
111 },
112 {
113 name: "auth wording does not auto plan",
114 input: "修复登录超时",
115 route: agent.PlannerRouteExecutorOnly,
116 reason: plannerReasonDefault,
117 },
118 {
119 name: "explicit goal start plans once",
120 input: "fix the crash",
121 meta: plannerTurnMetadata{ExplicitGoalStart: true},
122 route: agent.PlannerRoutePlanAndExecute,
123 reason: plannerReasonGoalStart,
124 },
125 {
126 name: "context dependent fix stays with executor",
127 input: "fix it",
128 meta: plannerTurnMetadata{HasConversationContext: true},
129 route: agent.PlannerRouteExecutorOnly,
130 reason: plannerReasonContextContinuation,
131 },
132 }
133 for _, tc := range cases {
134 t.Run(tc.name, func(t *testing.T) {
135 ctx := withPlannerTurnMetadata(context.Background(), tc.meta)
136 got := DecidePlannerRoute(ctx, tc.input)
137 if got.Route != tc.route || got.Reason != tc.reason {
138 t.Fatalf("decision = %+v, want route=%s reason=%s", got, tc.route, tc.reason)
139 }
140 })
141 }
142 }
143
144 func TestPlannerPolicyUsesPristineMetadataInsteadOfInjectedContext(t *testing.T) {
145 ctx := withPlannerTurnMetadata(context.Background(), plannerTurnMetadata{
146 UserText: "fix typo in README",
147 })
148 input := activeGoalBlock("migrate authentication across the backend") +
149 "\n\n<capability-route>\nhigh risk migration\n</capability-route>\n\nfix typo in README"
150 got := DecidePlannerRoute(ctx, input)
151 if got.Route != agent.PlannerRouteExecutorOnly {
152 t.Fatalf("decision used injected context instead of pristine user text: %+v", got)
153 }
154 }
155
155 lines GO