detached 3815014

test(runtime): align handshake mocks and helpers

commit
38150141df273717c36f21efbc480b9bbf3fc2ff
author
brice · · 2026-07-13 21:40 UTC
parent
aaad006
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +35 -54
M internal/usecase/client/client.go +9 -9
@@ -366,10 +366,10 @@ type attachResult struct {
366366 // boundedPreWelcome runs one blocking handshake operation. Transport methods
367367 // do not accept a context, so cancellation and expiry close this attempt's
368368 // transport to interrupt the call, then wait for its goroutine to exit.
369-func boundedPreWelcome(ctx context.Context, clk ports.Clock, transport ports.Transport, operation func() error) (error, bool) {
369+func boundedPreWelcome(ctx context.Context, clk ports.Clock, transport ports.Transport, operation func() error) (bool, error) {
370370 if err := ctx.Err(); err != nil {
371371 _ = transport.Close()
372- return err, true
372+ return true, err
373373 }
374374
375375 timer := clk.NewTimer(preWelcomeTimeout)
@@ -384,23 +384,23 @@ func boundedPreWelcome(ctx context.Context, clk ports.Clock, transport ports.Tra
384384 // already returned by the time we observe it here.
385385 if ctx.Err() != nil {
386386 _ = transport.Close()
387- return ctx.Err(), true
387+ return true, ctx.Err()
388388 }
389389 select {
390390 case <-timer.C():
391391 _ = transport.Close()
392- return context.DeadlineExceeded, true
392+ return true, context.DeadlineExceeded
393393 default:
394- return err, false
394+ return false, err
395395 }
396396 case <-ctx.Done():
397397 _ = transport.Close()
398398 <-completed
399- return ctx.Err(), true
399+ return true, ctx.Err()
400400 case <-timer.C():
401401 _ = transport.Close()
402402 <-completed
403- return context.DeadlineExceeded, true
403+ return true, context.DeadlineExceeded
404404 }
405405 }
406406
@@ -451,7 +451,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi
451451 TrueColor: trueColor,
452452 MaxOutputInFlight: requestedOutputWindow(transport),
453453 }
454- if err, closed := boundedPreWelcome(ctx, clk, transport, func() error {
454+ if closed, err := boundedPreWelcome(ctx, clk, transport, func() error {
455455 return transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)})
456456 }); err != nil {
457457 return attachResult{transportClosed: closed, err: fmt.Errorf("vev: sending hello: %w", err)}
@@ -460,7 +460,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi
460460
461461 // 2. Await Welcome or a typed rejection.
462462 var reply ports.Frame
463- err, closed := boundedPreWelcome(ctx, clk, transport, func() error {
463+ closed, err := boundedPreWelcome(ctx, clk, transport, func() error {
464464 var recvErr error
465465 reply, recvErr = transport.Recv()
466466 return recvErr
@@ -366,10 +366,10 @@ type attachResult struct {
366 // boundedPreWelcome runs one blocking handshake operation. Transport methods366 // boundedPreWelcome runs one blocking handshake operation. Transport methods
367 // do not accept a context, so cancellation and expiry close this attempt's367 // do not accept a context, so cancellation and expiry close this attempt's
368 // transport to interrupt the call, then wait for its goroutine to exit.368 // transport to interrupt the call, then wait for its goroutine to exit.
369-func boundedPreWelcome(ctx context.Context, clk ports.Clock, transport ports.Transport, operation func() error) (error, bool) {369+func boundedPreWelcome(ctx context.Context, clk ports.Clock, transport ports.Transport, operation func() error) (bool, error) {
370 if err := ctx.Err(); err != nil {370 if err := ctx.Err(); err != nil {
371 _ = transport.Close()371 _ = transport.Close()
372- return err, true372+ return true, err
373 }373 }
374 374
375 timer := clk.NewTimer(preWelcomeTimeout)375 timer := clk.NewTimer(preWelcomeTimeout)
@@ -384,23 +384,23 @@ func boundedPreWelcome(ctx context.Context, clk ports.Clock, transport ports.Tra
384 // already returned by the time we observe it here.384 // already returned by the time we observe it here.
385 if ctx.Err() != nil {385 if ctx.Err() != nil {
386 _ = transport.Close()386 _ = transport.Close()
387- return ctx.Err(), true387+ return true, ctx.Err()
388 }388 }
389 select {389 select {
390 case <-timer.C():390 case <-timer.C():
391 _ = transport.Close()391 _ = transport.Close()
392- return context.DeadlineExceeded, true392+ return true, context.DeadlineExceeded
393 default:393 default:
394- return err, false394+ return false, err
395 }395 }
396 case <-ctx.Done():396 case <-ctx.Done():
397 _ = transport.Close()397 _ = transport.Close()
398 <-completed398 <-completed
399- return ctx.Err(), true399+ return true, ctx.Err()
400 case <-timer.C():400 case <-timer.C():
401 _ = transport.Close()401 _ = transport.Close()
402 <-completed402 <-completed
403- return context.DeadlineExceeded, true403+ return true, context.DeadlineExceeded
404 }404 }
405 }405 }
406 406
@@ -451,7 +451,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi
451 TrueColor: trueColor,451 TrueColor: trueColor,
452 MaxOutputInFlight: requestedOutputWindow(transport),452 MaxOutputInFlight: requestedOutputWindow(transport),
453 }453 }
454- if err, closed := boundedPreWelcome(ctx, clk, transport, func() error {454+ if closed, err := boundedPreWelcome(ctx, clk, transport, func() error {
455 return transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)})455 return transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)})
456 }); err != nil {456 }); err != nil {
457 return attachResult{transportClosed: closed, err: fmt.Errorf("vev: sending hello: %w", err)}457 return attachResult{transportClosed: closed, err: fmt.Errorf("vev: sending hello: %w", err)}
@@ -460,7 +460,7 @@ func attachOnce(ctx context.Context, transport ports.Transport, term ports.Termi
460 460
461 // 2. Await Welcome or a typed rejection.461 // 2. Await Welcome or a typed rejection.
462 var reply ports.Frame462 var reply ports.Frame
463- err, closed := boundedPreWelcome(ctx, clk, transport, func() error {463+ closed, err := boundedPreWelcome(ctx, clk, transport, func() error {
464 var recvErr error464 var recvErr error
465 reply, recvErr = transport.Recv()465 reply, recvErr = transport.Recv()
466 return recvErr466 return recvErr
M internal/usecase/client/client_test.go +23 -38
@@ -155,41 +155,23 @@ func isType(typ ports.MsgType) any {
155155 return mock.MatchedBy(func(f ports.Frame) bool { return f.Type == typ })
156156 }
157157
158-// handshakeClock exposes its timer so the pre-Welcome deadline can be tested
159-// without waiting on wall time.
160-type handshakeClock struct {
161- mu sync.Mutex
162- duration time.Duration
163- created chan *handshakeTimer
164-}
165-
166-type handshakeTimer struct{ ch chan time.Time }
167-
168-func newHandshakeClock() *handshakeClock {
169- return &handshakeClock{created: make(chan *handshakeTimer, 2)}
170-}
171-
172-func (c *handshakeClock) Now() time.Time { return time.Time{} }
158+const testPreWelcomeTimeout = 15 * time.Second
173159
174-func (c *handshakeClock) NewTimer(d time.Duration) ports.Timer {
175- c.mu.Lock()
176- c.duration = d
177- c.mu.Unlock()
178- t := &handshakeTimer{ch: make(chan time.Time, 1)}
179- c.created <- t
180- return t
181-}
182-
183-func (c *handshakeClock) Duration() time.Duration {
184- c.mu.Lock()
185- defer c.mu.Unlock()
186- return c.duration
160+func newHandshakeClock(t *testing.T, timerCount int) (*portsmocks.MockClock, <-chan chan time.Time) {
161+ t.Helper()
162+ created := make(chan chan time.Time, timerCount)
163+ clk := portsmocks.NewMockClock(t)
164+ clk.EXPECT().NewTimer(testPreWelcomeTimeout).RunAndReturn(func(time.Duration) ports.Timer {
165+ timerC := make(chan time.Time, 1)
166+ timer := portsmocks.NewMockTimer(t)
167+ timer.EXPECT().C().Return((<-chan time.Time)(timerC)).Maybe()
168+ timer.EXPECT().Stop().Return(true).Once()
169+ created <- timerC
170+ return timer
171+ }).Times(timerCount)
172+ return clk, created
187173 }
188174
189-func (t *handshakeTimer) C() <-chan time.Time { return t.ch }
190-func (t *handshakeTimer) Reset(time.Duration) bool { return false }
191-func (t *handshakeTimer) Stop() bool { return true }
192-
193175 func TestRunBoundsPreWelcomeOperations(t *testing.T) {
194176 tests := []struct {
195177 name string
@@ -207,7 +189,11 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
207189 t.Run(tt.name, func(t *testing.T) {
208190 ctx, cancel := context.WithCancel(context.Background())
209191 defer cancel()
210- clk := newHandshakeClock()
192+ timerCount := 1
193+ if tt.phase == "recv" {
194+ timerCount = 2
195+ }
196+ clk, createdTimers := newHandshakeClock(t, timerCount)
211197
212198 term := portsmocks.NewMockTerminal(t)
213199 term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once()
@@ -243,13 +229,12 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
243229 errCh <- client.Run(ctx, dialer, term, clk, ports.IntentAttach, "main", false, nil, slog.New(slog.DiscardHandler))
244230 }()
245231
246- var timer *handshakeTimer
232+ var timerC chan time.Time
247233 select {
248- case timer = <-clk.created:
234+ case timerC = <-createdTimers:
249235 case <-time.After(time.Second):
250236 t.Fatal("pre-Welcome timer was not created")
251237 }
252- require.Equal(t, 15*time.Second, clk.Duration())
253238 select {
254239 case <-started:
255240 case <-time.After(time.Second):
@@ -257,7 +242,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
257242 }
258243 if tt.phase == "recv" {
259244 select {
260- case timer = <-clk.created:
245+ case timerC = <-createdTimers:
261246 case <-time.After(time.Second):
262247 t.Fatal("Welcome timer was not created")
263248 }
@@ -266,7 +251,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
266251 if tt.end == "cancel" {
267252 cancel()
268253 } else {
269- timer.ch <- time.Time{}
254+ timerC <- time.Time{}
270255 }
271256
272257 select {
@@ -155,41 +155,23 @@ func isType(typ ports.MsgType) any {
155 return mock.MatchedBy(func(f ports.Frame) bool { return f.Type == typ })155 return mock.MatchedBy(func(f ports.Frame) bool { return f.Type == typ })
156 }156 }
157 157
158-// handshakeClock exposes its timer so the pre-Welcome deadline can be tested158+const testPreWelcomeTimeout = 15 * time.Second
159-// without waiting on wall time.
160-type handshakeClock struct {
161- mu sync.Mutex
162- duration time.Duration
163- created chan *handshakeTimer
164-}
165-
166-type handshakeTimer struct{ ch chan time.Time }
167-
168-func newHandshakeClock() *handshakeClock {
169- return &handshakeClock{created: make(chan *handshakeTimer, 2)}
170-}
171-
172-func (c *handshakeClock) Now() time.Time { return time.Time{} }
173 159
174-func (c *handshakeClock) NewTimer(d time.Duration) ports.Timer {160+func newHandshakeClock(t *testing.T, timerCount int) (*portsmocks.MockClock, <-chan chan time.Time) {
175- c.mu.Lock()161+ t.Helper()
176- c.duration = d162+ created := make(chan chan time.Time, timerCount)
177- c.mu.Unlock()163+ clk := portsmocks.NewMockClock(t)
178- t := &handshakeTimer{ch: make(chan time.Time, 1)}164+ clk.EXPECT().NewTimer(testPreWelcomeTimeout).RunAndReturn(func(time.Duration) ports.Timer {
179- c.created <- t165+ timerC := make(chan time.Time, 1)
180- return t166+ timer := portsmocks.NewMockTimer(t)
181-}167+ timer.EXPECT().C().Return((<-chan time.Time)(timerC)).Maybe()
182-168+ timer.EXPECT().Stop().Return(true).Once()
183-func (c *handshakeClock) Duration() time.Duration {169+ created <- timerC
184- c.mu.Lock()170+ return timer
185- defer c.mu.Unlock()171+ }).Times(timerCount)
186- return c.duration172+ return clk, created
187 }173 }
188 174
189-func (t *handshakeTimer) C() <-chan time.Time { return t.ch }
190-func (t *handshakeTimer) Reset(time.Duration) bool { return false }
191-func (t *handshakeTimer) Stop() bool { return true }
192-
193 func TestRunBoundsPreWelcomeOperations(t *testing.T) {175 func TestRunBoundsPreWelcomeOperations(t *testing.T) {
194 tests := []struct {176 tests := []struct {
195 name string177 name string
@@ -207,7 +189,11 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
207 t.Run(tt.name, func(t *testing.T) {189 t.Run(tt.name, func(t *testing.T) {
208 ctx, cancel := context.WithCancel(context.Background())190 ctx, cancel := context.WithCancel(context.Background())
209 defer cancel()191 defer cancel()
210- clk := newHandshakeClock()192+ timerCount := 1
193+ if tt.phase == "recv" {
194+ timerCount = 2
195+ }
196+ clk, createdTimers := newHandshakeClock(t, timerCount)
211 197
212 term := portsmocks.NewMockTerminal(t)198 term := portsmocks.NewMockTerminal(t)
213 term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once()199 term.EXPECT().Size().Return(domain.Size{Cols: 80, Rows: 24}, nil).Once()
@@ -243,13 +229,12 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
243 errCh <- client.Run(ctx, dialer, term, clk, ports.IntentAttach, "main", false, nil, slog.New(slog.DiscardHandler))229 errCh <- client.Run(ctx, dialer, term, clk, ports.IntentAttach, "main", false, nil, slog.New(slog.DiscardHandler))
244 }()230 }()
245 231
246- var timer *handshakeTimer232+ var timerC chan time.Time
247 select {233 select {
248- case timer = <-clk.created:234+ case timerC = <-createdTimers:
249 case <-time.After(time.Second):235 case <-time.After(time.Second):
250 t.Fatal("pre-Welcome timer was not created")236 t.Fatal("pre-Welcome timer was not created")
251 }237 }
252- require.Equal(t, 15*time.Second, clk.Duration())
253 select {238 select {
254 case <-started:239 case <-started:
255 case <-time.After(time.Second):240 case <-time.After(time.Second):
@@ -257,7 +242,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
257 }242 }
258 if tt.phase == "recv" {243 if tt.phase == "recv" {
259 select {244 select {
260- case timer = <-clk.created:245+ case timerC = <-createdTimers:
261 case <-time.After(time.Second):246 case <-time.After(time.Second):
262 t.Fatal("Welcome timer was not created")247 t.Fatal("Welcome timer was not created")
263 }248 }
@@ -266,7 +251,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
266 if tt.end == "cancel" {251 if tt.end == "cancel" {
267 cancel()252 cancel()
268 } else {253 } else {
269- timer.ch <- time.Time{}254+ timerC <- time.Time{}
270 }255 }
271 256
272 select {257 select {
M internal/usecase/client/reconnect_toast_test.go +1 -1
@@ -187,7 +187,7 @@ func newReconnectHandshakeClock(t *testing.T) *portsmocks.MockClock {
187187 timer := portsmocks.NewMockTimer(t)
188188 timer.EXPECT().C().Return((<-chan time.Time)(make(chan time.Time))).Maybe()
189189 timer.EXPECT().Stop().Return(true).Maybe()
190- clk.EXPECT().NewTimer(15 * time.Second).Return(timer).Maybe()
190+ clk.EXPECT().NewTimer(preWelcomeTimeout).Return(timer).Maybe()
191191 return clk
192192 }
193193
@@ -187,7 +187,7 @@ func newReconnectHandshakeClock(t *testing.T) *portsmocks.MockClock {
187 timer := portsmocks.NewMockTimer(t)187 timer := portsmocks.NewMockTimer(t)
188 timer.EXPECT().C().Return((<-chan time.Time)(make(chan time.Time))).Maybe()188 timer.EXPECT().C().Return((<-chan time.Time)(make(chan time.Time))).Maybe()
189 timer.EXPECT().Stop().Return(true).Maybe()189 timer.EXPECT().Stop().Return(true).Maybe()
190- clk.EXPECT().NewTimer(15 * time.Second).Return(timer).Maybe()190+ clk.EXPECT().NewTimer(preWelcomeTimeout).Return(timer).Maybe()
191 return clk191 return clk
192 }192 }
193 193
M internal/usecase/daemon/render.go +2 -6
@@ -32,12 +32,8 @@ func (d *Daemon) paneRenderable(sess *session, tb *tab, p *pane) bool {
3232
3333 // The normal attached render path needs no cross-session picker lookup.
3434 // Only inactive or headless tabs can be renderable as a picker preview.
35- preview := false
36- if !active || !attached {
37- preview = d.tabIsPickerPreview(tb)
38- if !preview {
39- return false
40- }
35+ if (!active || !attached) && !d.tabIsPickerPreview(tb) {
36+ return false
4137 }
4238 tb.mu.Lock()
4339 defer tb.mu.Unlock()
@@ -32,12 +32,8 @@ func (d *Daemon) paneRenderable(sess *session, tb *tab, p *pane) bool {
32 32
33 // The normal attached render path needs no cross-session picker lookup.33 // The normal attached render path needs no cross-session picker lookup.
34 // Only inactive or headless tabs can be renderable as a picker preview.34 // Only inactive or headless tabs can be renderable as a picker preview.
35- preview := false35+ if (!active || !attached) && !d.tabIsPickerPreview(tb) {
36- if !active || !attached {36+ return false
37- preview = d.tabIsPickerPreview(tb)
38- if !preview {
39- return false
40- }
41 }37 }
42 tb.mu.Lock()38 tb.mu.Lock()
43 defer tb.mu.Unlock()39 defer tb.mu.Unlock()

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