same_peer_switch_test.go RawHistoryFolder

1package daemon
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/require"
7
8	"github.com/bnema/vev/internal/domain"
9	"github.com/bnema/vev/internal/protocol"
10)
11
12func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {
13	d, source, ac, sends, releases := newManualTabSession(t, 1)
14	defer releaseAll(releases)
15
16	lifecycle := domain.SessionLifecycleID{7}
17	target := &session{
18		sessionCore: sessionCore{
19			id: "target", name: "target", incarnation: lifecycle,
20			attachments: make(map[*attachedClient]struct{}),
21		},
22		ctx: source.ctx, cancel: func() {},
23		tabs: []*tab{newTab(nil, domain.Size{Cols: 80, Rows: 23})},
24	}
25	publishTiledPaneOwners(target, target.tabs[0])
26	d.mu.Lock()
27	d.sessions[target.id] = target
28	d.mu.Unlock()
29	ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
30
31	token := beginRecentRoutePaletteEffect(t, d, source, ac)
32	d.handleInputForAttachment(token, []byte("\x1b "))
33	awaitFrame(t, sends, "Output")
34	d.handleInputForAttachment(token, []byte("target\r"))
35	targetFrame := awaitFrame(t, sends, "AttachTarget")
36	attachTarget := decodeServerMessage(t, targetFrame).(protocol.AttachTarget)
37	require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, attachTarget.ExactTarget)
38
39	d.switchSamePeerForAttachment(token, protocol.SamePeerSwitchRequest{
40		RequestID: 1, Target: *attachTarget.ExactTarget,
41	})
42
43	require.Same(t, target, ac.currentAttachmentSession())
44	identityFrame := awaitFrame(t, sends, "CommittedRouteIdentity")
45	identity := decodeServerMessage(t, identityFrame).(protocol.CommittedRouteIdentity)
46	require.Equal(t, attachTarget.ExactTarget, &identity.Target)
47}
48
49func TestSamePeerSwitchTransitionsExactTargetAndPreferredTab(t *testing.T) {
50	d, source, ac, sends, releases := newManualTabSession(t, 1)
51	defer releaseAll(releases)
52
53	lifecycle := domain.SessionLifecycleID{7}
54	target := &session{
55		sessionCore: sessionCore{id: "target", name: "target", incarnation: lifecycle, attachments: make(map[*attachedClient]struct{})},
56		ctx:         source.ctx,
57		cancel:      func() {},
58		tabs: []*tab{
59			newTab(nil, domain.Size{Cols: 80, Rows: 23}),
60			newTab(nil, domain.Size{Cols: 80, Rows: 23}),
61		},
62	}
63	for _, tab := range target.tabs {
64		publishTiledPaneOwners(target, tab)
65	}
66	d.mu.Lock()
67	d.sessions[target.id] = target
68	d.mu.Unlock()
69	ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
70
71	token := source.captureAttachmentCapability(ac, ac.transport())
72	effect, admitted := ac.beginAttachmentEffect(token)
73	require.True(t, admitted)
74	defer effect.End()
75	requestTarget := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}
76	ac.offerSamePeerTarget(requestTarget)
77
78	d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{
79		RequestID:      1,
80		Target:         requestTarget,
81		PreferredTabID: domain.TabStableID(target.tabs[1].stableID),
82	})
83
84	require.Same(t, target, ac.currentAttachmentSession())
85	require.Equal(t, domain.TabStableID(target.tabs[1].stableID), ac.viewSnapshot().tabID)
86	identityFrame := awaitFrame(t, sends, "CommittedRouteIdentity")
87	identity := decodeServerMessage(t, identityFrame).(protocol.CommittedRouteIdentity)
88	require.Equal(t, protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, identity.Target)
89}
90
91func TestFinishSendErrorDetachClearsSamePeerOffer(t *testing.T) {
92	d, sess, ac, _, releases := newManualTabSession(t, 1)
93	defer releaseAll(releases)
94	ac.offerSamePeerTarget(protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "target"})
95
96	d.finishSendErrorDetach(sess, ac, ac.transport())
97
98	ac.samePeerOfferMu.Lock()
99	defer ac.samePeerOfferMu.Unlock()
100	require.Nil(t, ac.samePeerOffer)
101}
102
103func TestSamePeerSwitchRejectsStaleTargetWithoutMutation(t *testing.T) {
104	d, source, ac, sends, releases := newManualTabSession(t, 1)
105	defer releaseAll(releases)
106	ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
107
108	token := source.captureAttachmentCapability(ac, ac.transport())
109	effect, admitted := ac.beginAttachmentEffect(token)
110	require.True(t, admitted)
111	defer effect.End()
112
113	d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{
114		RequestID: 1,
115		Target:    protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{9}, SessionName: "missing"},
116	})
117
118	require.Same(t, source, ac.currentAttachmentSession())
119	failureFrame := awaitFrame(t, sends, "SamePeerSwitchFailure")
120	failure := decodeServerMessage(t, failureFrame).(protocol.SamePeerSwitchFailure)
121	require.Equal(t, protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchStaleTarget}, failure)
122}
123
124func TestSamePeerSwitchRefreshesTargetFromSwitchingAttachment(t *testing.T) {
125	targetEnv := []string{"SHELL=/usr/bin/fish", "WAYLAND_DISPLAY=wayland-old", "SSH_AUTH_SOCK=/run/other-agent"}
126	tests := []struct {
127		name      string
128		clientEnv []string
129		want      []string
130	}{
131		{
132			name:      "local attachment refreshes session-bound variables",
133			clientEnv: []string{"SHELL=/bin/bash", "WAYLAND_DISPLAY=wayland-1"},
134			want:      []string{"SHELL=/usr/bin/fish", "WAYLAND_DISPLAY=wayland-1"},
135		},
136		{
137			name: "daemon-owned attachment leaves the target untouched",
138			want: targetEnv,
139		},
140	}
141	for _, tt := range tests {
142		t.Run(tt.name, func(t *testing.T) {
143			d, source, ac, _, releases := newManualTabSession(t, 1)
144			defer releaseAll(releases)
145			ac.setClientEnvironment(tt.clientEnv)
146
147			lifecycle := domain.SessionLifecycleID{7}
148			target := &session{
149				sessionCore: sessionCore{id: "target", name: "target", incarnation: lifecycle, attachments: make(map[*attachedClient]struct{})},
150				ctx:         source.ctx,
151				cancel:      func() {},
152				tabs:        []*tab{newTab(nil, domain.Size{Cols: 80, Rows: 23})},
153				env:         append([]string(nil), targetEnv...),
154			}
155			publishTiledPaneOwners(target, target.tabs[0])
156			d.mu.Lock()
157			d.sessions[target.id] = target
158			d.mu.Unlock()
159			ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
160
161			token := source.captureAttachmentCapability(ac, ac.transport())
162			effect, admitted := ac.beginAttachmentEffect(token)
163			require.True(t, admitted)
164			defer effect.End()
165			requestTarget := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}
166			ac.offerSamePeerTarget(requestTarget)
167
168			d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{RequestID: 1, Target: requestTarget})
169
170			require.Same(t, target, ac.currentAttachmentSession())
171			target.mu.Lock()
172			defer target.mu.Unlock()
173			require.Equal(t, tt.want, target.env)
174		})
175	}
176}

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