1// Package replaytest owns the protocol-neutral transport replay fixture and
2// assertions shared by transport adapter tests. The transcript is a typed
3// Output conversation carried as serialized Protobuf envelopes; adapters
4// prove they preserve it byte-for-byte. The transcript stays value-only:
5// envelopes are composed from generated wire types here, never through a
6// concrete adapter.
7package replaytest
8
9import (
10 "bytes"
11 "errors"
12 "fmt"
13 "reflect"
14 "testing"
15
16 "google.golang.org/protobuf/proto"
17
18 "github.com/bnema/vev/internal/domain"
19 "github.com/bnema/vev/internal/protocol"
20 "github.com/bnema/vev/internal/protocol/wire"
21)
22
23func mustEncodeOutput(m protocol.Output) []byte {
24 m.Context = &protocol.ViewContext{
25 Publication: m.New,
26 Route: protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "replay"}},
27 TabID: "tab-1", FocusedPaneID: "pane-1",
28 }
29 if err := protocol.ValidateOutput(m); err != nil {
30 panic(err)
31 }
32 envelope := &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_Output{Output: &wire.Output{
33 Epoch: m.Epoch,
34 Base: m.Base,
35 NewState: m.New,
36 Echo: m.Echo,
37 ViewRevision: m.ViewRevision,
38 Cols: uint32(m.Size.Cols),
39 Rows: uint32(m.Size.Rows),
40 Full: m.Full,
41 Context: &wire.ViewContext{Publication: m.Context.Publication, Route: &wire.CommittedRouteIdentity{
42 Target: &wire.ExactTarget{
43 LifecycleId: &wire.LifecycleID{Value: append([]byte(nil), m.Context.Route.Target.LifecycleID[:]...)},
44 SessionName: m.Context.Route.Target.SessionName,
45 },
46 Ephemeral: m.Context.Route.Ephemeral,
47 }, TabId: string(m.Context.TabID), FocusedPaneId: string(m.Context.FocusedPaneID)},
48 Encoding: 0,
49 UncompressedLength: uint64(len(m.Data)),
50 Data: append([]byte(nil), m.Data...),
51 }}}
52 raw, err := proto.MarshalOptions{Deterministic: true}.Marshal(envelope)
53 if err != nil {
54 panic(err)
55 }
56 return raw
57}
58
59var transcript = []wire.Envelope{
60 {Payload: mustEncodeOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("\x1b[2J\x1b[Hone\r\ntwo")})},
61 {Payload: mustEncodeOutput(protocol.Output{Epoch: 1, Base: 1, New: 2, Echo: 7, Size: domain.Size{Cols: 80, Rows: 24}, Data: []byte("\x1b[2;1HTWO")})},
62}
63
64// Transcript returns a deep copy of the canonical replay transcript so an
65// adapter cannot mutate the fixture observed by another adapter.
66func Transcript() []wire.Envelope {
67 return cloneEnvelopes(transcript)
68}
69
70// Exchange carries already-composed envelopes through one transport adapter.
71type Exchange func(t *testing.T, envelopes []wire.Envelope) []wire.Envelope
72
73// Run verifies that an adapter preserves the canonical transcript exactly.
74func Run(t *testing.T, exchange Exchange) {
75 t.Helper()
76 want := Transcript()
77 got := exchange(t, cloneEnvelopes(want))
78 if len(got) != len(want) {
79 t.Fatalf("replayed envelope count = %d, want %d", len(got), len(want))
80 }
81 for i := range want {
82 if !bytes.Equal(got[i].Payload, want[i].Payload) {
83 t.Errorf("envelope %d payload differs in %d bytes, want byte-for-byte equality", i, len(want[i].Payload))
84 continue
85 }
86 gotOutput, err := decodeOutput(got[i].Payload)
87 if err != nil {
88 t.Errorf("envelope %d output decode: %v", i, err)
89 continue
90 }
91 wantOutput, err := decodeOutput(want[i].Payload)
92 if err != nil {
93 t.Fatalf("canonical envelope %d output decode: %v", i, err)
94 }
95 if !reflect.DeepEqual(gotOutput, wantOutput) {
96 t.Errorf("envelope %d output = %#v, want %#v", i, gotOutput, wantOutput)
97 }
98 }
99}
100
101func decodeOutput(payload []byte) (protocol.Output, error) {
102 envelope := &wire.ServerEnvelope{}
103 if err := wire.ScanEnvelope(envelope, payload); err != nil {
104 return protocol.Output{}, err
105 }
106 if err := (proto.UnmarshalOptions{DiscardUnknown: false}.Unmarshal(payload, envelope)); err != nil {
107 return protocol.Output{}, err
108 }
109 output, ok := envelope.Payload.(*wire.ServerEnvelope_Output)
110 if !ok || output.Output == nil {
111 return protocol.Output{}, &protocol.DecodeFailure{Category: protocol.DecodeWrongDirection, Err: errors.New("replaytest: expected Output envelope")}
112 }
113 decoded := protocol.Output{
114 Epoch: output.Output.GetEpoch(), Base: output.Output.GetBase(), New: output.Output.GetNewState(),
115 Echo: output.Output.GetEcho(), ViewRevision: output.Output.GetViewRevision(),
116 Size: domain.Size{Cols: int(output.Output.GetCols()), Rows: int(output.Output.GetRows())},
117 Full: output.Output.GetFull(), Data: append([]byte(nil), output.Output.GetData()...),
118 }
119 if context := output.Output.GetContext(); context != nil {
120 route, err := committedRouteFromWire(context.GetRoute())
121 if err != nil {
122 return protocol.Output{}, fmt.Errorf("replaytest: invalid route: %w", err)
123 }
124 decoded.Context = &protocol.ViewContext{
125 Publication: context.GetPublication(),
126 Route: route,
127 TabID: domain.TabStableID(context.GetTabId()),
128 FocusedPaneID: domain.PaneStableID(context.GetFocusedPaneId()),
129 }
130 }
131 if err := protocol.ValidateOutput(decoded); err != nil {
132 return protocol.Output{}, fmt.Errorf("replaytest: invalid output: %w", err)
133 }
134 return decoded, nil
135}
136
137func cloneEnvelopes(envelopes []wire.Envelope) []wire.Envelope {
138 cloned := make([]wire.Envelope, len(envelopes))
139 for i, envelope := range envelopes {
140 cloned[i] = wire.Envelope{Payload: append([]byte(nil), envelope.Payload...)}
141 }
142 return cloned
143}