test(daemon): cover stale wake across parked resume
- commit
- 2c28fcff1127fa4927d8b0650cad0a84c43ed85b
- author
- brice · · 2026-07-11 20:19 UTC
- committer
- brice · · 2026-07-11 20:21 UTC
- parent
- a710b51
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
1 file · +61 -0
A internal/usecase/daemon/resume_wake_incarnation_test.go +61 -0
| @@ -0,0 +1,61 @@ | |||
| 1 | + | package daemon | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "testing" | |
| 5 | + | ||
| 6 | + | "github.com/stretchr/testify/require" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/vev/internal/domain" | |
| 9 | + | "github.com/bnema/vev/internal/ports" | |
| 10 | + | ) | |
| 11 | + | ||
| 12 | + | // A wake can have passed coordinator scheduling before a link loss parks its | |
| 13 | + | // attachment. Resume deliberately reuses that attachment object, so pointer | |
| 14 | + | // identity alone must not let the old wake paint the new transport before its | |
| 15 | + | // Welcome frame. | |
| 16 | + | func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) { | |
| 17 | + | pty, releasePTY := newBlockingPTY(t) | |
| 18 | + | defer releasePTY() | |
| 19 | + | d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{}) | |
| 20 | + | ||
| 21 | + | oldTransport := &closeTrackingTransport{} | |
| 22 | + | sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport) | |
| 23 | + | require.NoError(t, err) | |
| 24 | + | token := ac.resumeToken | |
| 25 | + | rc := sess.renderCoordinator() | |
| 26 | + | require.NotNil(t, rc) | |
| 27 | + | ||
| 28 | + | // Pause after dispatch but before the coordinator's normal wake callback | |
| 29 | + | // reaches paint/sendMu. This is the precise stale-wake window. | |
| 30 | + | originalWake := rc.opts.wake | |
| 31 | + | wakeDispatched := make(chan struct{}) | |
| 32 | + | releaseWake := make(chan struct{}) | |
| 33 | + | rc.opts.wake = func(w renderWake) { | |
| 34 | + | close(wakeDispatched) | |
| 35 | + | <-releaseWake | |
| 36 | + | originalWake(w) | |
| 37 | + | } | |
| 38 | + | wakeDone := make(chan struct{}) | |
| 39 | + | go func() { | |
| 40 | + | d.invalidateRenderNow(sess, ac, true, "resume-wake-test") | |
| 41 | + | close(wakeDone) | |
| 42 | + | }() | |
| 43 | + | <-wakeDispatched | |
| 44 | + | ||
| 45 | + | d.clientGone(sess, ac, oldTransport, false) | |
| 46 | + | newTransport := &closeTrackingTransport{} | |
| 47 | + | resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, sess.name, token), newTransport, domain.Size{Cols: 80, Rows: 24}) | |
| 48 | + | require.NoError(t, err) | |
| 49 | + | require.True(t, ok) | |
| 50 | + | require.Same(t, sess, resumedSess) | |
| 51 | + | require.Same(t, ac, resumedAC) | |
| 52 | + | ||
| 53 | + | close(releaseWake) | |
| 54 | + | <-wakeDone | |
| 55 | + | require.Empty(t, newTransport.Sends(), "a stale pre-resume wake must not emit Output before Welcome") | |
| 56 | + | ||
| 57 | + | // The new attachment incarnation still accepts its normal required paint. | |
| 58 | + | rc.opts.wake = originalWake | |
| 59 | + | d.firstPaint(sess, ac, ac.size) | |
| 60 | + | require.NotEmpty(t, newTransport.Sends()) | |
| 61 | + | } | |
| @@ -0,0 +1,61 @@ | |||||
| 1 | + | package daemon | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "testing" | |||
| 5 | + | ||||
| 6 | + | "github.com/stretchr/testify/require" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/vev/internal/domain" | |||
| 9 | + | "github.com/bnema/vev/internal/ports" | |||
| 10 | + | ) | |||
| 11 | + | ||||
| 12 | + | // A wake can have passed coordinator scheduling before a link loss parks its | |||
| 13 | + | // attachment. Resume deliberately reuses that attachment object, so pointer | |||
| 14 | + | // identity alone must not let the old wake paint the new transport before its | |||
| 15 | + | // Welcome frame. | |||
| 16 | + | func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) { | |||
| 17 | + | pty, releasePTY := newBlockingPTY(t) | |||
| 18 | + | defer releasePTY() | |||
| 19 | + | d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{}) | |||
| 20 | + | ||||
| 21 | + | oldTransport := &closeTrackingTransport{} | |||
| 22 | + | sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport) | |||
| 23 | + | require.NoError(t, err) | |||
| 24 | + | token := ac.resumeToken | |||
| 25 | + | rc := sess.renderCoordinator() | |||
| 26 | + | require.NotNil(t, rc) | |||
| 27 | + | ||||
| 28 | + | // Pause after dispatch but before the coordinator's normal wake callback | |||
| 29 | + | // reaches paint/sendMu. This is the precise stale-wake window. | |||
| 30 | + | originalWake := rc.opts.wake | |||
| 31 | + | wakeDispatched := make(chan struct{}) | |||
| 32 | + | releaseWake := make(chan struct{}) | |||
| 33 | + | rc.opts.wake = func(w renderWake) { | |||
| 34 | + | close(wakeDispatched) | |||
| 35 | + | <-releaseWake | |||
| 36 | + | originalWake(w) | |||
| 37 | + | } | |||
| 38 | + | wakeDone := make(chan struct{}) | |||
| 39 | + | go func() { | |||
| 40 | + | d.invalidateRenderNow(sess, ac, true, "resume-wake-test") | |||
| 41 | + | close(wakeDone) | |||
| 42 | + | }() | |||
| 43 | + | <-wakeDispatched | |||
| 44 | + | ||||
| 45 | + | d.clientGone(sess, ac, oldTransport, false) | |||
| 46 | + | newTransport := &closeTrackingTransport{} | |||
| 47 | + | resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, sess.name, token), newTransport, domain.Size{Cols: 80, Rows: 24}) | |||
| 48 | + | require.NoError(t, err) | |||
| 49 | + | require.True(t, ok) | |||
| 50 | + | require.Same(t, sess, resumedSess) | |||
| 51 | + | require.Same(t, ac, resumedAC) | |||
| 52 | + | ||||
| 53 | + | close(releaseWake) | |||
| 54 | + | <-wakeDone | |||
| 55 | + | require.Empty(t, newTransport.Sends(), "a stale pre-resume wake must not emit Output before Welcome") | |||
| 56 | + | ||||
| 57 | + | // The new attachment incarnation still accepts its normal required paint. | |||
| 58 | + | rc.opts.wake = originalWake | |||
| 59 | + | d.firstPaint(sess, ac, ac.size) | |||
| 60 | + | require.NotEmpty(t, newTransport.Sends()) | |||
| 61 | + | } | |||