route_position_test.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5	"testing"
6
7	"github.com/stretchr/testify/require"
8
9	"github.com/bnema/vev/internal/domain"
10	"github.com/bnema/vev/internal/protocol"
11)
12
13func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
14	for _, test := range []struct {
15		name      string
16		preferred domain.TabStableID
17		want      domain.TabStableID
18	}{
19		{name: "existing preferred tab", preferred: "tab-2", want: "tab-2"},
20		{name: "missing preferred tab falls back", preferred: "missing", want: "tab-1"},
21	} {
22		t.Run(test.name, func(t *testing.T) {
23			d := newTestDaemon(t, nil, stubClock{})
24			sess := addControlSession(d, "work", "tab-1", "pane-1")
25			sess.incarnation = domain.SessionLifecycleID{1}
26			second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
27			sess.mu.Lock()
28			sess.tabs = append(sess.tabs, second)
29			sess.mu.Unlock()
30			tr := &closeTrackingTransport{}
31			target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
32			hello := protocol.Hello{
33				Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name,
34				Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,
35				EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
36			}
37
38			_, ac, err := d.routeWithContext(context.Background(), hello, tr)
39			require.NoError(t, err)
40			sess.repairAttachmentView(ac)
41			require.Equal(t, test.want, ac.viewSnapshot().tabID)
42			d.clientGone(sess, ac, tr, false)
43		})
44	}
45}
46
47func TestResumeAppliesPreferredTabToParkedAttachment(t *testing.T) {
48	pty, release := newBlockingPTY(t)
49	defer release()
50	d := newTestDaemon(t, newFactory(t, pty), stubClock{})
51	oldTransport := &closeTrackingTransport{}
52	sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
53	require.NoError(t, err)
54	second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
55	sess.mu.Lock()
56	sess.tabs = append(sess.tabs, second)
57	sess.mu.Unlock()
58	sess.repairAttachmentView(ac)
59	token := ac.resumeToken
60	d.clientGone(sess, ac, oldTransport, false)
61
62	hello := helloResumeCapable(protocol.IntentResume, "work", token)
63	hello.PreferredTabID = "tab-2"
64	newTransport := &closeTrackingTransport{}
65	resumedSession, resumed, err := d.route(hello, newTransport)
66	require.NoError(t, err)
67	require.Same(t, ac, resumed)
68	require.Equal(t, domain.TabStableID("tab-2"), resumed.viewSnapshot().tabID)
69	d.clientGone(resumedSession, resumed, newTransport, false)
70}
71
72func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
73	d := newTestDaemon(t, nil, stubClock{})
74	sess := addControlSession(d, "work", "tab-1", "pane-1")
75	sess.incarnation = domain.SessionLifecycleID{1}
76	second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
77	sess.mu.Lock()
78	sess.tabs = append(sess.tabs, second)
79	sess.mu.Unlock()
80	tr := &closeTrackingTransport{}
81	target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
82	hello := protocol.Hello{
83		Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name,
84		Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
85	}
86	_, ac, err := d.routeWithContext(context.Background(), hello, tr)
87	require.NoError(t, err)
88	rc := sess.renderCoordinator()
89	require.True(t, rc.markAttachmentReady(rc.attachmentLease(ac)))
90	sess.repairAttachmentView(ac)
91
92	require.True(t, d.firstPaintForTransition(sess.captureAttachmentCapability(ac, tr)))
93	sess.selectAttachmentTab(ac, "tab-2")
94	d.paint(sess, ac, false, nil)
95
96	var positions []protocol.RoutePosition
97	for _, frame := range tr.Sends() {
98		position, ok := decodeServerMessage(t, frame).(protocol.RoutePosition)
99		if !ok {
100			continue
101		}
102		positions = append(positions, position)
103	}
104	require.Equal(t, []protocol.RoutePosition{
105		{Target: target, ActiveTabID: "tab-1"},
106		{Target: target, ActiveTabID: "tab-2"},
107	}, positions)
108	d.clientGone(sess, ac, tr, false)
109}
110
111func TestWarmAndColdAttachmentPreferredTab(t *testing.T) {
112	for _, disappeared := range []bool{false, true} {
113		name := "matching retained tab"
114		if disappeared {
115			name = "retained tab disappeared"
116		}
117		t.Run(name, func(t *testing.T) {
118			d := newTestDaemon(t, nil, stubClock{})
119			sess := addControlSession(d, "work", "tab-1", "pane-1")
120			sess.incarnation = domain.SessionLifecycleID{1}
121			second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
122			sess.mu.Lock()
123			sess.tabs = append(sess.tabs, second)
124			sess.mu.Unlock()
125			target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
126			hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name, Size: defaultSize, ExactTarget: &target, PreferredTabID: "tab-2", EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
127			warmTransport := &closeTrackingTransport{}
128			_, warm, err := d.routeWithContext(context.Background(), hello, warmTransport)
129			require.NoError(t, err)
130			sess.repairAttachmentView(warm)
131			require.Equal(t, domain.TabStableID("tab-2"), warm.viewSnapshot().tabID)
132			rc := sess.renderCoordinator()
133			require.True(t, rc.markAttachmentReady(rc.attachmentLease(warm)))
134			require.NoError(t, d.suspendAttachment(sess.captureAttachmentCapability(warm, warmTransport), protocol.SuspendAttachment{RequestID: 1}))
135			if disappeared {
136				sess.mu.Lock()
137				sess.tabs = sess.tabs[:1]
138				sess.mu.Unlock()
139			}
140			require.NoError(t, d.activateAttachment(warm, warm.transportSnapshot(), protocol.ActivateAttachment{RequestID: 2, Target: target, Size: defaultSize}))
141			coldTransport := &closeTrackingTransport{}
142			_, cold, err := d.routeWithContext(context.Background(), hello, coldTransport)
143			require.NoError(t, err)
144			sess.repairAttachmentView(cold)
145			want := domain.TabStableID("tab-2")
146			if disappeared {
147				want = "tab-1"
148			}
149			require.Equal(t, want, cold.viewSnapshot().tabID)
150			require.Equal(t, cold.viewSnapshot().tabID, warm.viewSnapshot().tabID)
151			d.clientGone(sess, warm, warmTransport, false)
152			d.clientGone(sess, cold, coldTransport, false)
153		})
154	}
155}

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