返回 DeepSeek-Reasonix
plan_test.go
根目录 / internal / plancontract / plan_test.go
1 package plancontract
2
3 import (
4 "strings"
5 "testing"
6 )
7
8 func TestNormalizeAssignsMissingIDsWithoutColliding(t *testing.T) {
9 p := Plan{Objective: "o", Steps: []Step{
10 {Title: "first"},
11 {ID: "s1", Title: "second"},
12 {Title: "third"},
13 }}.Normalize()
14
15 got := []string{p.Steps[0].ID, p.Steps[1].ID, p.Steps[2].ID}
16 want := []string{"plan_step_01", "s1", "plan_step_02"}
17 for i := range want {
18 if got[i] != want[i] {
19 t.Fatalf("step ids = %v, want %v", got, want)
20 }
21 }
22 }
23
24 func TestNormalizeReassignsDuplicateStepIDs(t *testing.T) {
25 p := Plan{Objective: "o", Steps: []Step{
26 {ID: "a", Title: "first"},
27 {ID: "a", Title: "second"},
28 }}.Normalize()
29
30 if p.Steps[0].ID != "a" {
31 t.Fatalf("first step id = %q, want the submitted %q", p.Steps[0].ID, "a")
32 }
33 if p.Steps[1].ID == "a" {
34 t.Fatal("duplicate step id survived normalization")
35 }
36 if err := p.Validate(); err != nil {
37 t.Fatalf("normalized plan must validate: %v", err)
38 }
39 }
40
41 func TestNormalizeAssignsCriterionIDsAcrossSteps(t *testing.T) {
42 p := Plan{Objective: "o", Steps: []Step{
43 {ID: "a", Title: "first", Acceptance: []Criterion{{Text: "one"}}},
44 {ID: "b", Title: "second", Acceptance: []Criterion{{Text: "two"}, {Text: "three"}}},
45 }}.Normalize()
46
47 seen := map[string]bool{}
48 for _, step := range p.Steps {
49 for _, c := range step.Acceptance {
50 if c.ID == "" {
51 t.Fatalf("criterion %q has no id", c.Text)
52 }
53 if seen[c.ID] {
54 t.Fatalf("criterion id %q reused across steps", c.ID)
55 }
56 seen[c.ID] = true
57 }
58 }
59 }
60
61 func TestNormalizeDropsEmptyEntriesAndDeduplicates(t *testing.T) {
62 p := Plan{
63 Objective: " ship it ",
64 NonGoals: []string{" rewrite the world ", "", "rewrite the world"},
65 Assumptions: []Assumption{{Text: " "}, {Text: "cache is warm", Confirm: " check hit rate "}},
66 Steps: []Step{
67 {Title: " "},
68 {Title: " do it ", VerifiedFiles: []string{"a.go", "a.go", " b.go "}, Risks: []string{""},
69 Acceptance: []Criterion{{Text: " "}},
70 Verification: []Verification{{Command: " ", Expect: " "}}},
71 },
72 }.Normalize()
73
74 if p.Objective != "ship it" {
75 t.Fatalf("objective = %q", p.Objective)
76 }
77 if len(p.NonGoals) != 1 || p.NonGoals[0] != "rewrite the world" {
78 t.Fatalf("non-goals = %v", p.NonGoals)
79 }
80 if len(p.Assumptions) != 1 || p.Assumptions[0].Confirm != "check hit rate" {
81 t.Fatalf("assumptions = %+v", p.Assumptions)
82 }
83 if len(p.Steps) != 1 || p.Steps[0].Title != "do it" {
84 t.Fatalf("steps = %+v", p.Steps)
85 }
86 step := p.Steps[0]
87 if len(step.VerifiedFiles) != 2 || step.VerifiedFiles[1] != "b.go" {
88 t.Fatalf("verified files = %v", step.VerifiedFiles)
89 }
90 if step.Risks != nil || step.Acceptance != nil || step.Verification != nil {
91 t.Fatalf("empty entries survived: %+v", step)
92 }
93 }
94
95 func TestNormalizeRepairsParentReferences(t *testing.T) {
96 p := Plan{Objective: "o", Steps: []Step{
97 {ID: "phase", Title: "phase"},
98 {ID: "child", ParentID: "phase", Title: "child"},
99 {ID: "grandchild", ParentID: "child", Title: "grandchild"},
100 {ID: "orphan", ParentID: "nobody", Title: "orphan"},
101 {ID: "self", ParentID: "self", Title: "self"},
102 }}.Normalize()
103
104 want := map[string]string{
105 "phase": "", "child": "phase", "grandchild": "phase", "orphan": "", "self": "",
106 }
107 for _, step := range p.Steps {
108 if got := step.ParentID; got != want[step.ID] {
109 t.Errorf("step %q parent = %q, want %q", step.ID, got, want[step.ID])
110 }
111 }
112 }
113
114 func TestNormalizeDropsUnresolvableDependencies(t *testing.T) {
115 p := Plan{Objective: "o", Steps: []Step{
116 {ID: "a", Title: "a", DependsOn: []string{"a", "ghost", "b", "b"}},
117 {ID: "b", Title: "b"},
118 }}.Normalize()
119
120 got := p.Steps[0].DependsOn
121 if len(got) != 1 || got[0] != "b" {
122 t.Fatalf("depends-on = %v, want [b]", got)
123 }
124 }
125
126 func TestNormalizeIsIdempotent(t *testing.T) {
127 once := Plan{Objective: "o", Steps: []Step{
128 {Title: "phase"},
129 {ParentID: "s1", Title: "child", DependsOn: []string{"s1"}},
130 }}.Normalize()
131 twice := once.Normalize()
132
133 if len(once.Steps) != len(twice.Steps) {
134 t.Fatalf("step count changed: %d then %d", len(once.Steps), len(twice.Steps))
135 }
136 for i := range once.Steps {
137 if once.Steps[i].ID != twice.Steps[i].ID || once.Steps[i].ParentID != twice.Steps[i].ParentID {
138 t.Fatalf("step %d changed on re-normalize: %+v then %+v", i, once.Steps[i], twice.Steps[i])
139 }
140 }
141 }
142
143 func TestValidateRejectsMissingObjectiveAndSteps(t *testing.T) {
144 err := Plan{}.Validate()
145 if err == nil {
146 t.Fatal("empty plan must not validate")
147 }
148 for _, want := range []string{"no objective", "no steps"} {
149 if !strings.Contains(err.Error(), want) {
150 t.Errorf("error %q missing %q", err, want)
151 }
152 }
153 }
154
155 func TestValidateRejectsOversizedPlan(t *testing.T) {
156 steps := make([]Step, MaxSteps+1)
157 for i := range steps {
158 steps[i] = Step{Title: "step"}
159 }
160 err := Plan{Objective: "o", Steps: steps}.Normalize().Validate()
161 if err == nil || !strings.Contains(err.Error(), "the limit is") {
162 t.Fatalf("oversized plan error = %v", err)
163 }
164 }
165
166 func TestValidateRejectsRawDuplicateIDs(t *testing.T) {
167 err := Plan{Objective: "o", Steps: []Step{
168 {ID: "a", Title: "one"},
169 {ID: "a", Title: "two"},
170 }}.Validate()
171 if err == nil || !strings.Contains(err.Error(), "used more than once") {
172 t.Fatalf("duplicate id error = %v", err)
173 }
174 }
175
176 func TestValidateReportsDependencyCycle(t *testing.T) {
177 err := Plan{Objective: "o", Steps: []Step{
178 {ID: "a", Title: "a", DependsOn: []string{"b"}},
179 {ID: "b", Title: "b", DependsOn: []string{"a"}},
180 }}.Normalize().Validate()
181 if err == nil || !strings.Contains(err.Error(), "dependency cycle") {
182 t.Fatalf("cycle error = %v", err)
183 }
184 }
185
186 func TestValidateAcceptsWellFormedPlan(t *testing.T) {
187 p := Plan{
188 Objective: "make the cache key model-aware",
189 Steps: []Step{
190 {ID: "p1", Title: "thread the model ref through"},
191 {ID: "s1", ParentID: "p1", Title: "extend cacheKey"},
192 {ID: "s2", ParentID: "p1", Title: "update callers", DependsOn: []string{"s1"}},
193 },
194 }.Normalize()
195 if err := p.Validate(); err != nil {
196 t.Fatalf("well-formed plan must validate: %v", err)
197 }
198 }
199
199 lines GO