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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 416 | 416 | ||
| 417 | 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 | 483 | func TestRenderCoordinatorPreviewSubscriptionsAreIndependent(t *testing.T) { | |
| 420 | 484 | h := newCoordinatorHarness(t) | |
| 421 | 485 | 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{} | ||