1package client
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 TestSessionTabMemoryIsolation(t *testing.T) {
13 target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "alpha"}
14 local := routeAuthority{local: true}
15 var memory sessionTabMemory
16 memory.remember(local, target, "t_second")
17 memory.remember(local, target, "t_latest")
18 var otherClient sessionTabMemory
19 for _, tt := range []struct {
20 name string
21 memory *sessionTabMemory
22 authority routeAuthority
23 target protocol.ExactSessionTarget
24 explicit domain.TabStableID
25 want domain.TabStableID
26 }{
27 {name: "latest committed tab", memory: &memory, authority: local, target: target, want: "t_latest"},
28 {name: "explicit selection wins", memory: &memory, authority: local, target: target, explicit: "t_explicit", want: "t_explicit"},
29 {name: "other host", memory: &memory, authority: routeAuthority{endpoint: "remote"}, target: target},
30 {name: "other client", memory: &otherClient, authority: local, target: target},
31 {name: "recreated session", memory: &memory, authority: local, target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{2}, SessionName: "alpha"}},
32 {name: "no memory", authority: local, target: target},
33 } {
34 t.Run(tt.name, func(t *testing.T) {
35 require.Equal(t, tt.want, tt.memory.preferred(tt.authority, tt.target, tt.explicit))
36 })
37 }
38}
39
40func TestSessionTabMemorySurvivesAttachmentSwap(t *testing.T) {
41 for _, local := range []bool{true, false} {
42 name := "remote"
43 if local {
44 name = "local"
45 }
46 t.Run(name, func(t *testing.T) {
47 terminal := newWorkerTestTerminal()
48 supervisor := &Supervisor{
49 cfg: SupervisorConfig{Terminal: terminal}, clientID: [16]byte{1},
50 attachments: &attachmentHost{geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, geometryValid: true},
51 }
52 request := sessionTestRequest(local)
53 environment := sessionTestWorkerConfig(request).SessionEnvironment
54 worker, err := supervisor.newAttachmentWorker(request, attachmentTab{}, nil, environment)
55 require.NoError(t, err)
56 view := *sessionTestOutput(1, "").Context
57 view.TabID = "t_second"
58 worker.(*sessionAttachmentWorker).noteCommitted(nil, view)
59 other := view
60 other.Route.Target = protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{2}, SessionName: "beta"}
61 other.TabID = "t_beta"
62 worker.(*sessionAttachmentWorker).noteCommitted(nil, other)
63
64 for _, explicit := range []domain.TabStableID{"", "t_explicit"} {
65 stream := newSessionTestStream()
66 attachments := newSessionTestAttachments()
67 next, err := supervisor.newAttachmentWorker(request, attachmentTab{preferred: explicit}, nil, environment)
68 require.NoError(t, err)
69 host := newWorkerTestHost(terminal, nil, attachments.record)
70 events := make(chan AttachmentEvent, 1)
71 go func() {
72 event, _ := host.Run(t.Context(), AttachmentToken{Generation: 1, Attempt: 1}, next, stream)
73 events <- event
74 }()
75 hello := awaitHello(t, stream)
76 want := explicit
77 if want == "" {
78 want = "t_second"
79 }
80 require.Equal(t, want, hello.PreferredTabID)
81 require.NoError(t, protocol.ValidateHello(hello))
82 require.NoError(t, stream.Close())
83 <-events
84 }
85 })
86 }
87}
88
89func TestSessionTabMemorySamePeerRoundTrip(t *testing.T) {
90 stream := newSessionTestStream()
91 terminal := newWorkerTestTerminal()
92 attachments := newSessionTestAttachments()
93 var memory sessionTabMemory
94 cfg := sessionTestWorkerConfig(sessionTestRequest(true))
95 cfg.Tabs = &memory
96 events := sessionTestRun(t, stream, terminal, cfg, attachments)
97 t.Cleanup(func() { _ = stream.Close(); <-events })
98 awaitHello(t, stream)
99 stream.deliver(protocol.Welcome{SessionName: "alpha"})
100 initial := sessionTestOutput(1, "initial")
101 initial.Context.TabID = "t_second"
102 stream.deliver(initial)
103 attachments.token(t)
104 alpha := initial.Context.Route.Target
105 beta := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{2}, SessionName: "beta"}
106 stream.deliver(protocol.AttachTarget{Session: "beta", Intent: protocol.IntentAttach, ExactTarget: &beta, SamePeer: true, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned})
107 first := awaitSent(t, stream, "SamePeerSwitchRequest", isSent[protocol.SamePeerSwitchRequest]).(protocol.SamePeerSwitchRequest)
108 require.Empty(t, first.PreferredTabID)
109 output := sessionTestOutput(2, "beta")
110 output.Epoch = 2
111 output.Context.Route.Target = beta
112 output.Context.TabID = "t_beta"
113 stream.deliver(output)
114 awaitSent(t, stream, "Ack", func(message protocol.ClientMessage) bool {
115 ack, ok := message.(protocol.Ack)
116 return ok && ack.Epoch == output.Epoch
117 })
118 stream.deliver(protocol.AttachTarget{Session: "alpha", Intent: protocol.IntentAttach, ExactTarget: &alpha, SamePeer: true, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned})
119 second := awaitSent(t, stream, "return switch", func(message protocol.ClientMessage) bool {
120 request, ok := message.(protocol.SamePeerSwitchRequest)
121 return ok && request.RequestID == 2
122 }).(protocol.SamePeerSwitchRequest)
123 require.Equal(t, domain.TabStableID("t_second"), second.PreferredTabID)
124 stream.deliver(protocol.SamePeerSwitchFailure{RequestID: 2, Code: protocol.SamePeerSwitchStaleTarget})
125 stream.deliver(protocol.AttachTarget{Session: "alpha", Intent: protocol.IntentAttach, ExactTarget: &alpha, SamePeer: true, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, PreferredTabID: "t_explicit"})
126 third := awaitSent(t, stream, "explicit switch", func(message protocol.ClientMessage) bool {
127 request, ok := message.(protocol.SamePeerSwitchRequest)
128 return ok && request.RequestID == 3
129 }).(protocol.SamePeerSwitchRequest)
130 require.Equal(t, domain.TabStableID("t_explicit"), third.PreferredTabID)
131}
132
133func TestSessionTabMemoryLifecycleCleanup(t *testing.T) {
134 local := routeAuthority{local: true}
135 remote := routeAuthority{endpoint: "remote"}
136 alpha := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "alpha"}
137 beta := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "beta"}
138 replacement := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{2}, SessionName: "alpha"}
139 var memory sessionTabMemory
140 memory.remember(local, alpha, "t_old")
141 memory.remember(remote, alpha, "t_remote")
142 memory.remember(local, beta, "t_beta")
143 memory.remember(local, replacement, "t_new")
144 memory.remember(local, replacement, "t_latest")
145 for _, tt := range []struct {
146 name string
147 authority routeAuthority
148 target protocol.ExactSessionTarget
149 want domain.TabStableID
150 }{
151 {name: "old lifecycle reclaimed", authority: local, target: alpha},
152 {name: "new lifecycle retained", authority: local, target: replacement, want: "t_latest"},
153 {name: "other host retained", authority: remote, target: alpha, want: "t_remote"},
154 {name: "other session retained", authority: local, target: beta, want: "t_beta"},
155 } {
156 t.Run(tt.name, func(t *testing.T) { require.Equal(t, tt.want, memory.preferred(tt.authority, tt.target, "")) })
157 }
158}
159
160func TestSessionTabMemoryPreservesStoppedRemoteSelector(t *testing.T) {
161 request := sessionTestRequest(false)
162 var memory sessionTabMemory
163 memory.remember(requestAuthority(request), request.Target, "t_remembered")
164 selector := protocol.SessionAttachTarget{LifecycleID: request.Target.LifecycleID, SessionName: request.Target.SessionName, Stopped: true, TabID: "t_explicit", TabIndex: protocol.NoTabIndex}
165 cfg := sessionTestWorkerConfig(request)
166 cfg.Tabs = &memory
167 cfg.Tab = attachmentTab{stopped: &selector}
168 cfg.SessionEnvironment = SessionEnvironment{Provenance: SessionEnvironmentRemote}
169 stream := newSessionTestStream()
170 events := sessionTestRun(t, stream, newWorkerTestTerminal(), cfg, newSessionTestAttachments())
171 t.Cleanup(func() { _ = stream.Close(); <-events })
172 hello := awaitHello(t, stream)
173 require.Equal(t, &selector, hello.SessionTarget)
174 require.Empty(t, hello.PreferredTabID)
175 require.NoError(t, protocol.ValidateHello(hello))
176}