1package daemon
2
3import (
4 "sync"
5 "testing"
6 "time"
7
8 "github.com/stretchr/testify/require"
9
10 "github.com/bnema/vev/internal/ports"
11 "github.com/bnema/vev/internal/protocol"
12)
13
14// pickerGraceClock is a settable daemon clock. A hand-written fake is used
15// because render and attention workers read Now concurrently with the test's
16// Advance, which a mock expectation cannot express without obscuring the race
17// the mutex exists to prevent.
18type pickerGraceClock struct {
19 mu sync.Mutex
20 now time.Time
21}
22
23func (c *pickerGraceClock) Now() time.Time {
24 c.mu.Lock()
25 defer c.mu.Unlock()
26 return c.now
27}
28
29func (c *pickerGraceClock) Advance(d time.Duration) {
30 c.mu.Lock()
31 c.now = c.now.Add(d)
32 c.mu.Unlock()
33}
34
35func (*pickerGraceClock) NewTimer(time.Duration) ports.Timer { return stubTimer{} }
36
37// TestMovePickerInputFailSafe pins that raw input is never swallowed without a
38// live offer: bytes that arrive inside the acquisition grace were in flight
39// before the client saw the offer and are dropped, while raw input after the
40// grace proves the client never took the interaction and releases it.
41func TestMovePickerInputFailSafe(t *testing.T) {
42 tests := []struct {
43 name string
44 elapsed time.Duration
45 wantOpen bool
46 wantReleased bool
47 }{
48 {name: "in-flight input inside the grace is dropped", elapsed: 0, wantOpen: true},
49 {name: "input just before the grace is still dropped", elapsed: pickerAcquisitionGrace - time.Millisecond, wantOpen: true},
50 {name: "input after the grace releases the interaction", elapsed: pickerAcquisitionGrace, wantReleased: true},
51 }
52 for _, tt := range tests {
53 t.Run(tt.name, func(t *testing.T) {
54 clock := &pickerGraceClock{now: time.Unix(100, 0)}
55 d, source, ac, _, _, releases, sends := setupMovePickerSessionsCore(t, clock, 0)
56 defer releaseAll(releases)
57
58 openMovePickerForTest(t, d, ac, source, protocol.PickerIntentMovePane, moveSourceForSession(source, ac, "source-tab", "source-pane"))
59 ac.overlays.pickerMu.Lock()
60 interaction := ac.overlays.pickerInteraction
61 ac.overlays.pickerMu.Unlock()
62 clock.Advance(tt.elapsed)
63
64 effect := pickActionEffectForTest(t, source, ac)
65 d.handleInputForAttachment(effect, []byte("a"))
66 effect.End()
67
68 require.Equal(t, tt.wantOpen, ac.overlays.pickerClientActive())
69 if !tt.wantReleased {
70 return
71 }
72 closed := awaitSentServerMessage(t, sends, "PickerClosed", func(message protocol.ServerMessage) bool {
73 _, ok := message.(protocol.PickerClosed)
74 return ok
75 }).(protocol.PickerClosed)
76 require.Equal(t, interaction, closed.InteractionID)
77 })
78 }
79}