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/ports"
11)
12
13func TestRouteRetainsRemoteOriginWithoutDiscoveryTarget(t *testing.T) {
14 d := newTestDaemon(t, nil, stubClock{})
15 sess := addControlSession(d, "work", "tab-1", "pane-1")
16 sess.incarnation = domain.SessionLifecycleID{1}
17 tr := &closeTrackingTransport{}
18 target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
19 hello := ports.Hello{
20 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
21 Size: defaultSize, ExactTarget: &target, RemoteOrigin: "remote",
22 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
23 }
24
25 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
26 require.NoError(t, err)
27 require.Equal(t, "remote", ac.remoteOrigin)
28 d.clientGone(sess, ac, tr, false)
29}
30
31func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
32 for _, test := range []struct {
33 name string
34 preferred domain.TabStableID
35 want domain.TabStableID
36 }{
37 {name: "existing preferred tab", preferred: "tab-2", want: "tab-2"},
38 {name: "missing preferred tab falls back", preferred: "missing", want: "tab-1"},
39 } {
40 t.Run(test.name, func(t *testing.T) {
41 d := newTestDaemon(t, nil, stubClock{})
42 sess := addControlSession(d, "work", "tab-1", "pane-1")
43 sess.incarnation = domain.SessionLifecycleID{1}
44 second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
45 sess.mu.Lock()
46 sess.tabs = append(sess.tabs, second)
47 sess.mu.Unlock()
48 tr := &closeTrackingTransport{}
49 target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
50 hello := ports.Hello{
51 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
52 Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,
53 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
54 }
55
56 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
57 require.NoError(t, err)
58 sess.repairAttachmentView(ac)
59 require.Equal(t, test.want, ac.viewSnapshot().tabID)
60 d.clientGone(sess, ac, tr, false)
61 })
62 }
63}
64
65func TestResumeAppliesPreferredTabToParkedAttachment(t *testing.T) {
66 pty, release := newBlockingPTY(t)
67 defer release()
68 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
69 oldTransport := &closeTrackingTransport{}
70 sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
71 require.NoError(t, err)
72 second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
73 sess.mu.Lock()
74 sess.tabs = append(sess.tabs, second)
75 sess.mu.Unlock()
76 sess.repairAttachmentView(ac)
77 token := ac.resumeToken
78 d.clientGone(sess, ac, oldTransport, false)
79
80 hello := helloResumeCapable(ports.IntentResume, "work", token)
81 hello.PreferredTabID = "tab-2"
82 newTransport := &closeTrackingTransport{}
83 resumedSession, resumed, err := d.route(hello, newTransport)
84 require.NoError(t, err)
85 require.Same(t, ac, resumed)
86 require.Equal(t, domain.TabStableID("tab-2"), resumed.viewSnapshot().tabID)
87 d.clientGone(resumedSession, resumed, newTransport, false)
88}
89
90func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
91 d := newTestDaemon(t, nil, stubClock{})
92 sess := addControlSession(d, "work", "tab-1", "pane-1")
93 sess.incarnation = domain.SessionLifecycleID{1}
94 second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
95 sess.mu.Lock()
96 sess.tabs = append(sess.tabs, second)
97 sess.mu.Unlock()
98 tr := &closeTrackingTransport{}
99 target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
100 hello := ports.Hello{
101 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
102 Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
103 }
104 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
105 require.NoError(t, err)
106 rc := sess.renderCoordinator()
107 require.True(t, rc.markAttachmentReady(rc.attachmentLease(ac)))
108 sess.repairAttachmentView(ac)
109
110 require.True(t, d.firstPaintForTransition(sess.attachmentToken(ac, tr)))
111 sess.selectAttachmentTab(ac, "tab-2")
112 d.paint(sess, ac, false, nil)
113
114 var positions []ports.RoutePosition
115 for _, frame := range tr.Sends() {
116 if frame.Type != ports.MsgRoutePosition {
117 continue
118 }
119 position, err := ports.UnmarshalRoutePosition(frame.Payload)
120 require.NoError(t, err)
121 positions = append(positions, position)
122 }
123 require.Equal(t, []ports.RoutePosition{
124 {Target: target, ActiveTabID: "tab-1"},
125 {Target: target, ActiveTabID: "tab-2"},
126 }, positions)
127 d.clientGone(sess, ac, tr, false)
128}