v0.50.1 a637010

ext_notes_test.go RawHistoryFolder

1package modes
2
3import (
4	"fmt"
5	"strings"
6	"testing"
7
8	"github.com/bnema/zut/packages/tui"
9)
10
11func newNotesTestInteractive() *Interactive {
12	i := &Interactive{dirty: make(chan struct{}, 1)}
13	i.cfg.Theme = tui.Theme{
14		Muted: tui.Color256(8), Warning: tui.Color256(3), Error: tui.Color256(1),
15		Tool: tui.Color256(2), Accent: tui.Color256(4),
16	}
17	return i
18}
19
20func TestClearNotesRemovesOnlyOwnerNotes(t *testing.T) {
21	i := newNotesTestInteractive()
22
23	i.Notify("kagi", "info", "pending")
24	i.Notify("kagi", "success", "approved")
25	i.Notify("other", "info", "keep me")
26
27	if len(i.extNotes) != 3 {
28		t.Fatalf("expected 3 notes, got %d", len(i.extNotes))
29	}
30
31	i.ClearNotes("kagi")
32
33	if len(i.extNotes) != 1 {
34		t.Fatalf("expected 1 note after clear, got %d: %v", len(i.extNotes), i.extNotes)
35	}
36	if !strings.Contains(i.extNotes[0], "[other] ") {
37		t.Fatalf("expected the surviving note to belong to other, got %q", i.extNotes[0])
38	}
39}
40
41func TestClearNotesNoMatchKeepsNotes(t *testing.T) {
42	i := newNotesTestInteractive()
43	i.Notify("kagi", "info", "pending")
44
45	i.ClearNotes("nope")
46
47	if len(i.extNotes) != 1 {
48		t.Fatalf("expected note to survive, got %d", len(i.extNotes))
49	}
50}
51
52func TestPersistentExtensionWidgetRowsAreBounded(t *testing.T) {
53	i := newNotesTestInteractive()
54	lines := make([]string, 20)
55	for idx := range lines {
56		lines[idx] = "line"
57	}
58	i.SetWidget("extension", "widget", "above_input", "Title", lines)
59
60	i.mu.Lock()
61	got := i.extensionChromeLinesLocked(80)
62	i.mu.Unlock()
63	if len(got) != maxExtensionWidgetRows {
64		t.Fatalf("widget rows = %d, want %d: %v", len(got), maxExtensionWidgetRows, got)
65	}
66	if !strings.Contains(strings.Join(got, "\n"), "extension widgets truncated") {
67		t.Fatalf("bounded widget rows omitted truncation marker: %v", got)
68	}
69}
70
71func TestPersistentExtensionStatusRowsAreBounded(t *testing.T) {
72	i := newNotesTestInteractive()
73	for n := 0; n < maxExtensionStatusRows+4; n++ {
74		i.SetStatus("extension", fmt.Sprintf("status-%d", n), "info", "line")
75	}
76
77	i.mu.Lock()
78	got := i.extensionChromeLinesLocked(80)
79	i.mu.Unlock()
80	if len(got) != maxExtensionStatusRows {
81		t.Fatalf("status rows = %d, want %d: %v", len(got), maxExtensionStatusRows, got)
82	}
83	if !strings.Contains(strings.Join(got, "\n"), "extension statuses truncated") {
84		t.Fatalf("bounded status rows omitted truncation marker: %v", got)
85	}
86}
87
88func TestPersistentExtensionStatusAndWidget(t *testing.T) {
89	i := newNotesTestInteractive()
90	i.SetStatus("sample", "progress", "success", "2/4 tasks checked")
91	i.SetWidget("sample", "plan", "above_input", "Plan", []string{"Current phase: parse", "[ ] read files"})
92
93	i.mu.Lock()
94	lines := i.extensionChromeLinesLocked(80)
95	statusKey := i.extensionStatusesKeyLocked()
96	widgetKey := i.extensionWidgetsKeyLocked()
97	i.mu.Unlock()
98	joined := strings.Join(lines, "\n")
99	if !strings.Contains(joined, "2/4 tasks checked") || !strings.Contains(joined, "Plan") || !strings.Contains(joined, "[ ] read files") {
100		t.Fatalf("persistent extension chrome = %q", joined)
101	}
102	if !strings.Contains(statusKey, "sample/progress/success/2/4 tasks checked") {
103		t.Fatalf("status cache key = %q", statusKey)
104	}
105	if !strings.Contains(widgetKey, "sample/plan/above_input/Plan") {
106		t.Fatalf("widget cache key = %q", widgetKey)
107	}
108
109	i.SetStatus("sample", "progress", "", "")
110	i.ClearWidget("sample", "plan")
111	if len(i.extStatuses) != 0 || len(i.extWidgets) != 0 {
112		t.Fatalf("persistent extension chrome was not cleared: statuses=%v widgets=%v", i.extStatuses, i.extWidgets)
113	}
114}
115
116func TestClearExtensionChromeRemovesOnlyTheExitedExtension(t *testing.T) {
117	i := newNotesTestInteractive()
118	i.SetStatus("gone", "progress", "success", "done")
119	i.SetWidget("gone", "plan", "above_input", "Gone", []string{"old"})
120	i.SetStatus("keep", "progress", "info", "still here")
121	i.SetWidget("keep", "plan", "above_input", "Keep", []string{"new"})
122	i.extNotes = []string{"[gone] old note", "[keep] keep note"}
123
124	i.ClearExtensionChrome("gone")
125
126	if _, ok := i.extStatuses["gone"]; ok {
127		t.Fatal("exited extension status survived")
128	}
129	if _, ok := i.extWidgets["gone"]; ok {
130		t.Fatal("exited extension widget survived")
131	}
132	if _, ok := i.extStatuses["keep"]; !ok {
133		t.Fatal("unrelated extension status was removed")
134	}
135	if _, ok := i.extWidgets["keep"]; !ok {
136		t.Fatal("unrelated extension widget was removed")
137	}
138	if got := strings.Join(i.extNotes, "\n"); got != "[keep] keep note" {
139		t.Fatalf("extension notes after cleanup = %q", got)
140	}
141}
142
143func TestInteractiveRedrawPlacesWidgetAboveInput(t *testing.T) {
144	term := &alertTestTerminal{}
145	i := NewInteractive(InteractiveConfig{Terminal: term, Theme: tui.Dark})
146	i.rend.Resize(80, 24)
147	i.SetWidget("sample", "plan", "above_input", "Plan", []string{"[ ] read files"})
148	i.redraw()
149
150	out := stripANSIBytes(term.String())
151	if !strings.Contains(out, "Plan") || !strings.Contains(out, "[ ] read files") {
152		t.Fatalf("redraw omitted above-input widget: %q", out)
153	}
154}

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