detached 3a14a3c

fix(app): stop printing retryable broker failures over the retry notice

commit
3a14a3cf909322da8d3bc661c65d6bcbfb93aa87
author
brice · · 2026-09-24 03:38 UTC
parent
0b823c5
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +33 -3
M internal/app/broker_client.go +9 -3
@@ -230,10 +230,16 @@ func (p *brokerClientPresentation) pickerContext() ports.UIContext {
230230 // notice through the picker, so this is the complementary diagnostic: one
231231 // bounded line under the shared writer lock, never a raw cause chain and never
232232 // a competing terminal owner.
233-func (p *brokerClientPresentation) Failure(err error) {
233+func (p *brokerClientPresentation) Failure(state client.State, err error) {
234234 if p == nil || err == nil {
235235 return
236236 }
237+ // A retryable failure is already shown by the repainted retry notice. A
238+ // raw line here would scroll the painted screen on every attempt and
239+ // leave a trail of stale notices behind it.
240+ if state.Connectivity == client.ConnectivityRetryWait {
241+ return
242+ }
237243 text := err.Error()
238244 if len(text) > ports.BrokerMaxErrorBytes {
239245 text = text[:ports.BrokerMaxErrorBytes]
@@ -292,12 +298,12 @@ func runBrokerClient(ctx context.Context, cfg brokerClientConfig) error {
292298 SessionEnvironment: sessionEnv,
293299 LifecycleActions: cfg.LifecycleActions,
294300 Render: presentation.Render,
295- Notify: func(_ client.State, err error) {
301+ Notify: func(state client.State, err error) {
296302 if cfg.OnFailure != nil {
297303 cfg.OnFailure(err)
298304 return
299305 }
300- presentation.Failure(err)
306+ presentation.Failure(state, err)
301307 },
302308 NotifyLifecycle: func(notice client.LifecycleNotice) {
303309 if cfg.OnLifecycle != nil {
@@ -230,10 +230,16 @@ func (p *brokerClientPresentation) pickerContext() ports.UIContext {
230 // notice through the picker, so this is the complementary diagnostic: one230 // notice through the picker, so this is the complementary diagnostic: one
231 // bounded line under the shared writer lock, never a raw cause chain and never231 // bounded line under the shared writer lock, never a raw cause chain and never
232 // a competing terminal owner.232 // a competing terminal owner.
233-func (p *brokerClientPresentation) Failure(err error) {233+func (p *brokerClientPresentation) Failure(state client.State, err error) {
234 if p == nil || err == nil {234 if p == nil || err == nil {
235 return235 return
236 }236 }
237+ // A retryable failure is already shown by the repainted retry notice. A
238+ // raw line here would scroll the painted screen on every attempt and
239+ // leave a trail of stale notices behind it.
240+ if state.Connectivity == client.ConnectivityRetryWait {
241+ return
242+ }
237 text := err.Error()243 text := err.Error()
238 if len(text) > ports.BrokerMaxErrorBytes {244 if len(text) > ports.BrokerMaxErrorBytes {
239 text = text[:ports.BrokerMaxErrorBytes]245 text = text[:ports.BrokerMaxErrorBytes]
@@ -292,12 +298,12 @@ func runBrokerClient(ctx context.Context, cfg brokerClientConfig) error {
292 SessionEnvironment: sessionEnv,298 SessionEnvironment: sessionEnv,
293 LifecycleActions: cfg.LifecycleActions,299 LifecycleActions: cfg.LifecycleActions,
294 Render: presentation.Render,300 Render: presentation.Render,
295- Notify: func(_ client.State, err error) {301+ Notify: func(state client.State, err error) {
296 if cfg.OnFailure != nil {302 if cfg.OnFailure != nil {
297 cfg.OnFailure(err)303 cfg.OnFailure(err)
298 return304 return
299 }305 }
300- presentation.Failure(err)306+ presentation.Failure(state, err)
301 },307 },
302 NotifyLifecycle: func(notice client.LifecycleNotice) {308 NotifyLifecycle: func(notice client.LifecycleNotice) {
303 if cfg.OnLifecycle != nil {309 if cfg.OnLifecycle != nil {
M internal/app/broker_offline_client_render_test.go +24 -0
@@ -2,6 +2,7 @@ package app
22
33 import (
44 "bytes"
5+ "errors"
56 "io"
67 "strings"
78 "sync"
@@ -308,3 +309,26 @@ func TestOfflineClientPickerPaintCommitsPickerTransaction(t *testing.T) {
308309 require.Equal(t, 0, terminal.beginDepth, "the transaction is balanced")
309310 require.Equal(t, []bool{true}, terminal.endSuccesses, "the frame committed after write and flush")
310311 }
312+
313+// TestBrokerClientFailureWritesOnlyNonRetryableErrors pins that a retryable
314+// failure never writes a raw line: the retry notice already shows it, and a
315+// line per attempt would scroll the painted screen and duplicate the notice.
316+func TestBrokerClientFailureWritesOnlyNonRetryableErrors(t *testing.T) {
317+ tests := []struct {
318+ name string
319+ connectivity client.Connectivity
320+ wantLine bool
321+ }{
322+ {name: "retry wait stays silent", connectivity: client.ConnectivityRetryWait},
323+ {name: "disconnected is reported", connectivity: client.ConnectivityDisconnected, wantLine: true},
324+ }
325+ for _, tt := range tests {
326+ t.Run(tt.name, func(t *testing.T) {
327+ terminal := newOfflineRenderTerminal(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
328+ presentation := &brokerClientPresentation{terminal: terminal}
329+ presentation.Failure(client.State{Connectivity: tt.connectivity}, errors.New("broker connection lost"))
330+ written, _ := terminal.written()
331+ require.Equal(t, tt.wantLine, strings.Contains(written, "broker connection lost"))
332+ })
333+ }
334+}
@@ -2,6 +2,7 @@ package app
2 2
3 import (3 import (
4 "bytes"4 "bytes"
5+ "errors"
5 "io"6 "io"
6 "strings"7 "strings"
7 "sync"8 "sync"
@@ -308,3 +309,26 @@ func TestOfflineClientPickerPaintCommitsPickerTransaction(t *testing.T) {
308 require.Equal(t, 0, terminal.beginDepth, "the transaction is balanced")309 require.Equal(t, 0, terminal.beginDepth, "the transaction is balanced")
309 require.Equal(t, []bool{true}, terminal.endSuccesses, "the frame committed after write and flush")310 require.Equal(t, []bool{true}, terminal.endSuccesses, "the frame committed after write and flush")
310 }311 }
312+
313+// TestBrokerClientFailureWritesOnlyNonRetryableErrors pins that a retryable
314+// failure never writes a raw line: the retry notice already shows it, and a
315+// line per attempt would scroll the painted screen and duplicate the notice.
316+func TestBrokerClientFailureWritesOnlyNonRetryableErrors(t *testing.T) {
317+ tests := []struct {
318+ name string
319+ connectivity client.Connectivity
320+ wantLine bool
321+ }{
322+ {name: "retry wait stays silent", connectivity: client.ConnectivityRetryWait},
323+ {name: "disconnected is reported", connectivity: client.ConnectivityDisconnected, wantLine: true},
324+ }
325+ for _, tt := range tests {
326+ t.Run(tt.name, func(t *testing.T) {
327+ terminal := newOfflineRenderTerminal(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}})
328+ presentation := &brokerClientPresentation{terminal: terminal}
329+ presentation.Failure(client.State{Connectivity: tt.connectivity}, errors.New("broker connection lost"))
330+ written, _ := terminal.written()
331+ require.Equal(t, tt.wantLine, strings.Contains(written, "broker connection lost"))
332+ })
333+ }
334+}

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