detached 10f16b2

test(daemon): cover resize action adapter boundaries

commit
10f16b25e98755a43123b0398c21b12412357568
author
brice · · 2026-07-25 05:03 UTC
parent
563179c
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +68 -8
M internal/usecase/daemon/input_test.go +68 -8
@@ -69,6 +69,7 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
6969 type adapterState struct {
7070 tree *layout.Tree
7171 placements []layout.Placement
72+ ptySizes map[layout.PaneID][]domain.Size
7273 generationDelta uint64
7374 snapshotDirtyDelta uint64
7475 }
@@ -87,11 +88,15 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
8788
8889 for _, tt := range tests {
8990 t.Run(tt.name, func(t *testing.T) {
90- run := func(t *testing.T, adapter string) adapterState {
91- d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
91+ run := func(t *testing.T, adapter string, attached bool) adapterState {
92+ t.Helper()
93+ ptys := map[layout.PaneID]*resizeActionPTY{
94+ "pane-1": {}, "pane-2": {}, "pane-3": {},
95+ }
96+ d, sess, ac, _ := newManualSessionWithPTYs(t, ptys["pane-1"])
9297 tb := sess.activeTab()
93- p2 := newPane("pane-2", nil, tb.size)
94- p3 := newPane("pane-3", nil, tb.size)
98+ p2 := newPane("pane-2", ptys["pane-2"], tb.size)
99+ p3 := newPane("pane-3", ptys["pane-3"], tb.size)
95100 tb.mu.Lock()
96101 tb.panes[p2.id] = p2
97102 tb.panes[p3.id] = p3
@@ -114,6 +119,20 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
114119 dirty := sess.snapshotGeneration
115120 sess.snapshotMu.Unlock()
116121
122+ invalidations := make(chan renderInvalidation, 2)
123+ rc := newRenderCoordinator(renderCoordinatorOptions{
124+ wake: func(renderWake) {},
125+ onInvalidate: func(inv renderInvalidation) { invalidations <- inv },
126+ })
127+ sess.installRenderCoordinator(rc)
128+ if attached {
129+ rc.attach(ac)
130+ } else {
131+ sess.mu.Lock()
132+ sess.client = nil
133+ sess.mu.Unlock()
134+ }
135+
117136 switch adapter {
118137 case "key":
119138 daemonKeyHandler{d: d, ac: ac}.Action(tt.action)
@@ -135,18 +154,59 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
135154 sess.snapshotMu.Lock()
136155 gotDirty := sess.snapshotGeneration
137156 sess.snapshotMu.Unlock()
138- return adapterState{tree: tree, placements: placements, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty}
157+
158+ gotPTY := make(map[layout.PaneID][]domain.Size, len(ptys))
159+ for id, pty := range ptys {
160+ gotPTY[id] = pty.Sizes()
161+ }
162+ for _, placement := range placements {
163+ require.Equal(t, []domain.Size{{Cols: placement.Content.Width, Rows: placement.Content.Height}}, gotPTY[placement.ID], "canonical PTY resize for %s", placement.ID)
164+ }
165+ if attached {
166+ awaitInvalidation(t, invalidations)
167+ requireNoInvalidation(t, invalidations)
168+ } else {
169+ requireNoInvalidation(t, invalidations)
170+ }
171+ return adapterState{tree: tree, placements: placements, ptySizes: gotPTY, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty}
139172 }
140173
141- keyState := run(t, "key")
174+ keyState := run(t, "key", true)
142175 require.Equal(t, uint64(1), keyState.generationDelta)
143176 require.Equal(t, uint64(1), keyState.snapshotDirtyDelta)
144- require.Equal(t, keyState, run(t, "palette"))
145- require.Equal(t, keyState, run(t, "control"))
177+ require.Equal(t, keyState, run(t, "palette", true))
178+ require.Equal(t, keyState, run(t, "control", true))
179+ headless := run(t, "control", false)
180+ require.Equal(t, uint64(1), headless.generationDelta)
181+ require.Equal(t, uint64(1), headless.snapshotDirtyDelta)
182+ require.Equal(t, keyState, headless)
146183 })
147184 }
148185 }
149186
187+type resizeActionPTY struct {
188+ mu sync.Mutex
189+ sizes []domain.Size
190+}
191+
192+func (*resizeActionPTY) Read([]byte) (int, error) { return 0, nil }
193+func (*resizeActionPTY) Write(b []byte) (int, error) { return len(b), nil }
194+func (*resizeActionPTY) Close() error { return nil }
195+func (*resizeActionPTY) Pid() int { return 0 }
196+func (*resizeActionPTY) ForegroundPgid() (int, error) { return 0, nil }
197+func (p *resizeActionPTY) Resize(size domain.Size) error {
198+ p.mu.Lock()
199+ p.sizes = append(p.sizes, size)
200+ p.mu.Unlock()
201+ return nil
202+}
203+
204+func (p *resizeActionPTY) Sizes() []domain.Size {
205+ p.mu.Lock()
206+ defer p.mu.Unlock()
207+ return append([]domain.Size(nil), p.sizes...)
208+}
209+
150210 // stubClock returns timers whose channel never fires, so a scheduler under it
151211 // blocks in its debounce loop until the session context is cancelled. Used by
152212
@@ -69,6 +69,7 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
69 type adapterState struct {69 type adapterState struct {
70 tree *layout.Tree70 tree *layout.Tree
71 placements []layout.Placement71 placements []layout.Placement
72+ ptySizes map[layout.PaneID][]domain.Size
72 generationDelta uint6473 generationDelta uint64
73 snapshotDirtyDelta uint6474 snapshotDirtyDelta uint64
74 }75 }
@@ -87,11 +88,15 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
87 88
88 for _, tt := range tests {89 for _, tt := range tests {
89 t.Run(tt.name, func(t *testing.T) {90 t.Run(tt.name, func(t *testing.T) {
90- run := func(t *testing.T, adapter string) adapterState {91+ run := func(t *testing.T, adapter string, attached bool) adapterState {
91- d, sess, ac, _ := newManualSessionWithPTYs(t, nil)92+ t.Helper()
93+ ptys := map[layout.PaneID]*resizeActionPTY{
94+ "pane-1": {}, "pane-2": {}, "pane-3": {},
95+ }
96+ d, sess, ac, _ := newManualSessionWithPTYs(t, ptys["pane-1"])
92 tb := sess.activeTab()97 tb := sess.activeTab()
93- p2 := newPane("pane-2", nil, tb.size)98+ p2 := newPane("pane-2", ptys["pane-2"], tb.size)
94- p3 := newPane("pane-3", nil, tb.size)99+ p3 := newPane("pane-3", ptys["pane-3"], tb.size)
95 tb.mu.Lock()100 tb.mu.Lock()
96 tb.panes[p2.id] = p2101 tb.panes[p2.id] = p2
97 tb.panes[p3.id] = p3102 tb.panes[p3.id] = p3
@@ -114,6 +119,20 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
114 dirty := sess.snapshotGeneration119 dirty := sess.snapshotGeneration
115 sess.snapshotMu.Unlock()120 sess.snapshotMu.Unlock()
116 121
122+ invalidations := make(chan renderInvalidation, 2)
123+ rc := newRenderCoordinator(renderCoordinatorOptions{
124+ wake: func(renderWake) {},
125+ onInvalidate: func(inv renderInvalidation) { invalidations <- inv },
126+ })
127+ sess.installRenderCoordinator(rc)
128+ if attached {
129+ rc.attach(ac)
130+ } else {
131+ sess.mu.Lock()
132+ sess.client = nil
133+ sess.mu.Unlock()
134+ }
135+
117 switch adapter {136 switch adapter {
118 case "key":137 case "key":
119 daemonKeyHandler{d: d, ac: ac}.Action(tt.action)138 daemonKeyHandler{d: d, ac: ac}.Action(tt.action)
@@ -135,18 +154,59 @@ func TestResizeActionAdaptersProduceEquivalentGeometry(t *testing.T) {
135 sess.snapshotMu.Lock()154 sess.snapshotMu.Lock()
136 gotDirty := sess.snapshotGeneration155 gotDirty := sess.snapshotGeneration
137 sess.snapshotMu.Unlock()156 sess.snapshotMu.Unlock()
138- return adapterState{tree: tree, placements: placements, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty}157+
158+ gotPTY := make(map[layout.PaneID][]domain.Size, len(ptys))
159+ for id, pty := range ptys {
160+ gotPTY[id] = pty.Sizes()
161+ }
162+ for _, placement := range placements {
163+ require.Equal(t, []domain.Size{{Cols: placement.Content.Width, Rows: placement.Content.Height}}, gotPTY[placement.ID], "canonical PTY resize for %s", placement.ID)
164+ }
165+ if attached {
166+ awaitInvalidation(t, invalidations)
167+ requireNoInvalidation(t, invalidations)
168+ } else {
169+ requireNoInvalidation(t, invalidations)
170+ }
171+ return adapterState{tree: tree, placements: placements, ptySizes: gotPTY, generationDelta: gotGeneration - generation, snapshotDirtyDelta: gotDirty - dirty}
139 }172 }
140 173
141- keyState := run(t, "key")174+ keyState := run(t, "key", true)
142 require.Equal(t, uint64(1), keyState.generationDelta)175 require.Equal(t, uint64(1), keyState.generationDelta)
143 require.Equal(t, uint64(1), keyState.snapshotDirtyDelta)176 require.Equal(t, uint64(1), keyState.snapshotDirtyDelta)
144- require.Equal(t, keyState, run(t, "palette"))177+ require.Equal(t, keyState, run(t, "palette", true))
145- require.Equal(t, keyState, run(t, "control"))178+ require.Equal(t, keyState, run(t, "control", true))
179+ headless := run(t, "control", false)
180+ require.Equal(t, uint64(1), headless.generationDelta)
181+ require.Equal(t, uint64(1), headless.snapshotDirtyDelta)
182+ require.Equal(t, keyState, headless)
146 })183 })
147 }184 }
148 }185 }
149 186
187+type resizeActionPTY struct {
188+ mu sync.Mutex
189+ sizes []domain.Size
190+}
191+
192+func (*resizeActionPTY) Read([]byte) (int, error) { return 0, nil }
193+func (*resizeActionPTY) Write(b []byte) (int, error) { return len(b), nil }
194+func (*resizeActionPTY) Close() error { return nil }
195+func (*resizeActionPTY) Pid() int { return 0 }
196+func (*resizeActionPTY) ForegroundPgid() (int, error) { return 0, nil }
197+func (p *resizeActionPTY) Resize(size domain.Size) error {
198+ p.mu.Lock()
199+ p.sizes = append(p.sizes, size)
200+ p.mu.Unlock()
201+ return nil
202+}
203+
204+func (p *resizeActionPTY) Sizes() []domain.Size {
205+ p.mu.Lock()
206+ defer p.mu.Unlock()
207+ return append([]domain.Size(nil), p.sizes...)
208+}
209+
150 // stubClock returns timers whose channel never fires, so a scheduler under it210 // stubClock returns timers whose channel never fires, so a scheduler under it
151 // blocks in its debounce loop until the session context is cancelled. Used by211 // blocks in its debounce loop until the session context is cancelled. Used by
152 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 12

Tags 11

↑↓ moveenter openesc close