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