1package client
2
3import (
4 "context"
5 "errors"
6 "io"
7 "testing"
8 "time"
9
10 "github.com/stretchr/testify/require"
11
12 "github.com/bnema/vev/internal/domain"
13 "github.com/bnema/vev/internal/protocol"
14)
15
16func TestStripTerminalFocusReports(t *testing.T) {
17 tests := []struct {
18 name string
19 start domain.TerminalFocus
20 input string
21 want string
22 wantFocus domain.TerminalFocus
23 changed bool
24 }{
25 {name: "plain input passes through", input: "ls\r", want: "ls\r"},
26 {name: "focus in only", input: "\x1b[I", want: "", wantFocus: domain.TerminalFocusFocused, changed: true},
27 {name: "focus out only", start: domain.TerminalFocusFocused, input: "\x1b[O", want: "", wantFocus: domain.TerminalFocusUnfocused, changed: true},
28 {name: "last report wins", input: "\x1b[I\x1b[O", want: "", wantFocus: domain.TerminalFocusUnfocused, changed: true},
29 {name: "keys around a report keep their order", input: "a\x1b[Ib", want: "ab", wantFocus: domain.TerminalFocusFocused, changed: true},
30 {name: "arrow keys and csi are untouched", input: "\x1b[A\x1b[1;5C\x1b[<0;1;1M", want: "\x1b[A\x1b[1;5C\x1b[<0;1;1M"},
31 {name: "a lone escape is never withheld", input: "\x1b", want: "\x1b"},
32 {name: "a split report is forwarded as typed", input: "\x1b[", want: "\x1b["},
33 {name: "a repeated state changes nothing", start: domain.TerminalFocusFocused, input: "\x1b[I", want: "", wantFocus: domain.TerminalFocusFocused},
34 }
35 for _, tt := range tests {
36 t.Run(tt.name, func(t *testing.T) {
37 state := newTerminalFocusState()
38 state.set(tt.start)
39 _, changed := state.Watch()
40
41 got := stripTerminalFocusReports(state, []byte(tt.input))
42
43 require.Equal(t, tt.want, string(got))
44 want := tt.wantFocus
45 if want == domain.TerminalFocusUnknown {
46 want = tt.start
47 }
48 require.Equal(t, want, state.Focus())
49 select {
50 case <-changed:
51 require.True(t, tt.changed, "no change was expected")
52 default:
53 require.False(t, tt.changed, "a change was expected")
54 }
55 })
56 }
57}
58
59func TestFocusReporterSendsKnownChangesOnce(t *testing.T) {
60 state := newTerminalFocusState()
61 reporter := &focusReporter{state: state}
62
63 _, ok := reporter.next()
64 require.False(t, ok, "unknown focus is never sent")
65
66 steps := []struct {
67 focus domain.TerminalFocus
68 want protocol.TerminalFocus
69 wantOK bool
70 }{
71 {focus: domain.TerminalFocusFocused, want: protocol.TerminalFocus{Focus: domain.TerminalFocusFocused}, wantOK: true},
72 {focus: domain.TerminalFocusUnfocused, want: protocol.TerminalFocus{Focus: domain.TerminalFocusUnfocused}, wantOK: true},
73 {focus: domain.TerminalFocusUnfocused},
74 }
75 for _, step := range steps {
76 wake := reporter.changed
77 state.set(step.focus)
78 if step.wantOK {
79 select {
80 case <-wake:
81 default:
82 t.Fatalf("a change to %s must wake the reporter", step.focus)
83 }
84 }
85 got, ok := reporter.next()
86 require.Equal(t, step.wantOK, ok)
87 require.Equal(t, step.want, got)
88 }
89}
90
91// TestTerminalInputPumpStripsFocusReports proves focus reports never reach an
92// attachment: a read that only carried a report delivers nothing, the focus
93// the terminal reported is readable on the foreground, and later keys arrive
94// intact.
95func TestTerminalInputPumpStripsFocusReports(t *testing.T) {
96 pr, pw := io.Pipe()
97 pump := newTerminalInputPump(pr)
98 pump.start()
99 defer pump.stop()
100 defer pw.Close()
101 host := newWorkerTestHost(newWorkerTestTerminal(), pump, nil)
102
103 observed := make(chan []byte, 1)
104 focus := make(chan domain.TerminalFocus, 1)
105 worker := &fakeWorker{run: func(ctx context.Context, fg AttachmentForeground) AttachmentEvent {
106 event, ok := fg.Input(ctx)
107 if !ok {
108 return AttachmentEvent{Kind: AttachmentEventFailed, Err: errors.New("no input")}
109 }
110 fg.AckInput()
111 observed <- append([]byte(nil), event.Data...)
112 focus <- attachmentTerminalFocus(fg).Focus()
113 return AttachmentEvent{Kind: AttachmentEventEnded}
114 }}
115
116 go func() {
117 _, _ = pw.Write([]byte("\x1b[O"))
118 _, _ = pw.Write([]byte("\x1b[Iab"))
119 }()
120 ctx, cancel := context.WithTimeout(context.Background(), time.Second)
121 defer cancel()
122 _, adopted := host.Run(ctx, AttachmentToken{Generation: 1, Attempt: 1}, worker, newWorkerTestStream())
123 require.True(t, adopted)
124
125 require.Equal(t, []byte("ab"), <-observed, "the focus-only read is never delivered")
126 require.Equal(t, domain.TerminalFocusFocused, <-focus)
127}
128
129func TestNilTerminalFocusStateIsUnknown(t *testing.T) {
130 var state *TerminalFocusState
131 focus, changed := state.Watch()
132 require.Equal(t, domain.TerminalFocusUnknown, focus)
133 require.Nil(t, changed)
134 require.Equal(t, domain.TerminalFocusUnknown, state.Focus())
135}
136
137// TestAttachmentDeclaresFocusInHelloThenReportsChanges proves the daemon
138// learns a terminal's focus before its first paint: Hello carries the focus
139// already known, and each later change follows as TerminalFocus.
140func TestAttachmentDeclaresFocusInHelloThenReportsChanges(t *testing.T) {
141 pump := newTerminalInputPump(blockingReader{})
142 pump.focus.set(domain.TerminalFocusUnfocused)
143 stream := newSessionTestStream()
144 host := newWorkerTestHost(newWorkerTestTerminal(), pump, nil)
145 worker, err := newSessionAttachmentWorker(sessionTestWorkerConfig(sessionTestRequest(true)))
146 require.NoError(t, err)
147 ctx, cancel := context.WithCancel(context.Background())
148 defer cancel()
149 go func() { _, _ = host.Run(ctx, AttachmentToken{Generation: 4, Attempt: 1}, worker, stream) }()
150
151 hello := awaitHello(t, stream)
152 require.Equal(t, domain.TerminalFocusUnfocused, hello.TerminalFocus)
153 require.NoError(t, protocol.ValidateHello(hello))
154
155 stream.deliver(protocol.Welcome{SessionName: "alpha"})
156 stream.deliver(sessionTestOutput(1, "\x1b[Hready"))
157 pump.focus.set(domain.TerminalFocusFocused)
158 require.Eventually(t, func() bool {
159 for _, message := range stream.messages() {
160 if message == (protocol.TerminalFocus{Focus: domain.TerminalFocusFocused}) {
161 return true
162 }
163 }
164 return false
165 }, 5*time.Second, time.Millisecond, "a focus change after attach is reported")
166}
167
168// blockingReader is a terminal that never sends input.
169type blockingReader struct{}
170
171func (blockingReader) Read([]byte) (int, error) { select {} }