render_publication_attempt_test.go RawHistoryFolder
1package daemon
2
3import (
4 "errors"
5 "testing"
6
7 "github.com/stretchr/testify/mock"
8 "github.com/stretchr/testify/require"
9
10 "github.com/bnema/vev/internal/protocol/wire"
11)
12
13// TestPaintPublicationAttempt drives one complete publication attempt through
14// paint (capture, compose, publish, settle) and asserts its observable
15// outcome: what reached the transport, which VT damage was acknowledged,
16// whether the attachment stayed owned, and that settlement never ran under the
17// attachment's send lock.
18func TestPaintPublicationAttempt(t *testing.T) {
19 tests := []struct {
20 name string
21 sendErr error
22 unown bool
23 wantResult paintResult
24 wantSent bool
25 wantDamage bool
26 wantAttached bool
27 }{
28 {name: "success emits and commits damage", wantResult: paintEmitted, wantSent: true, wantAttached: true},
29 {name: "send failure retains damage and detaches", sendErr: errors.New("link lost"), wantResult: paintEmitted, wantDamage: true},
30 {name: "unowned attachment is rejected before capture", unown: true, wantResult: paintRejected, wantDamage: true},
31 }
32 for _, tt := range tests {
33 t.Run(tt.name, func(t *testing.T) {
34 d, sess, ac, sends := newManualSessionWithPTYs(t, newQuietPTY())
35 healthy := ac.transport()
36 d.paint(sess, ac, true, nil)
37 <-sends
38
39 p := sess.tabs[0].focusedPane()
40 p.mu.Lock()
41 p.screen.Write([]byte("\x1b[1;1Hchanged"))
42 p.mu.Unlock()
43
44 tr := newMockServerConnection(t)
45 var sendLockHeld []bool
46 tr.EXPECT().Send(mock.Anything).RunAndReturn(func(f wire.Envelope) error {
47 if tt.sendErr != nil {
48 return tt.sendErr
49 }
50 sends <- f
51 return nil
52 }).Maybe()
53 tr.EXPECT().Close().RunAndReturn(func() error {
54 // Send-error cleanup belongs to settlement, after sendMu is released.
55 locked := !ac.sendMu.TryLock()
56 if !locked {
57 ac.sendMu.Unlock()
58 }
59 sendLockHeld = append(sendLockHeld, locked)
60 return nil
61 }).Maybe()
62 ac.replaceTransport(tr)
63 if tt.unown {
64 sess.mu.Lock()
65 delete(sess.attachments, ac)
66 sess.mu.Unlock()
67 }
68
69 result := d.paint(sess, ac, false, nil)
70 d.attachmentCleanupWg.Wait()
71
72 require.Equal(t, tt.wantResult, result)
73 if tt.sendErr != nil {
74 require.NotEmpty(t, sendLockHeld, "send failure must close the failed link")
75 }
76 require.NotContains(t, sendLockHeld, true, "settlement ran under sendMu")
77 if tt.wantSent {
78 out := unmarshalTestOutput(t, (<-sends).Payload)
79 require.Contains(t, string(out.Data), "changed")
80 } else {
81 select {
82 case f := <-sends:
83 t.Fatalf("unexpected frame %s", envelopeMessageName(t, f.Payload))
84 default:
85 }
86 }
87 p.mu.Lock()
88 require.Equal(t, tt.wantDamage, len(p.screen.Damage()) != 0, "damage is acknowledged only after emission")
89 p.mu.Unlock()
90 sess.mu.Lock()
91 _, attached := sess.attachments[ac]
92 sess.mu.Unlock()
93 require.Equal(t, tt.wantAttached, attached)
94 require.True(t, ac.sendMu.TryLock(), "paint must release sendMu")
95 ac.sendMu.Unlock()
96
97 if tt.wantDamage && !tt.unown {
98 // A retry on a healthy link emits the retained change.
99 sess.mu.Lock()
100 sess.registerAttachmentLocked(ac)
101 sess.mu.Unlock()
102 ac.setSession(sess)
103 ac.replaceTransport(healthy)
104 require.Equal(t, paintEmitted, d.paint(sess, ac, false, nil))
105 out := unmarshalTestOutput(t, (<-sends).Payload)
106 require.Contains(t, string(out.Data), "changed")
107 }
108 })
109 }
110}