v0.4.2 83c2c60

main_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 1

Tags 476

↑↓ moveenter openesc close