1package daemon
2
3import (
4 "testing"
5
6 renderer "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/protocol"
8 scopy "github.com/bnema/vev/internal/usecase/copy"
9 "github.com/stretchr/testify/require"
10)
11
12func TestCopyCacheFailedPublicationAndRetry(t *testing.T) {
13 for _, failure := range []string{"prepare", "send"} {
14 t.Run(failure, func(t *testing.T) {
15 d, sess, ac, sends := newManualSessionWithPTYs(t)
16 healthy := ac.transport()
17 state := cacheState("live", 1)
18 state.floating = capturedFloatingRenderState{}
19 state.overlays = capturedOverlayRenderState{
20 copyActive: true, copyPaneID: "pane",
21 copyMode: scopy.NewMode(scopy.NewDocument(scopy.NewSnapshotFromRows([][]renderer.Cell{testRow("COPY")}, 6, 1), "")),
22 }
23 committed := composeFrame(state, composeCacheInput{})
24 require.True(t, committed.cache.valid)
25 ac.render.cache = committed.cache
26 before := cloneComposeCache(ac.render.cache)
27 state.bars.topRight = "retry"
28 state.attachment = ac
29 state.route.Target = protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
30 state.view.revision = ac.viewSnapshot().revision
31 pending := composeFrame(state, ac.render.cache, ac.render.spare)
32 if failure == "prepare" {
33 pending.frame = renderer.Frame{Width: 1}
34 } else {
35 ac.replaceTransport(cacheFailTransport{})
36 }
37 ac.sendMu.Lock()
38 require.True(t, d.publishCapturedFrameForTest(sess, ac, &state, pending))
39 require.Equal(t, before, cloneComposeCache(ac.render.cache))
40 require.Zero(t, ac.output.next)
41 if failure == "send" {
42 sess.mu.Lock()
43 sess.registerAttachmentLocked(ac)
44 sess.mu.Unlock()
45 ac.setSession(sess)
46 ac.replaceTransport(healthy)
47 }
48 pending = composeFrame(state, ac.render.cache, ac.render.spare)
49 state.view.revision = ac.viewSnapshot().revision
50 ac.sendMu.Lock()
51 require.True(t, d.publishCapturedFrameForTest(sess, ac, &state, pending))
52 require.Equal(t, pending.cache, ac.render.cache)
53 require.NotContains(t, frameText(ac.render.cache.frame), "COPY")
54 output := unmarshalTestOutput(t, (<-sends).Payload)
55 terminal := renderer.NewScreen(pending.frame.Width, pending.frame.Height)
56 terminal.Write(output.Data)
57 require.Contains(t, frameText(captureTestFrame(terminal)), "COPY", "retry must render copy cells despite interleaved ANSI styling")
58 require.Equal(t, uint64(1), ac.output.next)
59 })
60 }
61}
62
63func TestCopyCacheSearchTransition(t *testing.T) {
64 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
65 sess.tabs[0].focusedPane().screen.Write([]byte("alpha"))
66 ack := func() {
67 mustOutputData(t, sends)
68 ac.ackOutputState(ac.output.currentEpoch(), ac.output.next)
69 }
70 d.enterCopyMode(sess, ac)
71 ack()
72 require.True(t, ac.render.cache.valid)
73 base := captureTestFrame(ac.render.cache.frame)
74 d.handleInput(sess, ac, []byte("/"))
75 ack()
76 require.NotNil(t, ac.overlays.copySearch)
77 require.False(t, ac.render.cache.valid)
78 require.Equal(t, base, captureTestFrame(ac.render.cache.frame), "search modal must not contaminate the live base")
79 d.handleInput(sess, ac, []byte("\x1b"))
80 ack()
81 require.Nil(t, ac.overlays.copySearch)
82 require.NotNil(t, ac.overlays.copyMode)
83 require.True(t, ac.render.cache.valid)
84 require.Equal(t, base, captureTestFrame(ac.render.cache.frame))
85}
86
87func TestCopyExitRefreshesAfterOtherAttachmentConsumesDamage(t *testing.T) {
88 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
89 p := sess.tabs[0].focusedPane()
90 p.screen.Write([]byte("before"))
91 d.enterCopyMode(sess, ac)
92 mustOutputData(t, sends)
93 ac.ackOutputState(ac.output.currentEpoch(), ac.output.next)
94 copyBefore := captureTestFrame(ac.render.cache.frame)
95
96 tr, peerSends := newCapturingTransport(t)
97 peer := &attachedClient{tr: tr, output: newOutputStateStream(), size: ac.size}
98 peer.output.attachment = peer
99 peer.initOverlays()
100 peer.setSession(sess)
101 sess.mu.Lock()
102 require.True(t, sess.registerAttachmentLocked(peer))
103 sess.mu.Unlock()
104 p.mu.Lock()
105 p.screen.Write([]byte("\x1b[1;1Hafter-peer"))
106 p.mu.Unlock()
107 // A real peer capture/emission consumes the shared pane damage first.
108 d.paint(sess, peer, true, nil)
109 mustOutputData(t, peerSends)
110 p.mu.Lock()
111 require.Empty(t, p.screen.Damage())
112 p.mu.Unlock()
113 require.Contains(t, frameText(peer.render.cache.frame), "after-peer")
114 require.Equal(t, copyBefore, captureTestFrame(ac.render.cache.frame), "peer publication cannot mutate another attachment's cache")
115
116 // The copy document is still frozen, but leaving it must recapture live
117 // cells even though the other attachment already acknowledged the damage.
118 d.copyWheel(sess, ac, 1)
119 mustOutputData(t, sends)
120 require.Nil(t, ac.overlays.copyMode)
121 require.Contains(t, frameText(ac.render.cache.frame), "after-peer")
122 require.NoError(t, ac.render.cache.frame.CheckInvariants())
123}