v0.7.0 4c16dd8

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
124// TestSamePeerSwitchClientInitiated pins that the client picker can move its
125// attachment without a daemon offer, while a pending offer still admits only
126// its own exact target.
127func TestSamePeerSwitchClientInitiated(t *testing.T) {
128	lifecycle := domain.SessionLifecycleID{7}
129	requestTarget := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}
130	otherOffer := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{8}, SessionName: "other"}
131	tests := []struct {
132		name     string
133		offer    *protocol.ExactSessionTarget
134		switched bool
135	}{
136		{name: "no pending offer switches", switched: true},
137		{name: "matching offer switches", offer: &requestTarget, switched: true},
138		{name: "another pending offer refuses", offer: &otherOffer},
139	}
140	for _, tt := range tests {
141		t.Run(tt.name, func(t *testing.T) {
142			d, source, ac, sends, releases := newManualTabSession(t, 1)
143			defer releaseAll(releases)
144			target := &session{
145				sessionCore: sessionCore{id: "target", name: "target", incarnation: lifecycle, attachments: make(map[*attachedClient]struct{})},
146				ctx:         source.ctx,
147				cancel:      func() {},
148				tabs:        []*tab{newTab(nil, domain.Size{Cols: 80, Rows: 23})},
149			}
150			publishTiledPaneOwners(target, target.tabs[0])
151			d.mu.Lock()
152			d.sessions[target.id] = target
153			d.mu.Unlock()
154			ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
155			if tt.offer != nil {
156				ac.offerSamePeerTarget(*tt.offer)
157			}
158
159			token := source.captureAttachmentCapability(ac, ac.transport())
160			effect, admitted := ac.beginAttachmentEffect(token)
161			require.True(t, admitted)
162			defer effect.End()
163			d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{RequestID: 1, Target: requestTarget})
164
165			if tt.switched {
166				require.Same(t, target, ac.currentAttachmentSession())
167				identity := decodeServerMessage(t, awaitFrame(t, sends, "CommittedRouteIdentity")).(protocol.CommittedRouteIdentity)
168				require.Equal(t, requestTarget, identity.Target)
169				return
170			}
171			require.Same(t, source, ac.currentAttachmentSession())
172			failure := decodeServerMessage(t, awaitFrame(t, sends, "SamePeerSwitchFailure")).(protocol.SamePeerSwitchFailure)
173			require.Equal(t, protocol.SamePeerSwitchStaleTarget, failure.Code)
174		})
175	}
176}
177
178func TestSamePeerSwitchRefreshesTargetFromSwitchingAttachment(t *testing.T) {
179	targetEnv := []string{"SHELL=/usr/bin/fish", "WAYLAND_DISPLAY=wayland-old", "SSH_AUTH_SOCK=/run/other-agent"}
180	tests := []struct {
181		name      string
182		clientEnv []string
183		want      []string
184	}{
185		{
186			name:      "local attachment refreshes session-bound variables",
187			clientEnv: []string{"SHELL=/bin/bash", "WAYLAND_DISPLAY=wayland-1"},
188			want:      []string{"SHELL=/usr/bin/fish", "WAYLAND_DISPLAY=wayland-1"},
189		},
190		{
191			name: "daemon-owned attachment leaves the target untouched",
192			want: targetEnv,
193		},
194	}
195	for _, tt := range tests {
196		t.Run(tt.name, func(t *testing.T) {
197			d, source, ac, _, releases := newManualTabSession(t, 1)
198			defer releaseAll(releases)
199			ac.setClientEnvironment(tt.clientEnv)
200
201			lifecycle := domain.SessionLifecycleID{7}
202			target := &session{
203				sessionCore: sessionCore{id: "target", name: "target", incarnation: lifecycle, attachments: make(map[*attachedClient]struct{})},
204				ctx:         source.ctx,
205				cancel:      func() {},
206				tabs:        []*tab{newTab(nil, domain.Size{Cols: 80, Rows: 23})},
207				env:         append([]string(nil), targetEnv...),
208			}
209			publishTiledPaneOwners(target, target.tabs[0])
210			d.mu.Lock()
211			d.sessions[target.id] = target
212			d.mu.Unlock()
213			ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
214
215			token := source.captureAttachmentCapability(ac, ac.transport())
216			effect, admitted := ac.beginAttachmentEffect(token)
217			require.True(t, admitted)
218			defer effect.End()
219			requestTarget := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}
220			ac.offerSamePeerTarget(requestTarget)
221
222			d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{RequestID: 1, Target: requestTarget})
223
224			require.Same(t, target, ac.currentAttachmentSession())
225			target.mu.Lock()
226			defer target.mu.Unlock()
227			require.Equal(t, tt.want, target.env)
228		})
229	}
230}

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