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}