recent_route_snapshot_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5
6	"github.com/bnema/vev/internal/protocol"
7	"github.com/bnema/vev/internal/usecase/command"
8	"github.com/stretchr/testify/require"
9)
10
11func TestAttachmentStatusUsesClientRouteSnapshot(t *testing.T) {
12	d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
13	ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
14		Generation: 2,
15		Active:     protocol.RouteRef{Key: 3, Generation: 2},
16		Entries: []protocol.RecentRouteEntry{
17			{Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: protocol.RouteKindRemote, Visited: true},
18			testRouteEntry(1, 1, "work", 1, protocol.RouteKindLocal),
19		},
20	})
21
22	state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
23
24	require.Len(t, state.mru, 2)
25	require.Equal(t, []string{"logs@edge", "work"}, []string{state.mru[0].name, state.mru[1].name})
26}
27
28func TestAttachmentStatusUsesClientPublishedAttention(t *testing.T) {
29	d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
30	ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
31		Generation: 2,
32		Active:     protocol.RouteRef{Key: 3, Generation: 2},
33		Entries: []protocol.RecentRouteEntry{{
34			Key: 2, Generation: 1, Target: testRouteTarget("local", 9), Name: "local", Kind: protocol.RouteKindLocal, Attention: true, Visited: true,
35		}},
36	})
37
38	state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
39
40	require.Len(t, state.mru, 1)
41	require.True(t, state.mru[0].attention)
42}
43
44func TestRecentRouteSnapshotRepaintsWithoutDeferredIdentity(t *testing.T) {
45	d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
46	token := sess.captureAttachmentCapability(ac, ac.transport())
47	ac.installTestAttachmentCapability(token)
48	ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
49
50	require.False(t, d.handleAttachmentClientFrame(token, mustClientEnvelope(protocol.RecentRouteSnapshot{Generation: 2})))
51
52	awaitFrame(t, sends, "Output")
53}
54
55func TestAttachmentRouteSnapshotCopiesPublishedEntries(t *testing.T) {
56	_, _, ac, _ := newManualSessionWithPTYs(t, nil)
57	entries := []protocol.RecentRouteEntry{testRouteEntry(1, 2, "before", 1, protocol.RouteKindLocal)}
58	snapshot := protocol.RecentRouteSnapshot{Generation: 3, Entries: entries}
59	ac.setRouteSnapshot(snapshot)
60
61	entries[0].Name = "after"
62	got := ac.routeSnapshotCopy()
63	require.Equal(t, "before", got.Entries[0].Name)
64	got.Entries[0].Name = "mutated copy"
65	require.Equal(t, "before", ac.routeSnapshotCopy().Entries[0].Name)
66}
67
68func TestPaletteJumpRecentSessionTargetsHistoryRank(t *testing.T) {
69	unvisited := testRouteEntry(5, 1, "other", 5, protocol.RouteKindLocal)
70	unvisited.Visited = false
71	ringing := testRouteEntry(6, 1, "loud", 6, protocol.RouteKindLocal)
72	ringing.Visited, ringing.Attention, ringing.AttentionSeq = false, true, 1
73	quiet := protocol.RecentRouteSnapshot{
74		Generation: 4,
75		Entries:    []protocol.RecentRouteEntry{unvisited, testRouteEntry(2, 3, "remote", 2, protocol.RouteKindRemote)},
76	}
77	rung := protocol.RecentRouteSnapshot{
78		Generation: 4,
79		Entries:    []protocol.RecentRouteEntry{testRouteEntry(2, 3, "remote", 2, protocol.RouteKindRemote), unvisited, ringing},
80	}
81	tests := []struct {
82		name     string
83		snapshot protocol.RecentRouteSnapshot
84		rank     int
85		wantKey  uint64
86		wantGen  uint64
87		wantErr  error
88	}{
89		{name: "rank 1 skips the unvisited entry", snapshot: quiet, rank: 1, wantKey: 2, wantGen: 3},
90		{name: "rank beyond the history is refused", snapshot: quiet, rank: 2, wantErr: command.ErrInvalidArguments},
91		{name: "rank after the visited prefix reaches a ringing entry", snapshot: rung, rank: 2, wantKey: 6, wantGen: 1},
92	}
93	for _, tt := range tests {
94		t.Run(tt.name, func(t *testing.T) {
95			d, source, ac, _ := newManualSessionWithPTYs(t, nil)
96			transport := &closeTrackingTransport{}
97			ac.replaceTransport(transport)
98			rc := d.attachCoordinator(source, nil, ac, true)
99			token := source.captureAttachmentCapability(ac, transport)
100			token.lease = rc.attachmentLease(ac)
101			ac.installTestAttachmentCapability(token)
102			effect, admitted := ac.beginAttachmentEffect(token)
103			require.True(t, admitted)
104			defer effect.End()
105
106			exec := paletteExec{d: d, sess: source, attachment: source, ac: ac, routeSnapshot: tt.snapshot, effect: effect}
107			err := exec.JumpRecentSession(tt.rank)
108			if tt.wantErr != nil {
109				require.ErrorIs(t, err, tt.wantErr)
110				return
111			}
112			require.NoError(t, err)
113
114			var action protocol.RouteNavigationAction
115			found := false
116			for _, frame := range transport.Sends() {
117				if decoded, ok := decodeServerMessage(t, frame).(protocol.RouteNavigationAction); ok {
118					action, found = decoded, true
119				}
120			}
121			require.True(t, found)
122			require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 4, Key: tt.wantKey, Generation: tt.wantGen}, action)
123			hints := recentRouteHints(tt.snapshot, nil)
124			require.Equal(t, action.Key, hints.Recent[tt.rank-1].Key, "hint and jump share the rank mapping")
125		})
126	}
127}
128
129func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {
130	snapshot := protocol.RecentRouteSnapshot{
131		Generation: 8,
132		Entries: []protocol.RecentRouteEntry{{
133			Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote, Visited: true,
134		}},
135	}
136
137	hints := recentRouteHints(snapshot, nil)
138
139	require.Len(t, hints.Recent, 1)
140	require.Equal(t, "logs@edge", hints.Recent[0].Name)
141	require.Equal(t, uint64(8), hints.Recent[0].SnapshotGeneration)
142	require.Equal(t, uint64(11), hints.Recent[0].Key)
143	require.Equal(t, uint64(7), hints.Recent[0].Generation)
144}
145
146func TestStatusHistoryShowsVisitedAndRingingRoutes(t *testing.T) {
147	unvisited := testRouteEntry(4, 1, "other", 4, protocol.RouteKindLocal)
148	unvisited.Visited = false
149	ringingLocal := testRouteEntry(5, 1, "build", 5, protocol.RouteKindLocal)
150	ringingLocal.Visited, ringingLocal.Attention, ringingLocal.AttentionSeq = false, true, 1
151	ringingRemote := testRouteEntry(6, 1, "deploy", 6, protocol.RouteKindRemote)
152	ringingRemote.Visited, ringingRemote.Attention, ringingRemote.AttentionSeq = false, true, 2
153	tests := []struct {
154		name    string
155		entries []protocol.RecentRouteEntry
156		want    []string
157	}{
158		{name: "fresh client shows no history", entries: []protocol.RecentRouteEntry{unvisited}, want: nil},
159		{name: "visited routes keep their order", entries: []protocol.RecentRouteEntry{testRouteEntry(2, 1, "work", 2, protocol.RouteKindLocal), unvisited, testRouteEntry(3, 1, "logs", 3, protocol.RouteKindLocal)}, want: []string{"work", "logs"}},
160		{name: "fresh client still shows ringing local and remote routes", entries: []protocol.RecentRouteEntry{unvisited, ringingLocal, ringingRemote}, want: []string{"build", "deploy@remote"}},
161		{name: "ringing routes follow visited ones", entries: []protocol.RecentRouteEntry{testRouteEntry(2, 1, "work", 2, protocol.RouteKindLocal), unvisited, ringingLocal}, want: []string{"work", "build"}},
162	}
163	for _, tt := range tests {
164		t.Run(tt.name, func(t *testing.T) {
165			d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
166			ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 2, Entries: tt.entries})
167
168			state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
169
170			var names []string
171			for _, entry := range state.mru {
172				names = append(names, entry.name)
173				if entry.name == "build" || entry.name == "deploy@remote" {
174					require.True(t, entry.attention, "%s must draw its bell", entry.name)
175				}
176			}
177			require.Equal(t, tt.want, names)
178			require.Equal(t, len(tt.want), len(recentRouteHints(ac.routeSnapshotCopy(), nil).Recent))
179		})
180	}
181}
182
183func TestCtrlDigitSwitchesToRecentHistoryRank(t *testing.T) {
184	snapshot := protocol.RecentRouteSnapshot{
185		Generation: 4,
186		Entries: []protocol.RecentRouteEntry{
187			testRouteEntry(2, 3, "first", 2, protocol.RouteKindLocal),
188			testRouteEntry(7, 1, "second", 7, protocol.RouteKindRemote),
189		},
190	}
191	tests := []struct {
192		name    string
193		input   string
194		wantKey uint64
195		wantGen uint64
196		none    bool
197		legacy  bool
198	}{
199		{name: "ctrl 1 previous session", input: "\x1b[49;5u", wantKey: 2, wantGen: 3},
200		{name: "ctrl 2 second previous", input: "\x1b[50;5u", wantKey: 7, wantGen: 1},
201		{name: "azerty ctrl 2 base key", input: "\x1b[233::50;5u", wantKey: 7, wantGen: 1},
202		{name: "ctrl 3 beyond history is a no-op", input: "\x1b[51;5u", none: true},
203		{name: "legacy client does not intercept", input: "\x1b[49;5u", none: true, legacy: true},
204	}
205	for _, tt := range tests {
206		t.Run(tt.name, func(t *testing.T) {
207			d, source, ac, _ := newManualSessionWithPTYs(t, nil)
208			transport := &closeTrackingTransport{}
209			ac.replaceTransport(transport)
210			rc := d.attachCoordinator(source, nil, ac, true)
211			token := source.captureAttachmentCapability(ac, transport)
212			token.lease = rc.attachmentLease(ac)
213			ac.installTestAttachmentCapability(token)
214			ac.setRouteSnapshot(snapshot)
215			ac.keys.SetKittyKeyboard(!tt.legacy)
216			effect, admitted := ac.beginAttachmentEffect(token)
217			require.True(t, admitted)
218			defer effect.End()
219
220			d.handleInputForAttachment(effect, []byte(tt.input))
221
222			var actions []protocol.RouteNavigationAction
223			for _, frame := range transport.Sends() {
224				if decoded, ok := decodeServerMessage(t, frame).(protocol.RouteNavigationAction); ok {
225					actions = append(actions, decoded)
226				}
227			}
228			if tt.none {
229				require.Empty(t, actions)
230				return
231			}
232			require.Equal(t, []protocol.RouteNavigationAction{{SnapshotGeneration: 4, Key: tt.wantKey, Generation: tt.wantGen}}, actions)
233		})
234	}
235}

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