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}