test(daemon): cover atomic sync publication
- commit
- d264a5c81d7b63ed81afbed1876780a3590918d8
- author
- brice · · 2026-07-11 19:28 UTC
- parent
- 96e9b63
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +41 -0
M internal/usecase/daemon/render_coordinator_test.go +41 -0
| @@ -570,6 +570,47 @@ func TestRenderCoordinatorSyncGateFollowsDynamicRenderability(t *testing.T) { | |||
| 570 | 570 | }) | |
| 571 | 571 | } | |
| 572 | 572 | ||
| 573 | + | func TestRenderCoordinatorSyncGateRetriesWhenBatchPublishesDuringPredicate(t *testing.T) { | |
| 574 | + | h := newCoordinatorHarness(t) | |
| 575 | + | first, second := &pane{}, &pane{} | |
| 576 | + | gateEvaluated := make(chan struct{}) | |
| 577 | + | releaseGate := make(chan struct{}) | |
| 578 | + | var hookOnce sync.Once | |
| 579 | + | h.rc.opts.afterSyncGateEvaluated = func() { | |
| 580 | + | hookOnce.Do(func() { | |
| 581 | + | close(gateEvaluated) | |
| 582 | + | <-releaseGate | |
| 583 | + | }) | |
| 584 | + | } | |
| 585 | + | h.rc.noteSyncBeginWithRenderability(first, 1, func() bool { return false }) | |
| 586 | + | h.rc.invalidate(renderInvalidation{class: invalidateUrgent, producer: "test"}) | |
| 587 | + | ||
| 588 | + | fireDone := make(chan struct{}) | |
| 589 | + | go func() { | |
| 590 | + | h.rc.fireCurrent(false) | |
| 591 | + | close(fireDone) | |
| 592 | + | }() | |
| 593 | + | <-gateEvaluated | |
| 594 | + | // This publication is deliberately between an open predicate result and | |
| 595 | + | // pending consumption. A stale gate result must be rejected and retried. | |
| 596 | + | h.rc.noteSyncBeginWithRenderability(second, 2, func() bool { return true }) | |
| 597 | + | close(releaseGate) | |
| 598 | + | for range 4096 { | |
| 599 | + | select { | |
| 600 | + | case <-fireDone: | |
| 601 | + | goto fired | |
| 602 | + | default: | |
| 603 | + | runtime.Gosched() | |
| 604 | + | } | |
| 605 | + | } | |
| 606 | + | t.Fatal("fire did not finish after retrying the registry snapshot") | |
| 607 | + | ||
| 608 | + | fired: | |
| 609 | + | requireNoWake(t, h.wakes) | |
| 610 | + | h.rc.noteSyncEnd(second, 2) | |
| 611 | + | require.Equal(t, renderWake{urgent: true, coalesced: 1}, awaitWake(t, h.wakes)) | |
| 612 | + | } | |
| 613 | + | ||
| 573 | 614 | func TestRenderCoordinatorSynchronizedOutput(t *testing.T) { | |
| 574 | 615 | t.Run("completion flushes pending state in one wake", func(t *testing.T) { | |
| 575 | 616 | h := newCoordinatorHarness(t) | |
| @@ -570,6 +570,47 @@ func TestRenderCoordinatorSyncGateFollowsDynamicRenderability(t *testing.T) { | |||||
| 570 | }) | 570 | }) | ||
| 571 | } | 571 | } | ||
| 572 | 572 | ||||
| 573 | + | func TestRenderCoordinatorSyncGateRetriesWhenBatchPublishesDuringPredicate(t *testing.T) { | |||
| 574 | + | h := newCoordinatorHarness(t) | |||
| 575 | + | first, second := &pane{}, &pane{} | |||
| 576 | + | gateEvaluated := make(chan struct{}) | |||
| 577 | + | releaseGate := make(chan struct{}) | |||
| 578 | + | var hookOnce sync.Once | |||
| 579 | + | h.rc.opts.afterSyncGateEvaluated = func() { | |||
| 580 | + | hookOnce.Do(func() { | |||
| 581 | + | close(gateEvaluated) | |||
| 582 | + | <-releaseGate | |||
| 583 | + | }) | |||
| 584 | + | } | |||
| 585 | + | h.rc.noteSyncBeginWithRenderability(first, 1, func() bool { return false }) | |||
| 586 | + | h.rc.invalidate(renderInvalidation{class: invalidateUrgent, producer: "test"}) | |||
| 587 | + | ||||
| 588 | + | fireDone := make(chan struct{}) | |||
| 589 | + | go func() { | |||
| 590 | + | h.rc.fireCurrent(false) | |||
| 591 | + | close(fireDone) | |||
| 592 | + | }() | |||
| 593 | + | <-gateEvaluated | |||
| 594 | + | // This publication is deliberately between an open predicate result and | |||
| 595 | + | // pending consumption. A stale gate result must be rejected and retried. | |||
| 596 | + | h.rc.noteSyncBeginWithRenderability(second, 2, func() bool { return true }) | |||
| 597 | + | close(releaseGate) | |||
| 598 | + | for range 4096 { | |||
| 599 | + | select { | |||
| 600 | + | case <-fireDone: | |||
| 601 | + | goto fired | |||
| 602 | + | default: | |||
| 603 | + | runtime.Gosched() | |||
| 604 | + | } | |||
| 605 | + | } | |||
| 606 | + | t.Fatal("fire did not finish after retrying the registry snapshot") | |||
| 607 | + | ||||
| 608 | + | fired: | |||
| 609 | + | requireNoWake(t, h.wakes) | |||
| 610 | + | h.rc.noteSyncEnd(second, 2) | |||
| 611 | + | require.Equal(t, renderWake{urgent: true, coalesced: 1}, awaitWake(t, h.wakes)) | |||
| 612 | + | } | |||
| 613 | + | ||||
| 573 | func TestRenderCoordinatorSynchronizedOutput(t *testing.T) { | 614 | func TestRenderCoordinatorSynchronizedOutput(t *testing.T) { | ||
| 574 | t.Run("completion flushes pending state in one wake", func(t *testing.T) { | 615 | t.Run("completion flushes pending state in one wake", func(t *testing.T) { | ||
| 575 | h := newCoordinatorHarness(t) | 616 | h := newCoordinatorHarness(t) | ||