detached f50c254

test(daemon): cover ack-independent preview wakes

commit
f50c254f7f30e21715998d7a5fef07e11853748d
author
brice · · 2026-07-11 18:33 UTC
committer
brice · · 2026-07-11 18:35 UTC
parent
778b92a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +64 -0
M internal/usecase/daemon/render_coordinator_test.go +64 -0
@@ -416,6 +416,70 @@ func TestRenderCoordinatorPreviewSubscription(t *testing.T) {
416416
417417 // --- lifecycle and stale callbacks -----------------------------------------------
418418
419+func TestRenderCoordinatorPreviewWakesDoNotWaitForTargetAck(t *testing.T) {
420+ cases := []struct {
421+ name string
422+ attach bool
423+ }{
424+ {name: "attached cross-session target blocked on ack", attach: true},
425+ {name: "headless cross-session target blocked on ack"},
426+ }
427+ for _, tc := range cases {
428+ t.Run(tc.name, func(t *testing.T) {
429+ h := newCoordinatorHarness(t)
430+ h.ackReady.Store(false)
431+ if tc.attach {
432+ h.rc.attach(&attachedClient{})
433+ }
434+ viewer := &attachedClient{}
435+ h.rc.subscribePreviewFor(viewer, func(w renderWake) { h.previews <- w })
436+
437+ h.rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "target output"})
438+ timers := h.armedTimers(t)
439+ require.Len(t, timers, 1)
440+ timers[0].ch <- time.Time{}
441+
442+ require.Equal(t, renderWake{coalesced: 1}, awaitWake(t, h.previews),
443+ "the viewer preview must receive target output without target ACK capacity")
444+ requireNoWake(t, h.wakes)
445+ requireNoWake(t, h.previews)
446+
447+ h.ackReady.Store(true)
448+ h.rc.notifyAck()
449+ require.Equal(t, renderWake{coalesced: 1}, awaitWake(t, h.wakes),
450+ "the target primary frame remains pending for its own ACK")
451+ requireNoWake(t, h.previews)
452+ })
453+ }
454+}
455+
456+func TestRenderCoordinatorPreviewLifecycleDropsStaleTargetWakes(t *testing.T) {
457+ cases := []struct {
458+ name string
459+ transition func(*renderCoordinator, *attachedClient, *attachedClient)
460+ }{
461+ {"target detach", func(rc *renderCoordinator, target, _ *attachedClient) { rc.noteDetach(target) }},
462+ {"target replacement", func(rc *renderCoordinator, target, replacement *attachedClient) { rc.noteReplace(target, replacement) }},
463+ {"target teardown", func(rc *renderCoordinator, _ *attachedClient, _ *attachedClient) { rc.noteSessionTeardown() }},
464+ }
465+ for _, tc := range cases {
466+ t.Run(tc.name, func(t *testing.T) {
467+ h := newCoordinatorHarness(t)
468+ target, replacement, viewer := &attachedClient{}, &attachedClient{}, &attachedClient{}
469+ h.rc.attach(target)
470+ h.rc.subscribePreviewFor(viewer, func(w renderWake) { h.previews <- w })
471+ h.rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "target output"})
472+ stale := h.armedTimers(t)
473+ require.Len(t, stale, 1)
474+
475+ tc.transition(h.rc, target, replacement)
476+ stale[0].ch <- time.Time{}
477+ requireNoWake(t, h.wakes)
478+ requireNoWake(t, h.previews)
479+ })
480+ }
481+}
482+
419483 func TestRenderCoordinatorPreviewSubscriptionsAreIndependent(t *testing.T) {
420484 h := newCoordinatorHarness(t)
421485 one, two := &attachedClient{}, &attachedClient{}
@@ -416,6 +416,70 @@ func TestRenderCoordinatorPreviewSubscription(t *testing.T) {
416 416
417 // --- lifecycle and stale callbacks -----------------------------------------------417 // --- lifecycle and stale callbacks -----------------------------------------------
418 418
419+func TestRenderCoordinatorPreviewWakesDoNotWaitForTargetAck(t *testing.T) {
420+ cases := []struct {
421+ name string
422+ attach bool
423+ }{
424+ {name: "attached cross-session target blocked on ack", attach: true},
425+ {name: "headless cross-session target blocked on ack"},
426+ }
427+ for _, tc := range cases {
428+ t.Run(tc.name, func(t *testing.T) {
429+ h := newCoordinatorHarness(t)
430+ h.ackReady.Store(false)
431+ if tc.attach {
432+ h.rc.attach(&attachedClient{})
433+ }
434+ viewer := &attachedClient{}
435+ h.rc.subscribePreviewFor(viewer, func(w renderWake) { h.previews <- w })
436+
437+ h.rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "target output"})
438+ timers := h.armedTimers(t)
439+ require.Len(t, timers, 1)
440+ timers[0].ch <- time.Time{}
441+
442+ require.Equal(t, renderWake{coalesced: 1}, awaitWake(t, h.previews),
443+ "the viewer preview must receive target output without target ACK capacity")
444+ requireNoWake(t, h.wakes)
445+ requireNoWake(t, h.previews)
446+
447+ h.ackReady.Store(true)
448+ h.rc.notifyAck()
449+ require.Equal(t, renderWake{coalesced: 1}, awaitWake(t, h.wakes),
450+ "the target primary frame remains pending for its own ACK")
451+ requireNoWake(t, h.previews)
452+ })
453+ }
454+}
455+
456+func TestRenderCoordinatorPreviewLifecycleDropsStaleTargetWakes(t *testing.T) {
457+ cases := []struct {
458+ name string
459+ transition func(*renderCoordinator, *attachedClient, *attachedClient)
460+ }{
461+ {"target detach", func(rc *renderCoordinator, target, _ *attachedClient) { rc.noteDetach(target) }},
462+ {"target replacement", func(rc *renderCoordinator, target, replacement *attachedClient) { rc.noteReplace(target, replacement) }},
463+ {"target teardown", func(rc *renderCoordinator, _ *attachedClient, _ *attachedClient) { rc.noteSessionTeardown() }},
464+ }
465+ for _, tc := range cases {
466+ t.Run(tc.name, func(t *testing.T) {
467+ h := newCoordinatorHarness(t)
468+ target, replacement, viewer := &attachedClient{}, &attachedClient{}, &attachedClient{}
469+ h.rc.attach(target)
470+ h.rc.subscribePreviewFor(viewer, func(w renderWake) { h.previews <- w })
471+ h.rc.invalidate(renderInvalidation{class: invalidateOutput, producer: "target output"})
472+ stale := h.armedTimers(t)
473+ require.Len(t, stale, 1)
474+
475+ tc.transition(h.rc, target, replacement)
476+ stale[0].ch <- time.Time{}
477+ requireNoWake(t, h.wakes)
478+ requireNoWake(t, h.previews)
479+ })
480+ }
481+}
482+
419 func TestRenderCoordinatorPreviewSubscriptionsAreIndependent(t *testing.T) {483 func TestRenderCoordinatorPreviewSubscriptionsAreIndependent(t *testing.T) {
420 h := newCoordinatorHarness(t)484 h := newCoordinatorHarness(t)
421 one, two := &attachedClient{}, &attachedClient{}485 one, two := &attachedClient{}, &attachedClient{}

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