detached c4381db

test(client): restore reconnect toast coverage

commit
c4381db480eff655d4b4452466fe9c3fbaf9b98e
author
brice · · 2026-07-14 19:45 UTC
parent
1238e7d
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +41 -1
M internal/usecase/client/reconnect_toast_test.go +41 -1
@@ -8,6 +8,7 @@ import (
88 "io"
99 "log/slog"
1010 "strings"
11+ "sync"
1112 "testing"
1213 "time"
1314
@@ -71,6 +72,34 @@ func newReconnectToastTerminalHarnessWithOutput(t *testing.T, out io.Writer) *re
7172
7273 func (h *reconnectToastTerminalHarness) closeInput() { _ = h.inWriter.Close() }
7374
75+type reconnectToastOutputRecorder struct {
76+ mu sync.Mutex
77+ pending strings.Builder
78+ completed chan string
79+}
80+
81+func newReconnectToastOutputRecorder() *reconnectToastOutputRecorder {
82+ return &reconnectToastOutputRecorder{completed: make(chan string, 3)}
83+}
84+
85+func (r *reconnectToastOutputRecorder) Write(p []byte) (int, error) {
86+ const restoreCursor = "\x1b[0m\x1b[u"
87+
88+ r.mu.Lock()
89+ r.pending.Write(p)
90+ var output string
91+ if string(p) == restoreCursor {
92+ output = r.pending.String()
93+ r.pending.Reset()
94+ }
95+ r.mu.Unlock()
96+
97+ if output != "" {
98+ r.completed <- output
99+ }
100+ return len(p), nil
101+}
102+
74103 func reconnectToastWelcome(token uint64) ports.Frame {
75104 return reconnectToastWelcomeNamed("", token)
76105 }
@@ -225,7 +254,8 @@ func newReconnectAttachAttempt(term ports.Terminal, transport ports.Transport, c
225254 }
226255
227256 func TestReconnectToastDegradedClearsModalAndProbingIsVisible(t *testing.T) {
228- term := newReconnectToastTerminalHarness(t)
257+ out := newReconnectToastOutputRecorder()
258+ term := newReconnectToastTerminalHarnessWithOutput(t, out)
229259 defer term.closeInput()
230260 tr := newReconnectToastLinkTransport()
231261 tr.recvCh <- reconnectToastRecv{frame: reconnectToastWelcome(44)}
@@ -235,8 +265,18 @@ func TestReconnectToastDegradedClearsModalAndProbingIsVisible(t *testing.T) {
235265 attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
236266 go func() { resultCh <- attempt.run(context.Background()) }()
237267
268+ tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
269+ probed := <-out.completed
270+ require.Contains(t, probed, reconnectStageMessage(reconnectStageProbingUDP))
271+
238272 tr.events <- ports.LinkEvent{State: ports.LinkStateDegraded}
273+ cleared := <-out.completed
274+ assertReconnectToastClearCoversBounds(t, cleared, reconnectToastBoundsFor(term.size, reconnectStageMessage(reconnectStageProbingUDP)))
275+
239276 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
277+ probed = <-out.completed
278+ require.Contains(t, probed, reconnectStageMessage(reconnectStageProbingUDP))
279+
240280 tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}
241281 result := <-resultCh
242282 require.NoError(t, result.err)
@@ -8,6 +8,7 @@ import (
8 "io"8 "io"
9 "log/slog"9 "log/slog"
10 "strings"10 "strings"
11+ "sync"
11 "testing"12 "testing"
12 "time"13 "time"
13 14
@@ -71,6 +72,34 @@ func newReconnectToastTerminalHarnessWithOutput(t *testing.T, out io.Writer) *re
71 72
72 func (h *reconnectToastTerminalHarness) closeInput() { _ = h.inWriter.Close() }73 func (h *reconnectToastTerminalHarness) closeInput() { _ = h.inWriter.Close() }
73 74
75+type reconnectToastOutputRecorder struct {
76+ mu sync.Mutex
77+ pending strings.Builder
78+ completed chan string
79+}
80+
81+func newReconnectToastOutputRecorder() *reconnectToastOutputRecorder {
82+ return &reconnectToastOutputRecorder{completed: make(chan string, 3)}
83+}
84+
85+func (r *reconnectToastOutputRecorder) Write(p []byte) (int, error) {
86+ const restoreCursor = "\x1b[0m\x1b[u"
87+
88+ r.mu.Lock()
89+ r.pending.Write(p)
90+ var output string
91+ if string(p) == restoreCursor {
92+ output = r.pending.String()
93+ r.pending.Reset()
94+ }
95+ r.mu.Unlock()
96+
97+ if output != "" {
98+ r.completed <- output
99+ }
100+ return len(p), nil
101+}
102+
74 func reconnectToastWelcome(token uint64) ports.Frame {103 func reconnectToastWelcome(token uint64) ports.Frame {
75 return reconnectToastWelcomeNamed("", token)104 return reconnectToastWelcomeNamed("", token)
76 }105 }
@@ -225,7 +254,8 @@ func newReconnectAttachAttempt(term ports.Terminal, transport ports.Transport, c
225 }254 }
226 255
227 func TestReconnectToastDegradedClearsModalAndProbingIsVisible(t *testing.T) {256 func TestReconnectToastDegradedClearsModalAndProbingIsVisible(t *testing.T) {
228- term := newReconnectToastTerminalHarness(t)257+ out := newReconnectToastOutputRecorder()
258+ term := newReconnectToastTerminalHarnessWithOutput(t, out)
229 defer term.closeInput()259 defer term.closeInput()
230 tr := newReconnectToastLinkTransport()260 tr := newReconnectToastLinkTransport()
231 tr.recvCh <- reconnectToastRecv{frame: reconnectToastWelcome(44)}261 tr.recvCh <- reconnectToastRecv{frame: reconnectToastWelcome(44)}
@@ -235,8 +265,18 @@ func TestReconnectToastDegradedClearsModalAndProbingIsVisible(t *testing.T) {
235 attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)265 attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
236 go func() { resultCh <- attempt.run(context.Background()) }()266 go func() { resultCh <- attempt.run(context.Background()) }()
237 267
268+ tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
269+ probed := <-out.completed
270+ require.Contains(t, probed, reconnectStageMessage(reconnectStageProbingUDP))
271+
238 tr.events <- ports.LinkEvent{State: ports.LinkStateDegraded}272 tr.events <- ports.LinkEvent{State: ports.LinkStateDegraded}
273+ cleared := <-out.completed
274+ assertReconnectToastClearCoversBounds(t, cleared, reconnectToastBoundsFor(term.size, reconnectStageMessage(reconnectStageProbingUDP)))
275+
239 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}276 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
277+ probed = <-out.completed
278+ require.Contains(t, probed, reconnectStageMessage(reconnectStageProbingUDP))
279+
240 tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}280 tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}
241 result := <-resultCh281 result := <-resultCh
242 require.NoError(t, result.err)282 require.NoError(t, result.err)

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