1package client
2
3import (
4 "context"
5 "io"
6 "testing"
7 "time"
8
9 "github.com/stretchr/testify/require"
10
11 "github.com/bnema/vev/internal/domain"
12 "github.com/bnema/vev/internal/protocol"
13)
14
15func TestTerminalKeysNormalizeOnlyOnceEnabled(t *testing.T) {
16 tests := []struct {
17 name string
18 enabled bool
19 focus domain.TerminalFocus
20 in string
21 want string
22 wantHeld bool
23 }{
24 {name: "disabled passes through", in: "\x1b[57442;5u", want: "\x1b[57442;5u"},
25 {name: "enabled drops ctrl press", enabled: true, in: "\x1b[57442;5u", want: "", wantHeld: true},
26 {name: "enabled rewrites text", enabled: true, in: "\x1b[97;;97u", want: "a"},
27 {name: "lost focus forgets ctrl", enabled: true, focus: domain.TerminalFocusUnfocused, in: "\x1b[57442;5u", want: ""},
28 }
29 for _, tt := range tests {
30 t.Run(tt.name, func(t *testing.T) {
31 keys := newTerminalKeys()
32 if tt.enabled {
33 keys.enable()
34 }
35 require.Equal(t, tt.want, string(keys.normalize([]byte(tt.in), tt.focus)))
36 held, _ := keys.ctrl.Watch()
37 require.Equal(t, tt.wantHeld, held)
38 })
39 }
40}
41
42func TestCtrlReporterReportsHoldsAfterDelayAndReleasesAtOnce(t *testing.T) {
43 type step struct {
44 held bool // set the state, then handle the change
45 fire bool // instead, fire the hold timer
46 want *protocol.ModifierState
47 }
48 held, released := &protocol.ModifierState{Ctrl: true}, &protocol.ModifierState{}
49 tests := []struct {
50 name string
51 steps []step
52 }{
53 {name: "hold then release", steps: []step{{held: true}, {fire: true, want: held}, {held: false, want: released}}},
54 {name: "quick tap sends nothing", steps: []step{{held: true}, {held: false}, {fire: true}}},
55 {name: "hold reported once", steps: []step{{held: true}, {fire: true, want: held}, {fire: true}}},
56 }
57 for _, tt := range tests {
58 t.Run(tt.name, func(t *testing.T) {
59 state := newTerminalCtrlState()
60 clock := newSupervisorTestClock()
61 r := newCtrlReporter(state, clock)
62 var timer *supervisorTestTimer
63 for i, s := range tt.steps {
64 var message protocol.ModifierState
65 var ok bool
66 if s.fire {
67 if r.timerC() == nil {
68 require.Nil(t, s.want, "step %d: no timer to fire", i)
69 continue
70 }
71 timer.ch <- time.Time{}
72 <-r.timerC()
73 message, ok = r.due()
74 } else {
75 state.set(s.held)
76 <-r.changed
77 message, ok = r.next()
78 if s.held {
79 timer = clock.awaitTimer(t)
80 require.Equal(t, ctrlHoldDelay, timer.delay)
81 }
82 }
83 require.Equal(t, s.want != nil, ok, "step %d", i)
84 if s.want != nil {
85 require.Equal(t, *s.want, message, "step %d", i)
86 }
87 }
88 r.stop()
89 require.True(t, clock.allStopped())
90 })
91 }
92}
93
94// TestAttachmentReportsHeldCtrl drives a real pump: once kitty key events are
95// enabled, a held Ctrl reaches the daemon as ModifierState and its release
96// follows, while the dropped modifier events never reach the session as input.
97func TestAttachmentReportsHeldCtrl(t *testing.T) {
98 pr, pw := io.Pipe()
99 defer pw.Close()
100 pump := newTerminalInputPump(pr)
101 pump.keys.enable()
102 pump.start()
103 defer pump.stop()
104 stream := newSessionTestStream()
105 host := newWorkerTestHost(newWorkerTestTerminal(), pump, nil)
106 cfg := sessionTestWorkerConfig(sessionTestRequest(true))
107 clock := newSupervisorTestClock()
108 cfg.Clock = clock
109 worker, err := newSessionAttachmentWorker(cfg)
110 require.NoError(t, err)
111 ctx, cancel := context.WithCancel(context.Background())
112 defer cancel()
113 go func() { _, _ = host.Run(ctx, AttachmentToken{Generation: 4, Attempt: 1}, worker, stream) }()
114
115 awaitHello(t, stream)
116 stream.deliver(protocol.Welcome{SessionName: "alpha"})
117 stream.deliver(sessionTestOutput(1, "\x1b[Hready"))
118
119 sent := func(want protocol.ModifierState) func() bool {
120 return func() bool {
121 for _, message := range stream.messages() {
122 if message == want {
123 return true
124 }
125 }
126 return false
127 }
128 }
129 _, err = pw.Write([]byte("\x1b[57442;5u"))
130 require.NoError(t, err)
131 var timer *supervisorTestTimer
132 require.Eventually(t, func() bool {
133 select {
134 case created := <-clock.created:
135 if created.delay == ctrlHoldDelay {
136 timer = created
137 }
138 default:
139 }
140 return timer != nil
141 }, 5*time.Second, time.Millisecond, "a Ctrl press arms the hold delay")
142 timer.ch <- time.Time{}
143 require.Eventually(t, sent(protocol.ModifierState{Ctrl: true}), 5*time.Second, time.Millisecond)
144
145 _, err = pw.Write([]byte("\x1b[57442;1:3u"))
146 require.NoError(t, err)
147 require.Eventually(t, sent(protocol.ModifierState{}), 5*time.Second, time.Millisecond)
148 for _, message := range stream.messages() {
149 _, isInput := message.(protocol.Input)
150 require.False(t, isInput, "modifier events are never session input")
151 }
152}