1package daemon
2
3import (
4 "testing"
5
6 vt "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/domain/layout"
9 "github.com/bnema/vev/internal/protocol"
10 "github.com/stretchr/testify/require"
11)
12
13func TestAcceptancePaletteNewTabUsesSessionGeometryAfterAttachmentResize(t *testing.T) {
14 d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY())
15 first.clientID[0] = 1
16 firstTab := sess.tabs[0]
17 firstTab.stableID = "tab-first"
18 firstTab.panes[firstTab.focusedPane().id].stableID = "pane-first"
19
20 secondTransport, _ := newCapturingTransport(t)
21 second := &attachedClient{
22 tr: secondTransport,
23 output: newOutputStateStream(),
24 geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
25 }
26 second.initOverlays()
27 second.clientID[0] = 2
28 second.setSession(sess)
29 secondTab := newTabWithStableID("tab-second", "pane-second", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
30 secondTab.ctx, secondTab.cancel = sess.ctx, func() {}
31 publishTiledPaneOwners(sess, secondTab)
32 require.NoError(t, sess.runMutation(func() error {
33 sess.mu.Lock()
34 defer sess.mu.Unlock()
35 sess.tabs = append(sess.tabs, secondTab)
36 return nil
37 }))
38 require.True(t, sess.registerAttachment(second))
39 require.True(t, sess.selectAttachmentTab(first, domain.TabStableID(firstTab.stableID)))
40 require.True(t, sess.selectAttachmentTab(second, domain.TabStableID(secondTab.stableID)))
41
42 d.ptys = newFactory(t, newQuietPTY())
43 rc := d.attachCoordinator(sess, nil, first, true)
44 d.attachCoordinator(sess, nil, second, true)
45 firstToken := sess.captureAttachmentCapability(first, first.transport())
46 firstToken.lease = rc.attachmentLease(first)
47 first.installTestAttachmentCapability(firstToken)
48 secondToken := sess.captureAttachmentCapability(second, second.transport())
49 secondToken.lease = rc.attachmentLease(second)
50 second.installTestAttachmentCapability(secondToken)
51
52 firstView := first.viewSnapshot()
53 secondView := second.viewSnapshot()
54 first.sendMu.Lock()
55 firstEpoch := first.output.currentEpoch()
56 first.sendMu.Unlock()
57 secondEffect, admitted := second.beginAttachmentEffect(secondToken)
58 require.True(t, admitted)
59 require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40}))
60 secondEffect.End()
61
62 d.enterPalette(sess, second)
63 d.handlePaletteInput(second, []byte("CNT\r"))
64
65 sess.mu.Lock()
66 tabs := append([]*tab(nil), sess.tabs...)
67 sess.mu.Unlock()
68 require.Len(t, tabs, 3)
69 wantContent := domain.Size{Cols: 120, Rows: 38}
70 for _, tb := range tabs {
71 tb.mu.Lock()
72 gotTabSize := tb.size
73 pane := tb.focusedPane()
74 pane.mu.Lock()
75 gotPaneSize := domain.Size{Cols: pane.screen.Columns(), Rows: pane.screen.Rows()}
76 pane.mu.Unlock()
77 tb.mu.Unlock()
78 require.Equal(t, wantContent, gotTabSize)
79 require.Equal(t, wantContent, gotPaneSize)
80 }
81 require.Equal(t, firstView.tabID, first.viewSnapshot().tabID, "new tab selection crossed attachment boundary")
82 require.NotEqual(t, secondView.tabID, second.viewSnapshot().tabID, "palette did not select the created tab")
83 first.sendMu.Lock()
84 gotFirstEpoch := first.output.currentEpoch()
85 first.sendMu.Unlock()
86 require.Equal(t, firstEpoch, gotFirstEpoch, "new tab render crossed attachment boundary")
87 second.sendMu.Lock()
88 gotSecondSize := second.geometry.Size
89 second.sendMu.Unlock()
90 require.Equal(t, domain.Size{Cols: 120, Rows: 40}, gotSecondSize)
91}
92
93func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing.T) {
94 d, sess, first, _ := newManualSessionWithPTYs(t, newQuietPTY())
95 first.clientID[0] = 1
96 firstTab := sess.tabs[0]
97 firstTab.stableID = "tab-first"
98 firstTab.panes[firstTab.focusedPane().id].stableID = "pane-first"
99
100 secondTransport, _ := newCapturingTransport(t)
101 second := &attachedClient{
102 tr: secondTransport,
103 output: newOutputStateStream(),
104 geometry: domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}},
105 }
106 second.initOverlays()
107 second.clientID[0] = 2
108 second.setSession(sess)
109
110 secondTab := newTabWithStableID("tab-second", "pane-second", newQuietPTY(), domain.Size{Cols: 80, Rows: 23})
111 secondTab.ctx, secondTab.cancel = sess.ctx, func() {}
112 publishTiledPaneOwners(sess, secondTab)
113 require.NoError(t, sess.runMutation(func() error {
114 sess.mu.Lock()
115 defer sess.mu.Unlock()
116 sess.tabs = append(sess.tabs, secondTab)
117 return nil
118 }))
119 require.True(t, sess.registerAttachment(second))
120 require.True(t, sess.selectAttachmentTab(first, domain.TabStableID(firstTab.stableID)))
121 require.True(t, sess.selectAttachmentTab(second, domain.TabStableID(secondTab.stableID)))
122
123 firstBefore := first.viewSnapshot()
124 secondBefore := second.viewSnapshot()
125 require.True(t, sess.updateAttachmentView(first, func(view *attachmentView) {
126 view.windowTop = 7
127 view.windowRows = 12
128 view.bookmark = 11
129 view.liveBottom = false
130 }))
131 gotFirst := first.viewSnapshot()
132 gotSecond := second.viewSnapshot()
133 require.Equal(t, domain.TabStableID(firstTab.stableID), gotFirst.tabID)
134 require.Equal(t, 7, gotFirst.windowTop)
135 require.Equal(t, vt.RowID(11), gotFirst.bookmark)
136 require.False(t, gotFirst.liveBottom)
137 require.Equal(t, secondBefore, gotSecond, "first attachment view mutation leaked to peer")
138 require.NotEqual(t, firstBefore.revision, gotFirst.revision)
139
140 rc := d.attachCoordinator(sess, nil, first, true)
141 d.attachCoordinator(sess, nil, second, true)
142 firstToken := sess.captureAttachmentCapability(first, first.transport())
143 firstToken.lease = rc.attachmentLease(first)
144 first.installTestAttachmentCapability(firstToken)
145 secondToken := sess.captureAttachmentCapability(second, second.transport())
146 secondToken.lease = rc.attachmentLease(second)
147 second.installTestAttachmentCapability(secondToken)
148 firstEpoch := first.output.currentEpoch()
149 secondEpoch := second.output.currentEpoch()
150
151 d.handleAttachmentClientFrame(secondToken, mustClientEnvelope(protocol.OutputResetRequest{}))
152 require.Greater(t, second.output.currentEpoch(), secondEpoch)
153 require.Equal(t, firstEpoch, first.output.currentEpoch(), "peer output reset crossed attachment boundary")
154
155 secondEffect, admitted := second.beginAttachmentEffect(secondToken)
156 require.True(t, admitted)
157 require.True(t, d.resizeAttachmentForLease(secondEffect, domain.Size{Cols: 120, Rows: 40}))
158 secondEffect.End()
159 require.Equal(t, domain.Size{Cols: 120, Rows: 40}, second.geometry.Size)
160 require.Equal(t, domain.Size{Cols: 80, Rows: 24}, first.geometry.Size, "peer resize crossed attachment boundary")
161 sess.mu.Lock()
162 tb := sess.tabs[0]
163 sess.mu.Unlock()
164 tb.mu.Lock()
165 sharedSize := tb.size
166 tb.mu.Unlock()
167 require.Equal(t, domain.Size{Cols: 120, Rows: 38}, sharedSize, "latest attachment resize must update shared session content")
168
169 d.clientGone(sess, first, first.transport(), true)
170 attachments := sess.snapshotAttachments()
171 for _, candidate := range attachments {
172 require.NotSame(t, first, candidate, "detached attachment remained registered")
173 }
174 var peerRegistered bool
175 for _, candidate := range attachments {
176 if candidate == second {
177 peerRegistered = true
178 break
179 }
180 }
181 require.True(t, peerRegistered, "detaching one attachment removed its peer")
182 require.Same(t, sess, second.currentSession())
183}
184
185func TestAcceptanceDirectAttachmentSupportsVerticalSplit(t *testing.T) {
186 d, sess, ac, _ := newManualSessionWithPTYs(t, newQuietPTY())
187 d.ptys = newFactory(t, newQuietPTY())
188 rc := d.attachCoordinator(sess, nil, ac, true)
189 token := sess.captureAttachmentCapability(ac, ac.transport())
190 token.lease = rc.attachmentLease(ac)
191 ac.installTestAttachmentCapability(token)
192
193 d.enterPalette(sess, ac)
194 d.handlePaletteInput(ac, []byte("SPD\r"))
195
196 sess.mu.Lock()
197 tab := sess.tabs[0]
198 sess.mu.Unlock()
199 tab.mu.Lock()
200 defer tab.mu.Unlock()
201 require.NotNil(t, tab.tree)
202 require.NotNil(t, tab.tree.Root)
203 require.Equal(t, layout.Vertical, tab.tree.Root.Dir)
204 require.Len(t, tab.panes, 2)
205}