1package main
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/bnema/zut/packages/agent/ext"
8)
9
10func TestBuildChromeHidesEmptyPlan(t *testing.T) {
11 chrome, visible := buildChrome(createEmptyState())
12 if visible {
13 t.Fatal("empty plan should not publish persistent chrome")
14 }
15 if chrome.summary != "" || len(chrome.lines) != 0 {
16 t.Fatalf("empty chrome = %#v, want zero content", chrome)
17 }
18}
19
20func TestBuildChromeUsesCompactSummaryWithoutSpecText(t *testing.T) {
21 state := PlanState{
22 Version: 1,
23 Spec: "private spec text must not be persistent chrome",
24 Phases: []Phase{
25 {
26 ID: "phase-1",
27 Title: "Finished discovery",
28 Tasks: []PhaseTask{{ID: "task-1", Text: "Completed discovery", Checked: true}},
29 },
30 {
31 ID: "phase-2",
32 Title: "Implement compact output",
33 Tasks: []PhaseTask{
34 {ID: "task-2", Text: "Completed implementation", Checked: true},
35 {ID: "task-3", Text: "Remaining implementation task"},
36 },
37 },
38 },
39 CurrentPhaseID: "phase-2",
40 }
41
42 chrome, visible := buildChrome(state)
43 if !visible {
44 t.Fatal("stored plan should publish persistent chrome")
45 }
46 if want := "p 1/2 | t 2/3"; chrome.summary != want {
47 t.Fatalf("chrome summary = %q, want %q", chrome.summary, want)
48 }
49 if strings.Contains(chrome.summary, state.Spec) || strings.Contains(chrome.summary, state.Phases[1].Title) {
50 t.Fatalf("compact summary leaked plan text: %q", chrome.summary)
51 }
52 joined := strings.Join(chrome.lines, "\n")
53 for _, want := range []string{
54 "[x] Finished discovery 1/1",
55 " [x] Completed discovery",
56 "[>] Implement compact output 1/2",
57 " [>] Remaining implementation task",
58 } {
59 if !strings.Contains(joined, want) {
60 t.Fatalf("checklist missing %q: %s", want, joined)
61 }
62 }
63}
64
65func TestBuildChromeHidesCompletePlan(t *testing.T) {
66 closedAt := int64(1)
67 state := PlanState{
68 Version: 1,
69 Phases: []Phase{{
70 ID: "phase-1",
71 Title: "Ship",
72 Tasks: []PhaseTask{{ID: "task-1", Text: "Done", Checked: true}},
73 }},
74 ClosedAt: &closedAt,
75 }
76
77 if got, want := buildChromeSummary(state), "p 1/1 | t 1/1"; got != want {
78 t.Fatalf("closed chrome summary = %q, want %q", got, want)
79 }
80 if _, visible := buildChrome(state); visible {
81 t.Fatal("complete plan should not publish persistent chrome")
82 }
83}
84
85func TestBuildChromeHidesCompletePlanBeforeCloseMetadata(t *testing.T) {
86 state := PlanState{
87 Version: 1,
88 Spec: "complete plan",
89 Phases: []Phase{{
90 ID: "phase-1",
91 Title: "Ship",
92 Tasks: []PhaseTask{{ID: "task-1", Text: "Done", Checked: true}},
93 }},
94 }
95
96 if _, visible := buildChrome(state); visible {
97 t.Fatal("complete plan without close metadata should not publish persistent chrome")
98 }
99}
100
101type chromeRecorder struct {
102 statusSet int
103 statusClear int
104 statusTexts []string
105 widgetSet int
106 widgetClear int
107 widgetPositions []string
108 widgetTitles []string
109 widgetLines [][]string
110}
111
112func (r *chromeRecorder) SetStatus(_, text string) {
113 r.statusSet++
114 r.statusTexts = append(r.statusTexts, text)
115}
116func (r *chromeRecorder) ClearStatus(string) { r.statusClear++ }
117func (r *chromeRecorder) SetWidget(_, position, title string, lines []string) {
118 r.widgetSet++
119 r.widgetPositions = append(r.widgetPositions, position)
120 r.widgetTitles = append(r.widgetTitles, title)
121 r.widgetLines = append(r.widgetLines, append([]string(nil), lines...))
122}
123func (r *chromeRecorder) ClearWidget(string) { r.widgetClear++ }
124
125func TestApplyChromePublishesCompactStatusAndTitle(t *testing.T) {
126 recorder := &chromeRecorder{}
127 state := PlanState{
128 Version: 1,
129 Spec: "must not be shown",
130 Phases: []Phase{
131 {
132 ID: "phase-1",
133 Title: "Finished",
134 Tasks: []PhaseTask{{ID: "task-1", Text: "Ship", Checked: true}},
135 },
136 {
137 ID: "phase-2",
138 Title: "Pending",
139 Tasks: []PhaseTask{{ID: "task-2", Text: "Review"}},
140 },
141 },
142 }
143 applyChrome(recorder, state)
144
145 if recorder.widgetSet != 1 || len(recorder.widgetPositions) != 1 || recorder.widgetPositions[0] != ext.WidgetPositionRightBar {
146 t.Fatalf("widget placement = %v, want %q", recorder.widgetPositions, ext.WidgetPositionRightBar)
147 }
148 want := buildChromeSummary(state)
149 if len(recorder.statusTexts) != 1 || recorder.statusTexts[0] != want {
150 t.Fatalf("status updates = %v, want %q", recorder.statusTexts, want)
151 }
152 if len(recorder.widgetTitles) != 1 || recorder.widgetTitles[0] != want {
153 t.Fatalf("widget titles = %v, want %q", recorder.widgetTitles, want)
154 }
155 if len(recorder.widgetLines) != 1 || len(recorder.widgetLines[0]) == 0 {
156 t.Fatalf("widget lines = %v, want checklist rows", recorder.widgetLines)
157 }
158 if !strings.Contains(strings.Join(recorder.widgetLines[0], "\n"), "[>] Review") {
159 t.Fatalf("active task is missing from widget lines: %v", recorder.widgetLines[0])
160 }
161}
162
163func TestApplyChromeClearsEmptyPlan(t *testing.T) {
164 recorder := &chromeRecorder{}
165 applyChrome(recorder, createEmptyState())
166
167 if recorder.statusClear != 1 || recorder.widgetClear != 1 {
168 t.Fatalf("empty plan clear operations = status %d, widget %d; want one each", recorder.statusClear, recorder.widgetClear)
169 }
170 if recorder.statusSet != 0 || recorder.widgetSet != 0 {
171 t.Fatalf("empty plan published updates = status %d, widget %d; want none", recorder.statusSet, recorder.widgetSet)
172 }
173}
174
175func TestApplyChromeClearsCompletedPlan(t *testing.T) {
176 recorder := &chromeRecorder{}
177 state := PlanState{
178 Version: 1,
179 Phases: []Phase{{
180 ID: "phase-1",
181 Title: "Ship",
182 Tasks: []PhaseTask{{ID: "task-1", Text: "Done", Checked: true}},
183 }},
184 }
185 applyChrome(recorder, state)
186
187 if recorder.statusClear != 1 || recorder.widgetClear != 1 {
188 t.Fatalf("complete plan clear operations = status %d, widget %d; want one each", recorder.statusClear, recorder.widgetClear)
189 }
190 if recorder.statusSet != 0 || recorder.widgetSet != 0 {
191 t.Fatalf("complete plan published updates = status %d, widget %d; want none", recorder.statusSet, recorder.widgetSet)
192 }
193}
194
195func TestApplyChromeShowsEmptySummaryWithoutSpecText(t *testing.T) {
196 recorder := &chromeRecorder{}
197 state := PlanState{Version: 1, Spec: "spec only"}
198 applyChrome(recorder, state)
199
200 if recorder.statusClear != 0 || recorder.widgetClear != 0 || recorder.statusSet != 1 || recorder.widgetSet != 1 {
201 t.Fatalf("spec-only chrome lifecycle = %#v, want a compact update", recorder)
202 }
203 if got, want := recorder.statusTexts[0], "p 0/0 | t 0/0"; got != want {
204 t.Fatalf("spec-only status = %q, want %q", got, want)
205 }
206 if got, want := recorder.widgetTitles[0], "p 0/0 | t 0/0"; got != want {
207 t.Fatalf("spec-only widget title = %q, want %q", got, want)
208 }
209 if len(recorder.widgetLines[0]) != 1 || recorder.widgetLines[0][0] != "No phases yet" {
210 t.Fatalf("spec-only widget lines = %v, want empty-plan hint", recorder.widgetLines[0])
211 }
212}