1package sessionwire
2
3import (
4 "testing"
5
6 "github.com/bnema/vev/internal/protocol"
7 "github.com/bnema/vev/internal/protocol/wire"
8 "github.com/stretchr/testify/require"
9)
10
11func pickerWireOffer() protocol.PickerOffer {
12 return protocol.PickerOffer{
13 InteractionID: 7, RequestID: 3, Intent: protocol.PickerIntentMoveTab,
14 MoveSourceKey: "ab12/work#tab-1", BarrierEpoch: 3, BarrierState: 9, SizeEpoch: 1,
15 Title: " Sessions · grouped ",
16 }
17}
18
19func pickerWireSnapshot() protocol.PickerSnapshot {
20 return protocol.NormalizePickerSnapshot(protocol.PickerSnapshot{
21 InteractionID: 7, SourceID: "serving", SourceRevision: 2, Status: protocol.PickerSourceOK,
22 Lines: []protocol.PickerLine{
23 {Kind: protocol.PickerLineSection, Label: "LOCAL", Dim: true},
24 {Key: "ab12/work", Kind: protocol.PickerLineSession, Label: "work", Focusable: true, Actions: protocol.PickerCanNavigate},
25 {Key: "ab12/work#tab-1", Kind: protocol.PickerLineTab, Label: "editor", Detail: " (vim)", Attention: true, Focusable: true, Actions: protocol.PickerCanMove},
26 },
27 Cursor: protocol.PickerCursor{Key: "ab12/work", Index: 1},
28 })
29}
30
31func pickerWireSelection() protocol.PickerSelection {
32 return protocol.PickerSelection{
33 CauseActionID: 9, RequestID: 3, InteractionID: 7, SourceID: "serving",
34 SourceRevision: 2, Key: "ab12/work", Action: protocol.PickerActionMove,
35 }
36}
37
38func TestPickerClientMessagesEncodeWithTypes(t *testing.T) {
39 closeMessage := protocol.PickerClose{InteractionID: 7, RequestID: 3}
40 selection := pickerWireSelection()
41 tests := []struct {
42 name string
43 message protocol.ClientMessage
44 }{
45 {name: "close", message: closeMessage},
46 {name: "close pointer", message: &closeMessage},
47 {name: "selection", message: selection},
48 {name: "selection pointer", message: &selection},
49 }
50 for _, tt := range tests {
51 t.Run(tt.name, func(t *testing.T) {
52 raw := &scriptedTransport{recv: []wire.Envelope{mustPreambleResponse(t)}}
53 require.NoError(t, NewClientConnection(raw).SendClient(tt.message))
54 got, err := DecodeClientEnvelope(mustSingleAppPayload(t, raw))
55 require.NoError(t, err)
56 switch message := tt.message.(type) {
57 case *protocol.PickerClose:
58 require.Equal(t, *message, got)
59 case *protocol.PickerSelection:
60 require.Equal(t, *message, got)
61 default:
62 require.Equal(t, tt.message, got)
63 }
64 })
65 }
66}
67
68func TestPickerServerMessagesEncodeWithTypes(t *testing.T) {
69 offer := pickerWireOffer()
70 closed := protocol.PickerClosed{InteractionID: 7, BarrierEpoch: 3, BarrierState: 9}
71 result := protocol.PickerResult{CauseActionID: 9, InteractionID: 7, SourceID: "serving", Key: "ab12/work", Action: protocol.PickerActionKill}
72 failure := protocol.PickerFailure{CauseActionID: 9, InteractionID: 7, SourceID: "serving", Key: "ab12/work", Action: protocol.PickerActionNavigate, Code: protocol.PickerRetiredTarget}
73 tests := []struct {
74 name string
75 message protocol.ServerMessage
76 }{
77 {name: "offer", message: offer},
78 {name: "offer pointer", message: &offer},
79 {name: "closed", message: closed},
80 {name: "closed pointer", message: &closed},
81 {name: "result", message: result},
82 {name: "result pointer", message: &result},
83 {name: "failure", message: failure},
84 {name: "failure pointer", message: &failure},
85 }
86 for _, tt := range tests {
87 t.Run(tt.name, func(t *testing.T) {
88 raw := &scriptedTransport{}
89 require.NoError(t, NewServerConnection(raw).SendServer(tt.message))
90 require.Equal(t, 1, raw.sentLen())
91 got, err := DecodeServerEnvelope(raw.sentPayload(0))
92 require.NoError(t, err)
93 switch message := tt.message.(type) {
94 case *protocol.PickerOffer:
95 require.Equal(t, *message, got)
96 case *protocol.PickerClosed:
97 require.Equal(t, *message, got)
98 case *protocol.PickerResult:
99 require.Equal(t, *message, got)
100 case *protocol.PickerFailure:
101 require.Equal(t, *message, got)
102 default:
103 require.Equal(t, tt.message, got)
104 }
105 })
106 }
107}
108
109func TestPickerSnapshotNoDaemonStatusRoundTrips(t *testing.T) {
110 line := protocol.PickerLine{Status: protocol.PickerLineStatusNoDaemon, StatusDetail: "no daemon — Enter to create a session"}
111 encoded := pickerLineToWire(line)
112 require.Equal(t, uint32(protocol.PickerLineStatusNoDaemon), encoded.GetStatus())
113 require.Equal(t, line.StatusDetail, encoded.GetStatusDetail())
114 decoded, err := pickerLineFromWire(encoded)
115 require.NoError(t, err)
116 require.Equal(t, line, decoded)
117}
118
119func TestPickerSnapshotNormalizesIntoProjections(t *testing.T) {
120 snapshot := pickerWireSnapshot()
121 raw := &scriptedTransport{}
122 require.NoError(t, NewServerConnection(raw).SendServer(snapshot))
123 require.Equal(t, 1, raw.sentLen())
124 got, err := DecodeServerEnvelope(raw.sentPayload(0))
125 require.NoError(t, err)
126 decoded, ok := got.(protocol.PickerSnapshot)
127 require.True(t, ok, "got %T", got)
128 require.Equal(t, snapshot.Lines, decoded.Lines)
129 require.Equal(t, snapshot.Cursor, decoded.Cursor)
130 require.Equal(t, snapshot.Recent, decoded.Recent)
131 require.Equal(t, snapshot.Grouped, decoded.Grouped)
132 for _, projection := range []protocol.PickerProjection{decoded.Recent, decoded.Grouped} {
133 require.Len(t, projection.Lines, 3)
134 require.Equal(t, "ab12/work", projection.Cursor.Key)
135 }
136}
137
138func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {
139 offer := pickerWireOffer()
140 snapshot := pickerWireSnapshot()
141 selection := pickerWireSelection()
142 clientMessages := []protocol.ClientMessage{
143 protocol.PickerClose{InteractionID: 1},
144 selection,
145 }
146 serverMessages := []protocol.ServerMessage{
147 offer,
148 snapshot,
149 protocol.PickerClosed{InteractionID: 1},
150 protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "a/b", Action: protocol.PickerActionKill},
151 protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionKill, Code: protocol.PickerUnknownKey},
152 }
153 // Cross-direction decodability is structural, not directional: field
154 // numbers collide across envelopes, so a client payload may parse as
155 // an unrelated server message and vice versa. Direction is enforced
156 // structurally only where variants are unmapped; these cases pin the
157 // deterministic classification the scanner produces today. Honest
158 // wrong-direction rejection requires production to bind direction
159 // into the envelope (see preamble epoch/role).
160 for _, message := range clientMessages {
161 raw := mustEncodeClient(t, message)
162 got, err := DecodeClientEnvelope(raw)
163 require.NoError(t, err)
164 require.Equal(t, message, got)
165 }
166 for _, message := range serverMessages {
167 raw := mustEncodeServer(t, message)
168 got, err := DecodeServerEnvelope(raw)
169 require.NoError(t, err)
170 if snapshot, ok := message.(protocol.PickerSnapshot); ok {
171 decoded, ok := got.(protocol.PickerSnapshot)
172 require.True(t, ok, "got %T", got)
173 // Converters normalize into Recent/Grouped projections and
174 // repopulate the Lines/Cursor construction aliases.
175 require.Equal(t, snapshot.Lines, decoded.Lines)
176 require.Equal(t, snapshot.Cursor, decoded.Cursor)
177 require.Equal(t, snapshot.Recent, decoded.Recent)
178 require.Equal(t, snapshot.Grouped, decoded.Grouped)
179 continue
180 }
181 require.Equal(t, message, got)
182 }
183}