1package daemon
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/protocol"
9 "github.com/stretchr/testify/require"
10)
11
12func TestRouteExactSessionTargetSelectsLifecycle(t *testing.T) {
13 d := newTestDaemon(t, nil, stubClock{})
14 sess := addControlSession(d, "work", "tab-1", "pane-1")
15 sess.ephemeral = false
16 sess.incarnation = newTestLifecycle(t)
17 tr, _ := newCapturingTransport(t)
18 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
19 hello := protocol.Hello{
20 Version: protocol.Version,
21 Intent: protocol.IntentAttach,
22 Name: target.SessionName,
23 Size: domain.Size{Cols: 80, Rows: 24},
24 ExactTarget: &target,
25 }
26
27 routed, ac, err := d.routeWithContext(context.Background(), hello, tr)
28 require.NoError(t, err)
29 require.Same(t, sess, routed)
30 require.Same(t, sess, ac.currentAttachmentSession())
31 d.clientGone(sess, ac, tr, false)
32}
33
34func TestStoppedSessionAttachTargetAcceptsSameLifecycleNowLive(t *testing.T) {
35 d := newTestDaemon(t, nil, stubClock{})
36 sess := addControlSession(d, "work", "tab-1", "pane-1")
37 sess.ephemeral = false
38 sess.incarnation = newTestLifecycle(t)
39 tr, _ := newCapturingTransport(t)
40 target := protocol.SessionAttachTarget{LifecycleID: sess.incarnation, SessionName: sess.name, TabID: domain.TabStableID(sess.tabs[0].stableID), TabIndex: protocol.NoTabIndex, Stopped: true}
41 hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name, Size: domain.Size{Cols: 80, Rows: 24}, SessionTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
42
43 routed, ac, err := d.routeWithContext(context.Background(), hello, tr)
44 require.NoError(t, err)
45 require.Same(t, sess, routed)
46 d.mu.Lock()
47 require.Len(t, d.sessions, 1)
48 d.mu.Unlock()
49 d.clientGone(sess, ac, tr, false)
50}
51
52func TestSessionAttachTargetRejectsLifecycleReplacement(t *testing.T) {
53 d := newTestDaemon(t, nil, stubClock{})
54 sess := addControlSession(d, "work", "tab-1", "pane-1")
55 sess.incarnation = newTestLifecycle(t)
56 stale := sess.incarnation
57 sess.incarnation[0]++
58 tr, _ := newCapturingTransport(t)
59 target := protocol.SessionAttachTarget{LifecycleID: stale, SessionName: sess.name, TabID: domain.TabStableID(sess.tabs[0].stableID), TabIndex: protocol.NoTabIndex}
60 hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name, Size: domain.Size{Cols: 80, Rows: 24}, SessionTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
61
62 _, _, err := d.routeWithContext(context.Background(), hello, tr)
63 var protocolErr *protoErr
64 require.ErrorAs(t, err, &protocolErr)
65 require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
66}
67
68func TestRouteExactSessionTargetRejectsNameMismatch(t *testing.T) {
69 d := newTestDaemon(t, nil, stubClock{})
70 sess := addControlSession(d, "work", "tab-1", "pane-1")
71 sess.ephemeral = false
72 sess.incarnation = newTestLifecycle(t)
73 tr, _ := newCapturingTransport(t)
74 hello := protocol.Hello{
75 Version: protocol.Version,
76 Intent: protocol.IntentAttach,
77 Name: "other",
78 Size: domain.Size{Cols: 80, Rows: 24},
79 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},
80 }
81
82 _, _, err := d.routeWithContext(context.Background(), hello, tr)
83 var protocolErr *protoErr
84 require.ErrorAs(t, err, &protocolErr)
85 require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
86}
87
88func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
89 d := newTestDaemon(t, nil, stubClock{})
90 sess := addControlSession(d, "work", "tab-1", "pane-1")
91 sess.ephemeral = false
92 sess.incarnation = newTestLifecycle(t)
93 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
94 replacement := sess.incarnation
95 replacement[0]++
96 sess.incarnation = replacement
97
98 d.mu.Lock()
99 err := d.validateExactSessionTargetLocked(target)
100 d.mu.Unlock()
101
102 var protocolErr *protoErr
103 require.ErrorAs(t, err, &protocolErr)
104 require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
105}
106
107func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
108 d := newTestDaemon(t, nil, stubClock{})
109 sess := addControlSession(d, "work", "tab-1", "pane-1")
110 sess.ephemeral = false
111 sess.incarnation = newTestLifecycle(t)
112 tr, _ := newCapturingTransport(t)
113 wrong := sess.incarnation
114 wrong[0]++
115 hello := protocol.Hello{
116 Version: protocol.Version,
117 Intent: protocol.IntentAttach,
118 Name: sess.name,
119 Size: domain.Size{Cols: 80, Rows: 24},
120 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},
121 }
122
123 _, _, err := d.routeWithContext(context.Background(), hello, tr)
124 var protocolErr *protoErr
125 require.ErrorAs(t, err, &protocolErr)
126 require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
127}