session_tab_memory_test.go RawHistoryFolder

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}

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