1package daemon
2
3import (
4 "errors"
5 "testing"
6
7 renderer "github.com/bnema/vev-vt/ansi"
8 "github.com/bnema/vev/internal/adapters/sessionwire"
9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/protocol"
11 "github.com/stretchr/testify/require"
12)
13
14func publicationTestContext() protocol.ViewContext {
15 return protocol.ViewContext{
16 Route: protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}},
17 TabID: "tab-1", FocusedPaneID: "pane-1",
18 }
19}
20
21func TestPreparedOutputSemanticPublication(t *testing.T) {
22 for _, scenario := range []string{"success", "send failure", "stale", "exhausted"} {
23 t.Run(scenario, func(t *testing.T) {
24 stream := newOutputStateStream()
25 frame := renderer.NewFrame(3, 1)
26 prepared, err := stream.prepare(frame, nil, true)
27 require.NoError(t, err)
28 context := publicationTestContext()
29 prepared.context = &context
30 if scenario == "stale" {
31 stream.rebase()
32 }
33 if scenario == "exhausted" {
34 stream.viewPublication = ^uint64(0)
35 }
36 before := stream.viewPublication
37 called := false
38 sendErr := errors.New("send failed")
39 err = prepared.send(prepared.data, 9, func(output protocol.Output) error {
40 called = true
41 require.NotNil(t, output.Context)
42 require.Equal(t, before+1, output.Context.Publication)
43 require.Equal(t, context.Route, output.Context.Route)
44 require.Equal(t, uint64(9), output.Echo)
45 _, marshalErr := sessionwire.EncodeServerMessage(output)
46 require.NoError(t, marshalErr)
47 if scenario == "send failure" {
48 return sendErr
49 }
50 return nil
51 })
52 switch scenario {
53 case "success":
54 require.NoError(t, err)
55 require.True(t, called)
56 require.True(t, prepared.sent)
57 require.Equal(t, before+1, stream.viewPublication)
58 sideEffect, err := stream.sideEffect([]byte("query"), 9)
59 require.NoError(t, err)
60 require.Nil(t, sideEffect.Context)
61 require.Equal(t, context.Route, stream.lastViewContext.Route)
62 boundary := prepared.boundary
63 require.Equal(t, protocol.UIReceipt{Epoch: stream.epoch, State: stream.next, ViewPublication: stream.viewPublication, Outcome: protocol.UIReceiptProcessed}, boundary)
64 stream.rebase()
65 require.Equal(t, boundary, prepared.boundary, "later view rebases cannot change an accepted receipt boundary")
66 case "send failure":
67 require.ErrorIs(t, err, sendErr)
68 require.True(t, called)
69 require.False(t, prepared.sent)
70 require.Equal(t, before, stream.viewPublication)
71 require.Zero(t, prepared.boundary)
72 case "stale", "exhausted":
73 if scenario == "exhausted" {
74 require.Error(t, err)
75 } else {
76 require.NoError(t, err)
77 }
78 require.False(t, called)
79 require.False(t, prepared.sent)
80 require.Equal(t, before, stream.viewPublication)
81 }
82 })
83 }
84}
85
86func TestPreparedOutputNoBytePublication(t *testing.T) {
87 for _, scenario := range []string{"unchanged", "fence", "focus", "failure", "stale"} {
88 t.Run(scenario, func(t *testing.T) {
89 stream := newOutputStateStream()
90 frame := renderer.NewFrame(3, 1)
91 context := publicationTestContext()
92 first, err := stream.prepare(frame, nil, true)
93 require.NoError(t, err)
94 first.context = &context
95 require.NoError(t, first.send(first.data, 0, func(protocol.Output) error { return nil }))
96 prepared, err := stream.prepare(frame, nil, false)
97 require.NoError(t, err)
98 require.Empty(t, prepared.data)
99 prepared.context = &context
100 if scenario == "focus" {
101 context.FocusedPaneID = "pane-2"
102 }
103 if scenario == "stale" {
104 stream.rebase()
105 }
106 epoch, state, outstanding := stream.epoch, stream.next, stream.outstanding()
107 called := false
108 sendErr := errors.New("metadata send failed")
109 err = prepared.publishNoBytes(scenario == "fence" || scenario == "failure" || scenario == "stale", func(update protocol.UIViewUpdate) error {
110 called = true
111 require.Equal(t, epoch, update.Epoch)
112 require.Equal(t, state, update.State)
113 require.Equal(t, uint64(2), update.Context.Publication)
114 require.Equal(t, context.FocusedPaneID, update.Context.FocusedPaneID)
115 _, marshalErr := sessionwire.EncodeServerMessage(update)
116 require.NoError(t, marshalErr)
117 if scenario == "failure" {
118 return sendErr
119 }
120 return nil
121 })
122 if scenario == "failure" {
123 require.ErrorIs(t, err, sendErr)
124 } else {
125 require.NoError(t, err)
126 }
127 require.Equal(t, scenario != "unchanged" && scenario != "stale", called)
128 require.Equal(t, scenario != "failure" && scenario != "stale", prepared.sent)
129 wantPublication := uint64(1)
130 if scenario == "focus" || scenario == "fence" {
131 wantPublication = 2
132 }
133 require.Equal(t, wantPublication, stream.viewPublication)
134 require.Equal(t, epoch, stream.epoch)
135 require.Equal(t, state, stream.next)
136 require.Equal(t, outstanding, stream.outstanding())
137 if scenario == "focus" || scenario == "fence" {
138 boundary := prepared.boundary
139 require.Equal(t, protocol.UIReceipt{Epoch: epoch, State: state, ViewPublication: wantPublication, Outcome: protocol.UIReceiptProcessed}, boundary)
140 stream.rebase()
141 require.Equal(t, boundary, prepared.boundary)
142 } else {
143 require.Zero(t, prepared.boundary, "only an accepted publication supplies a receipt boundary")
144 }
145 })
146 }
147}
148
149func TestCapturedViewContextUsesCapturedFocus(t *testing.T) {
150 for _, floating := range []bool{false, true} {
151 t.Run(map[bool]string{false: "tiled", true: "floating"}[floating], func(t *testing.T) {
152 context := publicationTestContext()
153 state := capturedRenderState{
154 route: context.Route,
155 view: attachmentView{tabID: context.TabID, paneID: "not-the-rendered-focus"},
156 panes: []capturedPaneRenderState{
157 {stableID: "other-pane"},
158 {stableID: context.FocusedPaneID, focused: true},
159 },
160 floating: capturedFloatingRenderState{visible: floating, focused: floating, pane: capturedPaneRenderState{stableID: "floating-pane"}},
161 }
162 got := state.viewContext()
163 if floating {
164 context.FocusedPaneID = "floating-pane"
165 }
166 require.Equal(t, context, got)
167 state.route.Target.SessionName = "renamed"
168 state.panes[1].stableID = "replacement"
169 require.Equal(t, context, got, "published values do not alias capture storage")
170 })
171 }
172}