v0.8.1 1f54f61

picker_interaction_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close