navigation_attachment_incarnation_test.go RawHistoryFolder
1package daemon
2
3import (
4 "testing"
5
6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/usecase/picker"
8 "github.com/stretchr/testify/require"
9)
10
11func TestNavigationHandoffRejectsReplacedTargetLifecycleAtPublication(t *testing.T) {
12 d, source, ac, _ := newManualSessionWithPTYs(t, nil)
13 target := &session{sessionCore: sessionCore{id: "target", name: "target", incarnation: domain.SessionLifecycleID{1}}, ctx: source.ctx, cancel: func() {}, tabs: []*tab{
14 newTab(nil, domain.Size{Cols: 80, Rows: 23}),
15 newTab(nil, domain.Size{Cols: 80, Rows: 23}),
16 }}
17 d.mu.Lock()
18 d.sessions[target.id] = target
19 d.mu.Unlock()
20
21 rc := d.attachCoordinator(source, nil, ac, true)
22 token := source.captureAttachmentCapability(ac, ac.transport())
23 token.lease = rc.attachmentLease(ac)
24 ac.installTestAttachmentCapability(token)
25 effect, admitted := ac.beginAttachmentEffect(token)
26 require.True(t, admitted)
27
28 d.afterAttachmentTransitionCoordinatorsLocked = func() {
29 // This hook runs with the target session locked, immediately before the
30 // transition's lifecycle fence is validated.
31 target.incarnation = domain.SessionLifecycleID{2}
32 }
33 err := d.switchToTargetForAttachment(effect, picker.Target{
34 Session: target.id,
35 Incarnation: domain.SessionLifecycleID{1},
36 TabID: domain.TabStableID(target.tabs[1].stableID),
37 TabIndex: 1,
38 }, sessionHandoffGuard{}, "test-handoff")
39
40 require.Error(t, err)
41 var userErr *domain.UserError
42 require.ErrorAs(t, err, &userErr)
43 require.Equal(t, domain.NoticeSessionUnavailable, userErr.Code)
44 require.Same(t, source, ac.currentSession())
45 target.mu.Lock()
46 require.Empty(t, target.snapshotAttachmentsLocked())
47 require.Zero(t, testAttachmentTabIndexLocked(target))
48 target.mu.Unlock()
49}
50
51func TestNavigationHandoffDoesNotMutateAfterInitiatorIncarnationChanges(t *testing.T) {
52 tests := []struct {
53 name string
54 rebind func(*testing.T, *Daemon, *session, *attachedClient)
55 }{
56 {
57 name: "role generation and lease replaced on same attachment",
58 rebind: func(t *testing.T, d *Daemon, source *session, ac *attachedClient) {
59 result, err := d.transitionAttachment(attachmentTransitionRequest{
60 source: source, target: source, next: ac,
61
62 expectedTransport: ac.transportSnapshot(), ready: true,
63 })
64 require.NoError(t, err)
65 d.deferAttachmentTransitionCleanups(result)
66 },
67 },
68 {
69 name: "transport incarnation rebound on same attachment",
70 rebind: func(t *testing.T, _ *Daemon, source *session, ac *attachedClient) {
71 ac.replaceTransport(&closeTrackingTransport{})
72 current := source.captureAttachmentCapability(ac, ac.transport())
73 current.lease = source.renderCoordinator().attachmentLease(ac)
74 ac.installTestAttachmentCapability(current)
75 },
76 },
77 }
78
79 for _, tt := range tests {
80 t.Run(tt.name, func(t *testing.T) {
81 d, source, ac, _ := newManualSessionWithPTYs(t, nil)
82 target := &session{sessionCore: sessionCore{id: "target", name: "target", incarnation: domain.SessionLifecycleID{1}}, ctx: source.ctx, cancel: func() {}, tabs: []*tab{
83 newTab(nil, domain.Size{Cols: 80, Rows: 23}),
84 newTab(nil, domain.Size{Cols: 80, Rows: 23}),
85 }}
86 d.mu.Lock()
87 d.sessions[target.id] = target
88 d.mu.Unlock()
89
90 rc := d.attachCoordinator(source, nil, ac, true)
91 token := source.captureAttachmentCapability(ac, ac.transport())
92 token.lease = rc.attachmentLease(ac)
93 ac.installTestAttachmentCapability(token)
94 effect, admitted := ac.beginAttachmentEffect(token)
95 require.True(t, admitted)
96
97 ended := make(chan struct{})
98 release := make(chan struct{})
99 d.afterActionAttachmentEffectEnded = func(action string) {
100 if action == "detach" {
101 close(ended)
102 <-release
103 }
104 }
105 done := make(chan error, 1)
106 go func() {
107 done <- d.switchToTargetForAttachment(effect, picker.Target{Session: target.id, TabIndex: 1}, sessionHandoffGuard{}, "test-handoff")
108 }()
109 awaitTestCompletion(t, ended, "test handoff did not release its role ticket")
110 tt.rebind(t, d, source, ac)
111 close(release)
112 require.NoError(t, awaitTestValue(t, done, "test handoff did not finish"))
113
114 require.Same(t, source, ac.currentSession())
115 target.mu.Lock()
116 require.Empty(t, target.snapshotAttachmentsLocked())
117 require.Zero(t, testAttachmentTabIndexLocked(target))
118 target.mu.Unlock()
119 })
120 }
121}