detached ca92619

test(daemon): repair vacuous assertion and undeferred cleanup in notices tests

model_test.go's degenerate-dimensions test asserted frame dims are
never negative, which Render's max(_, 0) clamping already guarantees
by construction; assert the expected clamped dimensions per case
instead.

notices_overlay_test.go's q/Ctrl-C close test called release() as the
loop body's final statement, so a require failure on the 'q' iteration
would Goexit the test, skip the 0x03 iteration, and leak the blocking
PTY. Move each iteration into t.Run with a deferred release().
commit
ca926196a4bb024b5937cb40b96cfa1a48e8c352
author
brice · · 2026-07-20 13:41 UTC
parent
211b2e1
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +36 -19
M internal/usecase/daemon/notices_overlay_test.go +17 -14
@@ -1,6 +1,7 @@
11 package daemon
22
33 import (
4+ "fmt"
45 "testing"
56 "time"
67
@@ -72,20 +73,22 @@ func TestNoticesJKNavigatesSelection(t *testing.T) {
7273
7374 func TestNoticesQAndCtrlCCloseImmediatelyAndClearState(t *testing.T) {
7475 for _, key := range []byte{'q', 0x03} {
75- p, release := newBlockingPTY(t)
76- d, sess, ac, sends := newManualSessionWithPTYs(t, p)
77- d.notices.record(domain.Notification{Code: domain.NoticePaneSpawn, Message: "m", Time: time.Unix(1, 0)})
78-
79- d.enterNotices(sess, ac)
80- awaitFrame(t, sends, ports.MsgOutput)
81- require.True(t, ac.overlays.noticesActive())
82-
83- d.handleInput(sess, ac, []byte{key})
84- awaitFrame(t, sends, ports.MsgOutput)
85-
86- require.False(t, ac.overlays.noticesActive())
87- require.Nil(t, ac.overlays.noticesOverlay)
88- release()
76+ t.Run(fmt.Sprintf("key=%q", key), func(t *testing.T) {
77+ p, release := newBlockingPTY(t)
78+ defer release()
79+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
80+ d.notices.record(domain.Notification{Code: domain.NoticePaneSpawn, Message: "m", Time: time.Unix(1, 0)})
81+
82+ d.enterNotices(sess, ac)
83+ awaitFrame(t, sends, ports.MsgOutput)
84+ require.True(t, ac.overlays.noticesActive())
85+
86+ d.handleInput(sess, ac, []byte{key})
87+ awaitFrame(t, sends, ports.MsgOutput)
88+
89+ require.False(t, ac.overlays.noticesActive())
90+ require.Nil(t, ac.overlays.noticesOverlay)
91+ })
8992 }
9093 }
9194
@@ -1,6 +1,7 @@
1 package daemon1 package daemon
2 2
3 import (3 import (
4+ "fmt"
4 "testing"5 "testing"
5 "time"6 "time"
6 7
@@ -72,20 +73,22 @@ func TestNoticesJKNavigatesSelection(t *testing.T) {
72 73
73 func TestNoticesQAndCtrlCCloseImmediatelyAndClearState(t *testing.T) {74 func TestNoticesQAndCtrlCCloseImmediatelyAndClearState(t *testing.T) {
74 for _, key := range []byte{'q', 0x03} {75 for _, key := range []byte{'q', 0x03} {
75- p, release := newBlockingPTY(t)76+ t.Run(fmt.Sprintf("key=%q", key), func(t *testing.T) {
76- d, sess, ac, sends := newManualSessionWithPTYs(t, p)77+ p, release := newBlockingPTY(t)
77- d.notices.record(domain.Notification{Code: domain.NoticePaneSpawn, Message: "m", Time: time.Unix(1, 0)})78+ defer release()
78-79+ d, sess, ac, sends := newManualSessionWithPTYs(t, p)
79- d.enterNotices(sess, ac)80+ d.notices.record(domain.Notification{Code: domain.NoticePaneSpawn, Message: "m", Time: time.Unix(1, 0)})
80- awaitFrame(t, sends, ports.MsgOutput)81+
81- require.True(t, ac.overlays.noticesActive())82+ d.enterNotices(sess, ac)
82-83+ awaitFrame(t, sends, ports.MsgOutput)
83- d.handleInput(sess, ac, []byte{key})84+ require.True(t, ac.overlays.noticesActive())
84- awaitFrame(t, sends, ports.MsgOutput)85+
85-86+ d.handleInput(sess, ac, []byte{key})
86- require.False(t, ac.overlays.noticesActive())87+ awaitFrame(t, sends, ports.MsgOutput)
87- require.Nil(t, ac.overlays.noticesOverlay)88+
88- release()89+ require.False(t, ac.overlays.noticesActive())
90+ require.Nil(t, ac.overlays.noticesOverlay)
91+ })
89 }92 }
90 }93 }
91 94
M internal/usecase/notices/model_test.go +19 -5
@@ -157,11 +157,25 @@ func TestRenderEmptyHistoryShowsPlaceholderNotBlank(t *testing.T) {
157157
158158 func TestRenderDegenerateDimensionsDoesNotPanic(t *testing.T) {
159159 m := New([]domain.Notification{{Message: "x", Time: time.Unix(0, 0)}}, time.Unix(0, 0))
160- sizes := []domain.Size{{Cols: 0, Rows: 0}, {Cols: -1, Rows: 5}, {Cols: 5, Rows: -1}, {Cols: 0, Rows: 5}, {Cols: 1, Rows: 1}}
161- for _, size := range sizes {
162- frame := m.Render(size, testStyles())
163- if frame.Width < 0 || frame.Height < 0 {
164- t.Fatalf("Render(%+v) produced negative frame dims", size)
160+ tests := []struct {
161+ size domain.Size
162+ wantWidth int
163+ wantHeight int
164+ }{
165+ // Render clamps each negative dimension to 0 independently via
166+ // max(inner.Cols, 0) / max(inner.Rows, 0); it never clamps the pair
167+ // together, so a negative Cols with a valid Rows still yields a
168+ // zero-width, full-height frame (and vice versa).
169+ {domain.Size{Cols: 0, Rows: 0}, 0, 0},
170+ {domain.Size{Cols: -1, Rows: 5}, 0, 5},
171+ {domain.Size{Cols: 5, Rows: -1}, 5, 0},
172+ {domain.Size{Cols: 0, Rows: 5}, 0, 5},
173+ {domain.Size{Cols: 1, Rows: 1}, 1, 1},
174+ }
175+ for _, tt := range tests {
176+ frame := m.Render(tt.size, testStyles())
177+ if frame.Width != tt.wantWidth || frame.Height != tt.wantHeight {
178+ t.Fatalf("Render(%+v) = frame{Width: %d, Height: %d}, want {Width: %d, Height: %d}", tt.size, frame.Width, frame.Height, tt.wantWidth, tt.wantHeight)
165179 }
166180 }
167181 }
@@ -157,11 +157,25 @@ func TestRenderEmptyHistoryShowsPlaceholderNotBlank(t *testing.T) {
157 157
158 func TestRenderDegenerateDimensionsDoesNotPanic(t *testing.T) {158 func TestRenderDegenerateDimensionsDoesNotPanic(t *testing.T) {
159 m := New([]domain.Notification{{Message: "x", Time: time.Unix(0, 0)}}, time.Unix(0, 0))159 m := New([]domain.Notification{{Message: "x", Time: time.Unix(0, 0)}}, time.Unix(0, 0))
160- sizes := []domain.Size{{Cols: 0, Rows: 0}, {Cols: -1, Rows: 5}, {Cols: 5, Rows: -1}, {Cols: 0, Rows: 5}, {Cols: 1, Rows: 1}}160+ tests := []struct {
161- for _, size := range sizes {161+ size domain.Size
162- frame := m.Render(size, testStyles())162+ wantWidth int
163- if frame.Width < 0 || frame.Height < 0 {163+ wantHeight int
164- t.Fatalf("Render(%+v) produced negative frame dims", size)164+ }{
165+ // Render clamps each negative dimension to 0 independently via
166+ // max(inner.Cols, 0) / max(inner.Rows, 0); it never clamps the pair
167+ // together, so a negative Cols with a valid Rows still yields a
168+ // zero-width, full-height frame (and vice versa).
169+ {domain.Size{Cols: 0, Rows: 0}, 0, 0},
170+ {domain.Size{Cols: -1, Rows: 5}, 0, 5},
171+ {domain.Size{Cols: 5, Rows: -1}, 5, 0},
172+ {domain.Size{Cols: 0, Rows: 5}, 0, 5},
173+ {domain.Size{Cols: 1, Rows: 1}, 1, 1},
174+ }
175+ for _, tt := range tests {
176+ frame := m.Render(tt.size, testStyles())
177+ if frame.Width != tt.wantWidth || frame.Height != tt.wantHeight {
178+ t.Fatalf("Render(%+v) = frame{Width: %d, Height: %d}, want {Width: %d, Height: %d}", tt.size, frame.Width, frame.Height, tt.wantWidth, tt.wantHeight)
165 }179 }
166 }180 }
167 }181 }

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 12

Tags 11

↑↓ moveenter openesc close