1package wire
2
3import (
4 "strings"
5 "testing"
6
7 "github.com/bnema/vev/internal/protocol"
8 "google.golang.org/protobuf/proto"
9 "google.golang.org/protobuf/reflect/protoreflect"
10)
11
12// TestEnvelopeInventoryProvesDirectionAndSemanticPath is the P2.1 schema
13// inventory: every ClientEnvelope variant names a protocol.ClientMessage,
14// every ServerEnvelope variant names a protocol.ServerMessage, neither side
15// shares a payload type except route navigation failure, and the preamble
16// constants agree with the schema authority.
17func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
18 if protocol.Version != 65 {
19 t.Fatalf("protocol.Version = %d, want 65", protocol.Version)
20 }
21 if ProtocolEpoch != 1 {
22 t.Fatalf("ProtocolEpoch = %d, want 1", ProtocolEpoch)
23 }
24 if PreambleMagic != 0x56455631 {
25 t.Fatalf("PreambleMagic = %#x, want VEV1", PreambleMagic)
26 }
27
28 clientVariants := envelopeVariants(t, (&ClientEnvelope{}).ProtoReflect())
29 serverVariants := envelopeVariants(t, (&ServerEnvelope{}).ProtoReflect())
30
31 // Closed semantic unions, in wire-variant spelling.
32 clientSemantic := map[string]bool{
33 "suspend_attachment": true, "activate_attachment": true,
34 "hello": true, "input": true, "resize": true, "detach": true, "ping": true,
35 "list": true, "kill": true, "theme": true, "ack": true, "image_push": true,
36 "client_notice": true, "command_request": true, "output_reset_request": true,
37 "remote_preview_watch": true, "route_attention_subscription": true,
38 "same_peer_switch_request": true, "recent_route_snapshot": true,
39 "route_navigation_failure": true, "session_creation_failure": true,
40 "ui_fence": true, "navigation_inventory_request": true,
41 "navigation_inventory_publication": true, "navigation_inventory_failure": true,
42 "picker_close": true, "picker_selection": true,
43 "select_tab": true, "terminal_focus": true,
44 }
45 serverSemantic := map[string]bool{
46 "attachment_suspended": true, "attachment_activated": true,
47 "welcome": true, "error": true, "output": true, "detached": true, "pong": true,
48 "sessions": true, "command_result": true, "attach_target": true,
49 "remote_preview": true, "committed_route_identity": true,
50 "route_navigation_action": true, "route_create_session_action": true,
51 "route_navigation_failure": true, "route_position": true, "route_retired": true,
52 "same_peer_switch_failure": true, "ui_receipt": true, "ui_view_update": true,
53 "navigation_inventory_response": true, "navigation_inventory_demand": true,
54 "navigation_inventory_selection": true,
55 "picker_offer": true, "picker_snapshot": true, "picker_closed": true,
56 "picker_result": true, "picker_failure": true,
57 "kill_result": true,
58 }
59
60 if len(clientVariants) != len(clientSemantic) {
61 t.Fatalf("client envelope has %d variants, want %d (%v)", len(clientVariants), len(clientSemantic), clientVariants)
62 }
63 for _, name := range clientVariants {
64 if !clientSemantic[name] {
65 t.Fatalf("client variant %q has no semantic validation path", name)
66 }
67 }
68 if len(serverVariants) != len(serverSemantic) {
69 t.Fatalf("server envelope has %d variants, want %d (%v)", len(serverVariants), len(serverSemantic), serverVariants)
70 }
71 for _, name := range serverVariants {
72 if !serverSemantic[name] {
73 t.Fatalf("server variant %q has no semantic validation path", name)
74 }
75 }
76
77 // Direction closure: exactly one shared payload may appear on both sides.
78 clientSet := map[string]bool{}
79 for _, name := range clientVariants {
80 clientSet[name] = true
81 }
82 shared := []string{}
83 for _, name := range serverVariants {
84 if clientSet[name] {
85 shared = append(shared, name)
86 }
87 }
88 if len(shared) != 1 || shared[0] != "route_navigation_failure" {
89 t.Fatalf("shared client/server payloads = %v, want [route_navigation_failure]", shared)
90 }
91
92 // Every variant must be a distinct generated message type (no generic
93 // escape hatch: no Any, no shared catch-all payload).
94 // Every variant within one envelope must be a distinct generated message
95 // type (no generic escape hatch: no Any, no catch-all). The one shared
96 // payload (route navigation failure) is allowed across envelopes only;
97 // direction is enforced by which envelope wraps it.
98 for _, envelope := range []proto.Message{&ClientEnvelope{}, &ServerEnvelope{}} {
99 seen := map[string]string{}
100 descriptor := envelope.ProtoReflect().Descriptor()
101 if descriptor.Oneofs().Len() != 1 {
102 t.Fatalf("envelope has %d oneofs, want exactly the payload oneof", descriptor.Oneofs().Len())
103 }
104 oneof := descriptor.Oneofs().Get(0)
105 if string(oneof.Name()) != "payload" {
106 t.Fatalf("envelope oneof %q is not the payload oneof", oneof.Name())
107 }
108 for j := range oneof.Fields().Len() {
109 variant := oneof.Fields().Get(j)
110 message := variant.Message()
111 if message == nil {
112 t.Fatalf("variant %q is not a message", variant.Name())
113 }
114 key := string(message.FullName())
115 if prev, dup := seen[key]; dup {
116 t.Fatalf("payload type %v shared by %q and %q", key, prev, variant.Name())
117 }
118 seen[key] = string(variant.Name())
119 if variant.Number() == 0 {
120 t.Fatalf("variant %q has no field number", variant.Name())
121 }
122 }
123 }
124}
125
126func envelopeVariants(t *testing.T, message protoreflect.Message) []string {
127 t.Helper()
128 descriptor := message.Descriptor()
129 if descriptor.Oneofs().Len() != 1 {
130 t.Fatalf("envelope has %d oneofs, want exactly the payload oneof", descriptor.Oneofs().Len())
131 }
132 oneof := descriptor.Oneofs().Get(0)
133 if string(oneof.Name()) != "payload" {
134 t.Fatalf("envelope oneof %q is not the payload oneof", oneof.Name())
135 }
136 var names []string
137 for i := range oneof.Fields().Len() {
138 names = append(names, strings.ToLower(string(oneof.Fields().Get(i).Name())))
139 }
140 return names
141}