detached 46fa9b3

test(app): cover command palette integration

commit
46fa9b3160128786799336efe5795f90b7303b3c
author
brice · · 2026-07-03 05:47 UTC
committer
brice · · 2026-07-03 07:35 UTC
parent
e431dff
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +61 -2
M internal/app/daemon_integration_test.go +61 -2
@@ -86,6 +86,13 @@ func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size)
8686 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once
8787 // the reconstructed grid contains want.
8888 func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {
89+ t.Helper()
90+ _ = awaitScreenText(t, p, sz, want)
91+}
92+
93+// awaitScreenText is like awaitText, but returns the reconstructed screen text
94+// at the point the wanted text appears so callers can make additional checks.
95+func awaitScreenText(t *testing.T, p *pump, sz domain.Size, want string) string {
8996 t.Helper()
9097 screen := vt.NewScreen(sz.Cols, sz.Rows)
9198 timeout := time.After(5 * time.Second)
@@ -99,8 +106,9 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {
99106 o, err := ports.UnmarshalOutput(f.Payload)
100107 require.NoError(t, err)
101108 screen.Write(o.Data)
102- if strings.Contains(screenText(screen), want) {
103- return
109+ text := screenText(screen)
110+ if strings.Contains(text, want) {
111+ return text
104112 }
105113 }
106114 case <-timeout:
@@ -109,6 +117,28 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {
109117 }
110118 }
111119
120+func assertNoTextAfterInput(t *testing.T, p *pump, sz domain.Size, absent string) {
121+ t.Helper()
122+ screen := vt.NewScreen(sz.Cols, sz.Rows)
123+ timeout := time.After(300 * time.Millisecond)
124+ for {
125+ select {
126+ case f, ok := <-p.ch:
127+ if !ok {
128+ return
129+ }
130+ if f.Type == ports.MsgOutput {
131+ o, err := ports.UnmarshalOutput(f.Payload)
132+ require.NoError(t, err)
133+ screen.Write(o.Data)
134+ require.NotContains(t, screenText(screen), absent)
135+ }
136+ case <-timeout:
137+ return
138+ }
139+ }
140+}
141+
112142 func screenText(s *vt.Screen) string {
113143 var b strings.Builder
114144 for y := range s.Frame.Height {
@@ -144,6 +174,35 @@ func TestIntegration_InputRoundtrip(t *testing.T) {
144174 awaitText(t, p, sz, "PINGPONG")
145175 }
146176
177+func TestIntegration_CommandPaletteCreatesTab(t *testing.T) {
178+ sz := domain.Size{Cols: 80, Rows: 24}
179+ dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
180+
181+ tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
182+ defer func() { _ = tr.Close() }()
183+ awaitText(t, p, sz, "READY")
184+
185+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
186+ awaitText(t, p, sz, "Commands")
187+
188+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))
189+ text := awaitScreenText(t, p, sz, " 1 2 ")
190+ require.Contains(t, text, " 1 2 ")
191+}
192+
193+func TestIntegration_AltCWithoutPaletteDoesNotCreateTab(t *testing.T) {
194+ sz := domain.Size{Cols: 80, Rows: 24}
195+ dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
196+
197+ tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
198+ defer func() { _ = tr.Close() }()
199+ text := awaitScreenText(t, p, sz, "READY")
200+ require.NotContains(t, text, " 1 2 ")
201+
202+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1bc")})}))
203+ assertNoTextAfterInput(t, p, sz, " 1 2 ")
204+}
205+
147206 func TestIntegration_EphemeralGoneOnDetach(t *testing.T) {
148207 sz := domain.Size{Cols: 80, Rows: 24}
149208 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
@@ -86,6 +86,13 @@ func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size)
86 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once86 // awaitText decodes MsgOutput frames into a fresh VT screen and returns once
87 // the reconstructed grid contains want.87 // the reconstructed grid contains want.
88 func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {88 func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {
89+ t.Helper()
90+ _ = awaitScreenText(t, p, sz, want)
91+}
92+
93+// awaitScreenText is like awaitText, but returns the reconstructed screen text
94+// at the point the wanted text appears so callers can make additional checks.
95+func awaitScreenText(t *testing.T, p *pump, sz domain.Size, want string) string {
89 t.Helper()96 t.Helper()
90 screen := vt.NewScreen(sz.Cols, sz.Rows)97 screen := vt.NewScreen(sz.Cols, sz.Rows)
91 timeout := time.After(5 * time.Second)98 timeout := time.After(5 * time.Second)
@@ -99,8 +106,9 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {
99 o, err := ports.UnmarshalOutput(f.Payload)106 o, err := ports.UnmarshalOutput(f.Payload)
100 require.NoError(t, err)107 require.NoError(t, err)
101 screen.Write(o.Data)108 screen.Write(o.Data)
102- if strings.Contains(screenText(screen), want) {109+ text := screenText(screen)
103- return110+ if strings.Contains(text, want) {
111+ return text
104 }112 }
105 }113 }
106 case <-timeout:114 case <-timeout:
@@ -109,6 +117,28 @@ func awaitText(t *testing.T, p *pump, sz domain.Size, want string) {
109 }117 }
110 }118 }
111 119
120+func assertNoTextAfterInput(t *testing.T, p *pump, sz domain.Size, absent string) {
121+ t.Helper()
122+ screen := vt.NewScreen(sz.Cols, sz.Rows)
123+ timeout := time.After(300 * time.Millisecond)
124+ for {
125+ select {
126+ case f, ok := <-p.ch:
127+ if !ok {
128+ return
129+ }
130+ if f.Type == ports.MsgOutput {
131+ o, err := ports.UnmarshalOutput(f.Payload)
132+ require.NoError(t, err)
133+ screen.Write(o.Data)
134+ require.NotContains(t, screenText(screen), absent)
135+ }
136+ case <-timeout:
137+ return
138+ }
139+ }
140+}
141+
112 func screenText(s *vt.Screen) string {142 func screenText(s *vt.Screen) string {
113 var b strings.Builder143 var b strings.Builder
114 for y := range s.Frame.Height {144 for y := range s.Frame.Height {
@@ -144,6 +174,35 @@ func TestIntegration_InputRoundtrip(t *testing.T) {
144 awaitText(t, p, sz, "PINGPONG")174 awaitText(t, p, sz, "PINGPONG")
145 }175 }
146 176
177+func TestIntegration_CommandPaletteCreatesTab(t *testing.T) {
178+ sz := domain.Size{Cols: 80, Rows: 24}
179+ dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
180+
181+ tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
182+ defer func() { _ = tr.Close() }()
183+ awaitText(t, p, sz, "READY")
184+
185+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
186+ awaitText(t, p, sz, "Commands")
187+
188+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))
189+ text := awaitScreenText(t, p, sz, " 1 2 ")
190+ require.Contains(t, text, " 1 2 ")
191+}
192+
193+func TestIntegration_AltCWithoutPaletteDoesNotCreateTab(t *testing.T) {
194+ sz := domain.Size{Cols: 80, Rows: 24}
195+ dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
196+
197+ tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)
198+ defer func() { _ = tr.Close() }()
199+ text := awaitScreenText(t, p, sz, "READY")
200+ require.NotContains(t, text, " 1 2 ")
201+
202+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1bc")})}))
203+ assertNoTextAfterInput(t, p, sz, " 1 2 ")
204+}
205+
147 func TestIntegration_EphemeralGoneOnDetach(t *testing.T) {206 func TestIntegration_EphemeralGoneOnDetach(t *testing.T) {
148 sz := domain.Size{Cols: 80, Rows: 24}207 sz := domain.Size{Cols: 80, Rows: 24}
149 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))208 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))

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 2

Tags 13

↑↓ moveenter openesc close