fix(review): bound TUISink.Wait, join the pump, surface drops (crew findings) · Entire

fix(review): bound TUISink.Wait, join the pump, surface drops (crew findings)

62abe4b→main·

peyton-alt·1w ago·2 files·+97 added/-3 removed

The live full-crew review of this branch (approve-path run, 3/3 reviewers succeeded through the hardened sink) returned three findings:

Pinned by a stubbornProgram fake whose Run never returns even after Kill: Wait must return within the bounded escalation.

Co-Authored-By: Claude Fable 5 noreply@anthropic.com

Sessions

01KX0JNDR51T3R0G488FSB63K7View transcript

Changes

2

8 unmodified lines

9
10
11
12
13
14
15
16
17
18
19
20
21
22
143 unmodified lines

166
167
168
167
168
169
170
171
172
173
174
175
176
177
178
179
1 unmodified line

181
182
183
176
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
111 unmodified lines

312
313
314
315
316
317
318
319
320
321
322
323

8 unmodified lines

import (
    "context"
    "io"
    "log/slog"
    "sync"
    "time"

tea "charm.land/bubbletea/v2"
    "golang.org/x/term"

"github.com/entireio/cli/cmd/entire/cli/logging"
    reviewtypes "github.com/entireio/cli/cmd/entire/cli/review/types"
)

143 unmodified lines

// Wait blocks until the Bubble Tea program exits. Safe to call after Start.
// If Start was never called, Wait returns immediately.
// Wait blocks until the Bubble Tea program exits, with a bounded escalation
// so teardown can never hang: in the normal flow PostRunComplete has already
// quit the program and Wait returns immediately; otherwise (early-error
// return paths, or a wedged loop that survived Kill) Wait gives the program
// one grace period, Kills it, gives it one more, and then abandons the
// goroutine — a stuck display must not hold command exit hostage. Joins the
// pump goroutine whenever the program actually exited. Safe to call after
// Start; if Start was never called, returns immediately.
func (s *TUISink) Wait() {
    s.mu.Lock()
    started := s.started

if !started {
        return
    }
    <-s.done
    select {
    case <-s.done:
        <-s.pumpDone
        return
    case <-time.After(tuiPostRunCompleteGrace):
    }
    s.program.Kill()
    select {
    case <-s.done:
        <-s.pumpDone
    case <-time.After(tuiPostRunCompleteGrace):
        // Bubble Tea never returned from Run despite Kill. Abandon the
        // program and pump goroutines rather than hanging teardown.
    }
}

// AgentEvent (Sink interface): translate ev into a tea.Msg and enqueue it for
111 unmodified lines

case <-time.After(tuiPostRunCompleteGrace):
        s.program.Kill()
    }

// Surface silent loss: a healthy run never drops. A non-zero count means
// the TUI loop stalled or lagged badly enough to jam the queue — exactly
// the diagnostic a future wedge investigation needs first.
if n := s.droppedCount(); n > 0 {
    logging.Debug(context.Background(), "tui sink dropped messages under backpressure",
        slog.Int("dropped", n))
}
}

Mcmd/entire/cli/review/tui_sink.go+32/-3

400 unmodified lines

401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468

400 unmodified lines

t.Fatal("RunFinished blocked past its bounded wait on a wedged TUI")
    }
}
// stubbornProgram is a teaRunner whose Run NEVER returns, even after Kill —
// modeling a Bubble Tea teardown stuck restoring a blocked terminal. Send
// unblocks on Kill so the pump can drain, but done never closes.
type stubbornProgram struct {
    killed chan struct{}
    block  chan struct{}
}

func newStubbornProgram() *stubbornProgram {
    return &stubbornProgram{killed: make(chan struct{}), block: make(chan struct{})}
}

func (p *stubbornProgram) Run() (tea.Model, error) {
    <-p.block       // never closed — Run never returns
    return nil, nil //nolint:nilnil // unreachable; mirrors tea.Program.Run's shape
}

func (p *stubbornProgram) Send(tea.Msg) { <-p.killed }

func (p *stubbornProgram) Kill() {
    select {
    case <-p.killed:
    default:
        close(p.killed)
    }
}

// TestTUISink_WaitIsBoundedWhenProgramNeverExits pins the teardown guarantee:
// `defer tuiSink.Wait()` must not hang the command forever when the Bubble
// Tea program never returns from Run, even after Kill. Wait escalates
// (grace → Kill → grace) and then abandons the goroutine.
func TestTUISink_WaitIsBoundedWhenProgramNeverExits(t *testing.T) {
    t.Parallel()
    prog := newStubbornProgram()
    sink := newTUISinkWithProgram(prog)
    sink.Start()

finished := make(chan struct{})
    go func() {
        sink.Wait()
        close(finished)
    }()
    select {
    case <-finished:
    case <-time.After(2*tuiPostRunCompleteGrace + 3*time.Second):
        t.Fatal("Wait hung on a program that never exits — teardown wedge")
    }
}

// TestTUISink_WaitJoinsPump pins that a normal Wait joins the pump goroutine
// (no leak between done closing and the pump observing it).
func TestTUISink_WaitJoinsPump(t *testing.T) {
    t.Parallel()
    prog := newRecordingProgram()
    sink := newTUISinkWithProgram(prog)
    sink.Start()
    prog.Kill()
    sink.Wait()
    select {
    case <-sink.pumpDone:
    case <-time.After(2 * time.Second):
        t.Fatal("Wait returned before the pump goroutine exited")
    }
}