1package daemon
2
3import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/require"
8
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"
11 "github.com/bnema/vev/internal/usecase/picker"
12)
13
14func remoteLifecycleForTest() domain.SessionLifecycleID {
15 var id domain.SessionLifecycleID
16 id[0] = 7
17 return id
18}
19
20func TestLocalPickerHandoffCarriesExactLifecycle(t *testing.T) {
21 d := newRemotePickerDaemon(nil)
22 source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source")
23 target, _, _ := addRemoteRefreshPickerOwner(t, d, "target")
24 target.incarnation = remoteLifecycleForTest()
25 token := source.attachmentToken(ac, ac.transport())
26 effect, admitted := ac.beginAttachmentEffect(token)
27 require.True(t, admitted)
28 token.effect = effect
29 defer effect.End()
30
31 require.NoError(t, d.switchToTargetForAttachment(token, picker.Target{Session: target.id}, sessionHandoffGuard{}, "picker-select"))
32 frame := receiveRemotePicker(t, sends, "local attach target")
33 got, err := ports.UnmarshalAttachTarget(frame.Payload)
34 require.NoError(t, err)
35 require.Empty(t, got.Endpoint)
36 require.Equal(t, ports.IntentAttach, got.Intent)
37 require.Equal(t, &ports.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)
38 require.Nil(t, ac.currentAttachmentSession())
39}
40
41func TestRemotePickerRichHandoffCarriesLifecycleTabAndPolicy(t *testing.T) {
42 d := newRemotePickerDaemon(nil)
43 lifecycle := remoteLifecycleForTest()
44 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
45 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
46 LifecycleID: lifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
47 }},
48 }})
49 d.remoteCatalog.mu.Lock()
50 d.remoteCatalog.status["arch"] = remoteHostFresh
51 d.remoteCatalog.mu.Unlock()
52 sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local")
53 token := sess.attachmentToken(ac, ac.transport())
54 effect, admitted := ac.beginAttachmentEffect(token)
55 require.True(t, admitted)
56 token.effect = effect
57 defer effect.End()
58 key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"}
59 remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"}
60 target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"}
61 require.NoError(t, d.sendRemoteAttachTargetForAttachment(token, target, key, sessionHandoffGuard{closePicker: true}, "picker-select"))
62 frame := receiveRemotePicker(t, sends, "rich attach target")
63 got, err := ports.UnmarshalAttachTarget(frame.Payload)
64 require.NoError(t, err)
65 require.NotNil(t, got.RemoteTarget)
66 require.Equal(t, remoteTarget, *got.RemoteTarget)
67 require.Equal(t, ports.EnvironmentPolicyDaemonOwned, got.EnvironmentPolicy)
68}
69
70func TestRemotePickerRichHandoffRejectsReplacedLifecycle(t *testing.T) {
71 d := newRemotePickerDaemon(nil)
72 lifecycle := remoteLifecycleForTest()
73 cachedLifecycle := lifecycle
74 cachedLifecycle[0]++
75 d.remoteCatalog.replaceCache([]ports.RemoteCatalogCacheEntry{{
76 Host: "arch", FetchedAt: time.Unix(10, 0), Sessions: []ports.RemoteCatalogSession{{
77 LifecycleID: cachedLifecycle, Name: "work", State: "up", Tabs: []ports.RemoteCatalogTab{{ID: "tab-1", Index: 0, Name: "main"}},
78 }},
79 }})
80 d.remoteCatalog.mu.Lock()
81 d.remoteCatalog.status["arch"] = remoteHostFresh
82 d.remoteCatalog.mu.Unlock()
83 sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local")
84 token := sess.attachmentToken(ac, ac.transport())
85 effect, admitted := ac.beginAttachmentEffect(token)
86 require.True(t, admitted)
87 token.effect = effect
88 defer effect.End()
89 key := domain.RemoteSessionKey{Host: "arch", Name: "work", LifecycleID: lifecycle, DisplayOrigin: "arch"}
90 remoteTarget := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"}
91 target := picker.Target{Session: key.ID(), RemoteKey: &key, RemoteTarget: &remoteTarget, TabID: "tab-1"}
92 require.ErrorIs(t, d.sendRemoteAttachTargetForAttachment(token, target, key, sessionHandoffGuard{}, "picker-select"), errAttachmentTransition)
93 for {
94 select {
95 case frame := <-sends:
96 require.NotEqual(t, ports.MsgAttachTarget, frame.Type, "rejected handoff must not send an attach target")
97 default:
98 return
99 }
100 }
101}