detached 1a097f5

feat(wire): add broker preview messages and trim daemon picker schema

commit
1a097f52ea6a8d5b8865bb6aa8f77b9194fafc56
author
brice · · 2026-09-22 19:23 UTC
parent
b2ffe45
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
27 files · +1616 -1355
M internal/adapters/brokerwire/codec.go +39 -0
@@ -237,6 +237,28 @@ func encodeClientEnvelope(message ClientMessage, maxChunkBytes uint64) (*wire.Br
237237 return nil, ErrInvalidMessage
238238 }
239239 return encodeClientEnvelope(*m, maxChunkBytes)
240+ case StartPreview:
241+ converted, err := startPreviewToWire(m)
242+ if err != nil {
243+ return nil, err
244+ }
245+ return &wire.BrokerClientEnvelope{Payload: &wire.BrokerClientEnvelope_StartPreview{StartPreview: converted}}, nil
246+ case *StartPreview:
247+ if m == nil {
248+ return nil, ErrInvalidMessage
249+ }
250+ return encodeClientEnvelope(*m, maxChunkBytes)
251+ case CancelPreview:
252+ converted, err := cancelPreviewToWire(m)
253+ if err != nil {
254+ return nil, err
255+ }
256+ return &wire.BrokerClientEnvelope{Payload: &wire.BrokerClientEnvelope_CancelPreview{CancelPreview: converted}}, nil
257+ case *CancelPreview:
258+ if m == nil {
259+ return nil, ErrInvalidMessage
260+ }
261+ return encodeClientEnvelope(*m, maxChunkBytes)
240262 default:
241263 return nil, ErrWrongDirection
242264 }
@@ -279,6 +301,10 @@ func decodeClientEnvelope(envelope *wire.BrokerClientEnvelope, maxChunkBytes uin
279301 return nil, ErrInvalidMessage
280302 }
281303 return CloseStream{Epoch: epoch, Connection: connection, Stream: stream}, nil
304+ case *wire.BrokerClientEnvelope_StartPreview:
305+ return startPreviewFromWire(payload.StartPreview)
306+ case *wire.BrokerClientEnvelope_CancelPreview:
307+ return cancelPreviewFromWire(payload.CancelPreview)
282308 default:
283309 return nil, ErrWrongDirection
284310 }
@@ -867,6 +893,17 @@ func encodeServerEnvelope(message ServerMessage, maxChunkBytes uint64) (*wire.Br
867893 return nil, ErrInvalidMessage
868894 }
869895 return encodeServerEnvelope(*m, maxChunkBytes)
896+ case PreviewPublication:
897+ converted, err := previewPublicationToWire(m)
898+ if err != nil {
899+ return nil, err
900+ }
901+ return &wire.BrokerServerEnvelope{Payload: &wire.BrokerServerEnvelope_PreviewPublication{PreviewPublication: converted}}, nil
902+ case *PreviewPublication:
903+ if m == nil {
904+ return nil, ErrInvalidMessage
905+ }
906+ return encodeServerEnvelope(*m, maxChunkBytes)
870907 default:
871908 return nil, ErrWrongDirection
872909 }
@@ -920,6 +957,8 @@ func decodeServerEnvelope(envelope *wire.BrokerServerEnvelope, maxChunkBytes uin
920957 return BrokerErrorMessage{Epoch: epoch, Connection: connection, Error: detail}, nil
921958 case *wire.BrokerServerEnvelope_Shutdown:
922959 return shutdownFromWire(payload.Shutdown)
960+ case *wire.BrokerServerEnvelope_PreviewPublication:
961+ return previewPublicationFromWire(payload.PreviewPublication)
923962 default:
924963 return nil, ErrWrongDirection
925964 }
@@ -237,6 +237,28 @@ func encodeClientEnvelope(message ClientMessage, maxChunkBytes uint64) (*wire.Br
237 return nil, ErrInvalidMessage237 return nil, ErrInvalidMessage
238 }238 }
239 return encodeClientEnvelope(*m, maxChunkBytes)239 return encodeClientEnvelope(*m, maxChunkBytes)
240+ case StartPreview:
241+ converted, err := startPreviewToWire(m)
242+ if err != nil {
243+ return nil, err
244+ }
245+ return &wire.BrokerClientEnvelope{Payload: &wire.BrokerClientEnvelope_StartPreview{StartPreview: converted}}, nil
246+ case *StartPreview:
247+ if m == nil {
248+ return nil, ErrInvalidMessage
249+ }
250+ return encodeClientEnvelope(*m, maxChunkBytes)
251+ case CancelPreview:
252+ converted, err := cancelPreviewToWire(m)
253+ if err != nil {
254+ return nil, err
255+ }
256+ return &wire.BrokerClientEnvelope{Payload: &wire.BrokerClientEnvelope_CancelPreview{CancelPreview: converted}}, nil
257+ case *CancelPreview:
258+ if m == nil {
259+ return nil, ErrInvalidMessage
260+ }
261+ return encodeClientEnvelope(*m, maxChunkBytes)
240 default:262 default:
241 return nil, ErrWrongDirection263 return nil, ErrWrongDirection
242 }264 }
@@ -279,6 +301,10 @@ func decodeClientEnvelope(envelope *wire.BrokerClientEnvelope, maxChunkBytes uin
279 return nil, ErrInvalidMessage301 return nil, ErrInvalidMessage
280 }302 }
281 return CloseStream{Epoch: epoch, Connection: connection, Stream: stream}, nil303 return CloseStream{Epoch: epoch, Connection: connection, Stream: stream}, nil
304+ case *wire.BrokerClientEnvelope_StartPreview:
305+ return startPreviewFromWire(payload.StartPreview)
306+ case *wire.BrokerClientEnvelope_CancelPreview:
307+ return cancelPreviewFromWire(payload.CancelPreview)
282 default:308 default:
283 return nil, ErrWrongDirection309 return nil, ErrWrongDirection
284 }310 }
@@ -867,6 +893,17 @@ func encodeServerEnvelope(message ServerMessage, maxChunkBytes uint64) (*wire.Br
867 return nil, ErrInvalidMessage893 return nil, ErrInvalidMessage
868 }894 }
869 return encodeServerEnvelope(*m, maxChunkBytes)895 return encodeServerEnvelope(*m, maxChunkBytes)
896+ case PreviewPublication:
897+ converted, err := previewPublicationToWire(m)
898+ if err != nil {
899+ return nil, err
900+ }
901+ return &wire.BrokerServerEnvelope{Payload: &wire.BrokerServerEnvelope_PreviewPublication{PreviewPublication: converted}}, nil
902+ case *PreviewPublication:
903+ if m == nil {
904+ return nil, ErrInvalidMessage
905+ }
906+ return encodeServerEnvelope(*m, maxChunkBytes)
870 default:907 default:
871 return nil, ErrWrongDirection908 return nil, ErrWrongDirection
872 }909 }
@@ -920,6 +957,8 @@ func decodeServerEnvelope(envelope *wire.BrokerServerEnvelope, maxChunkBytes uin
920 return BrokerErrorMessage{Epoch: epoch, Connection: connection, Error: detail}, nil957 return BrokerErrorMessage{Epoch: epoch, Connection: connection, Error: detail}, nil
921 case *wire.BrokerServerEnvelope_Shutdown:958 case *wire.BrokerServerEnvelope_Shutdown:
922 return shutdownFromWire(payload.Shutdown)959 return shutdownFromWire(payload.Shutdown)
960+ case *wire.BrokerServerEnvelope_PreviewPublication:
961+ return previewPublicationFromWire(payload.PreviewPublication)
923 default:962 default:
924 return nil, ErrWrongDirection963 return nil, ErrWrongDirection
925 }964 }
M internal/adapters/brokerwire/codec_test.go +5 -1
@@ -358,8 +358,10 @@ func TestBrokerClientVariants(t *testing.T) {
358358 {"client_stream_data", ClientStreamData{Epoch: 7, Connection: connection, Stream: 3, Data: []byte("frame")}, 109},
359359 {"close_stream", CloseStream{Epoch: 7, Connection: connection, Stream: 3}, 110},
360360 {"update_host_policy", UpdateHostPolicy{Epoch: 7, Connection: connection, Operation: operation, Registration: testRegistration(), Policy: testPolicy()}, 111},
361+ {"start_preview", testStartPreview(connection), 112},
362+ {"cancel_preview", CancelPreview{Epoch: 7, Connection: connection, Generation: 3}, 113},
361363 }
362- require.Len(t, messages, 11)
364+ require.Len(t, messages, 13)
363365 for _, tc := range messages {
364366 t.Run(tc.name, func(t *testing.T) {
365367 raw := mustEncodeClient(t, tc.message)
@@ -602,6 +604,8 @@ func TestBrokerServerVariants(t *testing.T) {
602604 {"progress", Progress{Epoch: 7, Connection: connection, Stream: 3, Phase: BrokerProgressProbing, Text: "probing"}, 207},
603605 {"broker_error", BrokerErrorMessage{Epoch: 7, Connection: connection, Error: testErrorDetail()}, 208},
604606 {"shutdown", Shutdown{Epoch: 7, Connection: connection, Reason: ShutdownTerminating, Text: "stopping"}, 209},
607+ {"preview_publication", testPreviewPublication(connection), 210},
608+ {"preview_publication_error", PreviewPublication{Epoch: 7, Connection: connection, Generation: 5, Error: testErrorDetail(), HasError: true}, 210},
605609 }
606610 for _, tc := range messages {
607611 t.Run(tc.name, func(t *testing.T) {
@@ -358,8 +358,10 @@ func TestBrokerClientVariants(t *testing.T) {
358 {"client_stream_data", ClientStreamData{Epoch: 7, Connection: connection, Stream: 3, Data: []byte("frame")}, 109},358 {"client_stream_data", ClientStreamData{Epoch: 7, Connection: connection, Stream: 3, Data: []byte("frame")}, 109},
359 {"close_stream", CloseStream{Epoch: 7, Connection: connection, Stream: 3}, 110},359 {"close_stream", CloseStream{Epoch: 7, Connection: connection, Stream: 3}, 110},
360 {"update_host_policy", UpdateHostPolicy{Epoch: 7, Connection: connection, Operation: operation, Registration: testRegistration(), Policy: testPolicy()}, 111},360 {"update_host_policy", UpdateHostPolicy{Epoch: 7, Connection: connection, Operation: operation, Registration: testRegistration(), Policy: testPolicy()}, 111},
361+ {"start_preview", testStartPreview(connection), 112},
362+ {"cancel_preview", CancelPreview{Epoch: 7, Connection: connection, Generation: 3}, 113},
361 }363 }
362- require.Len(t, messages, 11)364+ require.Len(t, messages, 13)
363 for _, tc := range messages {365 for _, tc := range messages {
364 t.Run(tc.name, func(t *testing.T) {366 t.Run(tc.name, func(t *testing.T) {
365 raw := mustEncodeClient(t, tc.message)367 raw := mustEncodeClient(t, tc.message)
@@ -602,6 +604,8 @@ func TestBrokerServerVariants(t *testing.T) {
602 {"progress", Progress{Epoch: 7, Connection: connection, Stream: 3, Phase: BrokerProgressProbing, Text: "probing"}, 207},604 {"progress", Progress{Epoch: 7, Connection: connection, Stream: 3, Phase: BrokerProgressProbing, Text: "probing"}, 207},
603 {"broker_error", BrokerErrorMessage{Epoch: 7, Connection: connection, Error: testErrorDetail()}, 208},605 {"broker_error", BrokerErrorMessage{Epoch: 7, Connection: connection, Error: testErrorDetail()}, 208},
604 {"shutdown", Shutdown{Epoch: 7, Connection: connection, Reason: ShutdownTerminating, Text: "stopping"}, 209},606 {"shutdown", Shutdown{Epoch: 7, Connection: connection, Reason: ShutdownTerminating, Text: "stopping"}, 209},
607+ {"preview_publication", testPreviewPublication(connection), 210},
608+ {"preview_publication_error", PreviewPublication{Epoch: 7, Connection: connection, Generation: 5, Error: testErrorDetail(), HasError: true}, 210},
605 }609 }
606 for _, tc := range messages {610 for _, tc := range messages {
607 t.Run(tc.name, func(t *testing.T) {611 t.Run(tc.name, func(t *testing.T) {
M internal/adapters/brokerwire/doc.go +1 -1
@@ -1,7 +1,7 @@
11 // Package brokerwire adapts typed broker messages to Protobuf envelopes.
22 //
33 // The broker is a separate conversation from the session protocol: client
4-// tags 101-111 and server tags 201-209 are disjoint from every session tag,
4+// tags 101-113 and server tags 201-210 are disjoint from every session tag,
55 // and each direction is its own closed oneof union. Every connection starts
66 // with the bounded broker preamble (roles 3/4); magic, epoch, exact
77 // protocol version, and negotiated limits are validated before application
@@ -1,7 +1,7 @@
1 // Package brokerwire adapts typed broker messages to Protobuf envelopes.1 // Package brokerwire adapts typed broker messages to Protobuf envelopes.
2 //2 //
3 // The broker is a separate conversation from the session protocol: client3 // The broker is a separate conversation from the session protocol: client
4-// tags 101-111 and server tags 201-209 are disjoint from every session tag,4+// tags 101-113 and server tags 201-210 are disjoint from every session tag,
5 // and each direction is its own closed oneof union. Every connection starts5 // and each direction is its own closed oneof union. Every connection starts
6 // with the bounded broker preamble (roles 3/4); magic, epoch, exact6 // with the bounded broker preamble (roles 3/4); magic, epoch, exact
7 // protocol version, and negotiated limits are validated before application7 // protocol version, and negotiated limits are validated before application
M internal/adapters/brokerwire/messages.go +40 -0
@@ -162,6 +162,30 @@ type CloseStream struct {
162162
163163 func (CloseStream) brokerClientMessage() {}
164164
165+// StartPreview starts or replaces one connection-scoped live preview.
166+// Generation is the connection-scoped preview authority; Route is the exact
167+// observation stream authority and Preview is the bounded viewport request
168+// sent to the daemon. Route/request dimensions are retained by client
169+// subscription state.
170+type StartPreview struct {
171+ Epoch ports.BrokerEpoch
172+ Connection ports.BrokerConnectionID
173+ Generation ports.BrokerPreviewGeneration
174+ Route OpenStream
175+ Preview protocol.RemotePreviewRequest
176+}
177+
178+func (StartPreview) brokerClientMessage() {}
179+
180+// CancelPreview cancels one connection-scoped live preview.
181+type CancelPreview struct {
182+ Epoch ports.BrokerEpoch
183+ Connection ports.BrokerConnectionID
184+ Generation ports.BrokerPreviewGeneration
185+}
186+
187+func (CancelPreview) brokerClientMessage() {}
188+
165189 // Registered confirms one accepted client connection and its assigned scope.
166190 type Registered struct {
167191 Epoch ports.BrokerEpoch
@@ -452,6 +476,22 @@ type BrokerErrorMessage struct {
452476
453477 func (BrokerErrorMessage) brokerServerMessage() {}
454478
479+// PreviewPublication is the newest result for one preview subscription.
480+// Every authority field is repeated so a client can reject a late
481+// completion. Exactly one of preview/error travels via the wire result
482+// oneof: a failed publication carries HasError with a typed ErrorDetail
483+// and no Preview; a successful one carries Preview and no error.
484+type PreviewPublication struct {
485+ Epoch ports.BrokerEpoch
486+ Connection ports.BrokerConnectionID
487+ Generation ports.BrokerPreviewGeneration
488+ Preview protocol.RemotePreview
489+ Error ErrorDetail
490+ HasError bool
491+}
492+
493+func (PreviewPublication) brokerServerMessage() {}
494+
455495 // MaxBrokerAdmissionCode is the highest admission-refusal code carried on
456496 // ErrorDetail. The taxonomy is closed: 0 is none, 1 limit, 2 closed, 3 stale
457497 // connection or stream, 4 invalid request (mirroring
@@ -162,6 +162,30 @@ type CloseStream struct {
162 162
163 func (CloseStream) brokerClientMessage() {}163 func (CloseStream) brokerClientMessage() {}
164 164
165+// StartPreview starts or replaces one connection-scoped live preview.
166+// Generation is the connection-scoped preview authority; Route is the exact
167+// observation stream authority and Preview is the bounded viewport request
168+// sent to the daemon. Route/request dimensions are retained by client
169+// subscription state.
170+type StartPreview struct {
171+ Epoch ports.BrokerEpoch
172+ Connection ports.BrokerConnectionID
173+ Generation ports.BrokerPreviewGeneration
174+ Route OpenStream
175+ Preview protocol.RemotePreviewRequest
176+}
177+
178+func (StartPreview) brokerClientMessage() {}
179+
180+// CancelPreview cancels one connection-scoped live preview.
181+type CancelPreview struct {
182+ Epoch ports.BrokerEpoch
183+ Connection ports.BrokerConnectionID
184+ Generation ports.BrokerPreviewGeneration
185+}
186+
187+func (CancelPreview) brokerClientMessage() {}
188+
165 // Registered confirms one accepted client connection and its assigned scope.189 // Registered confirms one accepted client connection and its assigned scope.
166 type Registered struct {190 type Registered struct {
167 Epoch ports.BrokerEpoch191 Epoch ports.BrokerEpoch
@@ -452,6 +476,22 @@ type BrokerErrorMessage struct {
452 476
453 func (BrokerErrorMessage) brokerServerMessage() {}477 func (BrokerErrorMessage) brokerServerMessage() {}
454 478
479+// PreviewPublication is the newest result for one preview subscription.
480+// Every authority field is repeated so a client can reject a late
481+// completion. Exactly one of preview/error travels via the wire result
482+// oneof: a failed publication carries HasError with a typed ErrorDetail
483+// and no Preview; a successful one carries Preview and no error.
484+type PreviewPublication struct {
485+ Epoch ports.BrokerEpoch
486+ Connection ports.BrokerConnectionID
487+ Generation ports.BrokerPreviewGeneration
488+ Preview protocol.RemotePreview
489+ Error ErrorDetail
490+ HasError bool
491+}
492+
493+func (PreviewPublication) brokerServerMessage() {}
494+
455 // MaxBrokerAdmissionCode is the highest admission-refusal code carried on495 // MaxBrokerAdmissionCode is the highest admission-refusal code carried on
456 // ErrorDetail. The taxonomy is closed: 0 is none, 1 limit, 2 closed, 3 stale496 // ErrorDetail. The taxonomy is closed: 0 is none, 1 limit, 2 closed, 3 stale
457 // connection or stream, 4 invalid request (mirroring497 // connection or stream, 4 invalid request (mirroring
A internal/adapters/brokerwire/preview.go +588 -0
@@ -0,0 +1,588 @@
1+package brokerwire
2+
3+// Preview wire converters (broker preview slice 1).
4+//
5+// StartPreview/CancelPreview travel client-to-server; PreviewPublication
6+// travels server-to-client with exactly one of its preview/error result
7+// members set. Conversion mirrors the port preview types losslessly and
8+// validates every narrowing numeric cast before it happens, following the
9+// shared converter patterns in convert.go: wire uint32 taxonomy fields
10+// never truncate into a smaller semantic enum, identity lengths are exact,
11+// and semantic validation failures map to ErrInvalidMessage.
12+//
13+// The preview viewport (RemotePreviewRequest/RemotePreview) reuses the
14+// terminal wire shapes owned by the session conversation; the broker
15+// conversation carries them as nested messages under its own envelope
16+// tags. Repeated authority is scope/generation plus the preview target
17+// identity response; route/request dimensions are retained by client
18+// subscription state and re-checked here through the ports preview
19+// contract.
20+
21+import (
22+ "errors"
23+ "math"
24+
25+ renderer "github.com/bnema/vev-vt"
26+ "github.com/bnema/vev/internal/domain"
27+ "github.com/bnema/vev/internal/ports"
28+ "github.com/bnema/vev/internal/protocol"
29+ "github.com/bnema/vev/internal/protocol/wire"
30+)
31+
32+func previewLifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {
33+ value := append([]byte(nil), id[:]...)
34+ return &wire.LifecycleID{Value: value}
35+}
36+
37+func previewLifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {
38+ var id domain.SessionLifecycleID
39+ if message == nil || len(message.GetValue()) != len(id) {
40+ return id, errConvertRange
41+ }
42+ copy(id[:], message.GetValue())
43+ return id, nil
44+}
45+
46+func previewTabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {
47+ return &wire.TabSelector{
48+ Kind: uint32(selector.Kind),
49+ StableId: string(selector.StableID),
50+ Ordinal: uint32(selector.Ordinal),
51+ RawName: selector.RawName,
52+ ExpectedCount: uint32(selector.ExpectedCount),
53+ }
54+}
55+
56+func previewTabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {
57+ var selector domain.TabSelector
58+ if message == nil {
59+ return selector, nil
60+ }
61+ kind, err := brokerEnum8[domain.TabSelectorKind](message.GetKind())
62+ if err != nil {
63+ return domain.TabSelector{}, err
64+ }
65+ selector.Kind = kind
66+ selector.StableID = domain.TabStableID(message.GetStableId())
67+ ordinal, err := brokerEnum16[uint16](message.GetOrdinal())
68+ if err != nil {
69+ return domain.TabSelector{}, err
70+ }
71+ selector.Ordinal = ordinal
72+ selector.RawName = message.GetRawName()
73+ expected, err := brokerEnum16[uint16](message.GetExpectedCount())
74+ if err != nil {
75+ return domain.TabSelector{}, err
76+ }
77+ selector.ExpectedCount = expected
78+ return selector, nil
79+}
80+
81+func previewRemoteTargetToWire(target domain.RemoteSessionTarget) (*wire.RemoteTarget, error) {
82+ lifecycle := previewLifecycleToWire(target.LifecycleID)
83+ return &wire.RemoteTarget{
84+ Endpoint: target.Endpoint,
85+ DisplayOrigin: target.DisplayOrigin,
86+ LifecycleId: lifecycle,
87+ SessionName: target.SessionName,
88+ LiveTabId: string(target.LiveTabID),
89+ StoppedTab: previewTabSelectorToWire(target.StoppedTab),
90+ Stopped: target.Stopped,
91+ }, nil
92+}
93+
94+func previewRemoteTargetFromWire(message *wire.RemoteTarget) (domain.RemoteSessionTarget, error) {
95+ var target domain.RemoteSessionTarget
96+ if message == nil {
97+ return target, errConvertRange
98+ }
99+ lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
100+ if err != nil {
101+ return domain.RemoteSessionTarget{}, err
102+ }
103+ stopped, err := previewTabSelectorFromWire(message.GetStoppedTab())
104+ if err != nil {
105+ return domain.RemoteSessionTarget{}, err
106+ }
107+ return domain.RemoteSessionTarget{
108+ Endpoint: message.GetEndpoint(),
109+ DisplayOrigin: message.GetDisplayOrigin(),
110+ LifecycleID: lifecycle,
111+ SessionName: message.GetSessionName(),
112+ LiveTabID: domain.TabStableID(message.GetLiveTabId()),
113+ StoppedTab: stopped,
114+ Stopped: message.GetStopped(),
115+ }, nil
116+}
117+
118+func previewRGBToWire(color renderer.RGB) *wire.RGB {
119+ return &wire.RGB{R: uint32(color.R), G: uint32(color.G), B: uint32(color.B)}
120+}
121+
122+func previewRGBFromWire(message *wire.RGB) (renderer.RGB, error) {
123+ var color renderer.RGB
124+ if message == nil {
125+ return color, nil
126+ }
127+ r, err := brokerEnum8[uint8](message.GetR())
128+ if err != nil {
129+ return renderer.RGB{}, err
130+ }
131+ g, err := brokerEnum8[uint8](message.GetG())
132+ if err != nil {
133+ return renderer.RGB{}, err
134+ }
135+ b, err := brokerEnum8[uint8](message.GetB())
136+ if err != nil {
137+ return renderer.RGB{}, err
138+ }
139+ return renderer.RGB{R: r, G: g, B: b}, nil
140+}
141+
142+func previewCellStyleToWire(style renderer.Style) *wire.CellStyle {
143+ return &wire.CellStyle{
144+ Bold: style.Bold,
145+ Italic: style.Italic,
146+ Inverse: style.Inverse,
147+ Attrs: uint32(style.Attrs),
148+ Foreground: int32(style.Foreground),
149+ Background: int32(style.Background),
150+ HasForegroundRgb: style.HasForegroundRGB,
151+ HasBackgroundRgb: style.HasBackgroundRGB,
152+ HasUnderlineColor: style.HasUnderlineColor,
153+ HasUnderlineColorRgb: style.HasUnderlineColorRGB,
154+ UnderlineStyle: uint32(style.UnderlineStyle),
155+ UnderlineColor: int32(style.UnderlineColor),
156+ ForegroundRgb: previewRGBToWire(style.ForegroundRGB),
157+ BackgroundRgb: previewRGBToWire(style.BackgroundRGB),
158+ UnderlineColorRgb: previewRGBToWire(style.UnderlineColorRGB),
159+ }
160+}
161+
162+func previewCellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {
163+ var style renderer.Style
164+ if message == nil {
165+ return style, nil
166+ }
167+ foreground := message.GetForeground()
168+ if foreground < math.MinInt16 || foreground > math.MaxInt16 {
169+ return renderer.Style{}, errConvertRange
170+ }
171+ background := message.GetBackground()
172+ if background < math.MinInt16 || background > math.MaxInt16 {
173+ return renderer.Style{}, errConvertRange
174+ }
175+ underline := message.GetUnderlineColor()
176+ if underline < math.MinInt16 || underline > math.MaxInt16 {
177+ return renderer.Style{}, errConvertRange
178+ }
179+ underlineStyle, err := brokerEnum8[uint8](message.GetUnderlineStyle())
180+ if err != nil {
181+ return renderer.Style{}, err
182+ }
183+ attrs := message.GetAttrs()
184+ if attrs > math.MaxUint16 {
185+ return renderer.Style{}, errConvertRange
186+ }
187+ foregroundRGB, err := previewRGBFromWire(message.GetForegroundRgb())
188+ if err != nil {
189+ return renderer.Style{}, err
190+ }
191+ backgroundRGB, err := previewRGBFromWire(message.GetBackgroundRgb())
192+ if err != nil {
193+ return renderer.Style{}, err
194+ }
195+ underlineRGB, err := previewRGBFromWire(message.GetUnderlineColorRgb())
196+ if err != nil {
197+ return renderer.Style{}, err
198+ }
199+ return renderer.Style{
200+ Bold: message.GetBold(),
201+ Italic: message.GetItalic(),
202+ Inverse: message.GetInverse(),
203+ Attrs: renderer.StyleAttrs(attrs),
204+ Foreground: int(foreground),
205+ Background: int(background),
206+ HasForegroundRGB: message.GetHasForegroundRgb(),
207+ HasBackgroundRGB: message.GetHasBackgroundRgb(),
208+ HasUnderlineColor: message.GetHasUnderlineColor(),
209+ HasUnderlineColorRGB: message.GetHasUnderlineColorRgb(),
210+ UnderlineStyle: renderer.UnderlineStyle(underlineStyle),
211+ UnderlineColor: int(underline),
212+ ForegroundRGB: foregroundRGB,
213+ BackgroundRGB: backgroundRGB,
214+ UnderlineColorRGB: underlineRGB,
215+ }, nil
216+}
217+
218+func previewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {
219+ if cell.Rune < 0 || cell.Rune > 0x10FFFF {
220+ return nil, errConvertRange
221+ }
222+ return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: previewCellStyleToWire(cell.Style)}, nil
223+}
224+
225+func previewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {
226+ var cell renderer.Cell
227+ if message == nil {
228+ return cell, errConvertRange
229+ }
230+ style, err := previewCellStyleFromWire(message.GetStyle())
231+ if err != nil {
232+ return renderer.Cell{}, err
233+ }
234+ runeValue := message.GetRuneValue()
235+ if runeValue > 0x10FFFF {
236+ return renderer.Cell{}, errConvertRange
237+ }
238+ return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil
239+}
240+
241+func previewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {
242+ if len(cells) == 0 {
243+ return nil, nil
244+ }
245+ out := make([]*wire.PreviewCell, 0, len(cells))
246+ for _, cell := range cells {
247+ converted, err := previewCellToWire(cell)
248+ if err != nil {
249+ return nil, err
250+ }
251+ out = append(out, converted)
252+ }
253+ return out, nil
254+}
255+
256+func previewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {
257+ if len(cells) == 0 {
258+ return nil, nil
259+ }
260+ out := make([]renderer.Cell, 0, len(cells))
261+ for _, cell := range cells {
262+ converted, err := previewCellFromWire(cell)
263+ if err != nil {
264+ return nil, err
265+ }
266+ out = append(out, converted)
267+ }
268+ return out, nil
269+}
270+
271+func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
272+ if err := protocol.ValidateRemotePreviewRequest(message); err != nil {
273+ return nil, ErrInvalidMessage
274+ }
275+ target, err := previewRemoteTargetToWire(message.Target)
276+ if err != nil {
277+ return nil, err
278+ }
279+ return &wire.RemotePreviewRequest{
280+ Version: uint32(message.Version),
281+ Target: target,
282+ Width: uint32(message.Width),
283+ Height: uint32(message.Height),
284+ }, nil
285+}
286+
287+func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
288+ var request protocol.RemotePreviewRequest
289+ if message == nil {
290+ return request, ErrInvalidMessage
291+ }
292+ version, err := brokerEnum16[uint16](message.GetVersion())
293+ if err != nil {
294+ return protocol.RemotePreviewRequest{}, errConvertRange
295+ }
296+ target, err := previewRemoteTargetFromWire(message.GetTarget())
297+ if err != nil {
298+ return protocol.RemotePreviewRequest{}, err
299+ }
300+ width, err := brokerEnum16[uint16](message.GetWidth())
301+ if err != nil {
302+ return protocol.RemotePreviewRequest{}, errConvertRange
303+ }
304+ height, err := brokerEnum16[uint16](message.GetHeight())
305+ if err != nil {
306+ return protocol.RemotePreviewRequest{}, errConvertRange
307+ }
308+ request = protocol.RemotePreviewRequest{Version: version, Target: target, Width: width, Height: height}
309+ if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
310+ return protocol.RemotePreviewRequest{}, ErrInvalidMessage
311+ }
312+ return request, nil
313+}
314+
315+func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
316+ if err := protocol.ValidateRemotePreview(message); err != nil {
317+ if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
318+ return nil, ErrTooLarge
319+ }
320+ return nil, ErrInvalidMessage
321+ }
322+ cells, err := previewCellsToWire(message.Cells)
323+ if err != nil {
324+ return nil, err
325+ }
326+ return &wire.RemotePreview{
327+ Version: uint32(message.Version),
328+ Status: uint32(message.Status),
329+ LifecycleId: previewLifecycleToWire(message.LifecycleID),
330+ TabId: string(message.TabID),
331+ Revision: message.Revision,
332+ Width: uint32(message.Width),
333+ Height: uint32(message.Height),
334+ Cells: cells,
335+ }, nil
336+}
337+
338+func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
339+ var preview protocol.RemotePreview
340+ if message == nil {
341+ return preview, ErrInvalidMessage
342+ }
343+ version, err := brokerEnum16[uint16](message.GetVersion())
344+ if err != nil {
345+ return protocol.RemotePreview{}, errConvertRange
346+ }
347+ status, err := brokerEnum8[protocol.RemotePreviewStatus](message.GetStatus())
348+ if err != nil {
349+ return protocol.RemotePreview{}, errConvertRange
350+ }
351+ lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
352+ if err != nil {
353+ return protocol.RemotePreview{}, err
354+ }
355+ width, err := brokerEnum16[uint16](message.GetWidth())
356+ if err != nil {
357+ return protocol.RemotePreview{}, errConvertRange
358+ }
359+ height, err := brokerEnum16[uint16](message.GetHeight())
360+ if err != nil {
361+ return protocol.RemotePreview{}, errConvertRange
362+ }
363+ cells, err := previewCellsFromWire(message.GetCells())
364+ if err != nil {
365+ return protocol.RemotePreview{}, err
366+ }
367+ preview = protocol.RemotePreview{
368+ Version: version,
369+ Status: status,
370+ LifecycleID: lifecycle,
371+ TabID: domain.TabStableID(message.GetTabId()),
372+ Revision: message.GetRevision(),
373+ Width: width,
374+ Height: height,
375+ Cells: cells,
376+ }
377+ if err := protocol.ValidateRemotePreview(preview); err != nil {
378+ if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
379+ return protocol.RemotePreview{}, ErrTooLarge
380+ }
381+ return protocol.RemotePreview{}, ErrInvalidMessage
382+ }
383+ return preview, nil
384+}
385+
386+// previewRouteRequest mirrors one StartPreview route as the ports stream
387+// request the preview contract gates, so encode and decode refuse exactly
388+// what ports.BrokerPreviewRequest.Validate refuses.
389+func previewRouteRequest(m StartPreview) ports.BrokerPreviewRequest {
390+ return ports.BrokerPreviewRequest{
391+ Epoch: m.Epoch,
392+ Connection: m.Connection,
393+ Generation: m.Generation,
394+ Route: ports.BrokerOpenStreamRequest{
395+ Epoch: m.Route.Epoch, Purpose: m.Route.Purpose,
396+ Admission: m.Route.Admission, Name: m.Route.Name, Local: m.Route.Local,
397+ Connection: m.Route.Connection, Stream: m.Route.Stream,
398+ Endpoint: m.Route.Endpoint, Registration: m.Route.Registration,
399+ Target: m.Route.Target, Env: m.Route.Env, Policy: m.Route.Policy,
400+ StartMode: m.Route.StartMode,
401+ },
402+ Preview: m.Preview,
403+ }
404+}
405+
406+func startPreviewToWire(m StartPreview) (*wire.StartPreview, error) {
407+ if m.Epoch == 0 {
408+ return nil, ErrInvalidMessage
409+ }
410+ if err := m.Connection.Validate(); err != nil {
411+ return nil, ErrInvalidMessage
412+ }
413+ if err := m.Generation.Validate(); err != nil {
414+ return nil, ErrInvalidMessage
415+ }
416+ // Authority repeats on the route: a route bound to another scope or a
417+ // non-observation purpose is refused before it travels.
418+ if m.Route.Epoch != m.Epoch || m.Route.Connection != m.Connection {
419+ return nil, ErrInvalidMessage
420+ }
421+ if m.Route.Purpose != ports.BrokerStreamObservation {
422+ return nil, ErrInvalidMessage
423+ }
424+ route, err := openStreamToWire(m.Route)
425+ if err != nil {
426+ return nil, err
427+ }
428+ preview, err := remotePreviewRequestToWire(m.Preview)
429+ if err != nil {
430+ return nil, err
431+ }
432+ if err := previewRouteRequest(m).Validate(); err != nil {
433+ return nil, ErrInvalidMessage
434+ }
435+ return &wire.StartPreview{
436+ Scope: scopeToWire(m.Epoch, m.Connection),
437+ Generation: uint64(m.Generation),
438+ Route: route,
439+ Preview: preview,
440+ }, nil
441+}
442+
443+func startPreviewFromWire(message *wire.StartPreview) (StartPreview, error) {
444+ var out StartPreview
445+ if message == nil {
446+ return out, ErrInvalidMessage
447+ }
448+ epoch, connection, err := scopeFromWire(message.GetScope())
449+ if err != nil {
450+ return StartPreview{}, ErrInvalidMessage
451+ }
452+ generation := ports.BrokerPreviewGeneration(message.GetGeneration())
453+ if err := generation.Validate(); err != nil {
454+ return StartPreview{}, ErrInvalidMessage
455+ }
456+ route, err := openStreamFromWire(message.GetRoute())
457+ if err != nil {
458+ return StartPreview{}, err
459+ }
460+ preview, err := remotePreviewRequestFromWire(message.GetPreview())
461+ if err != nil {
462+ return StartPreview{}, err
463+ }
464+ candidate := StartPreview{Epoch: epoch, Connection: connection, Generation: generation, Route: route, Preview: preview}
465+ if err := previewRouteRequest(candidate).Validate(); err != nil {
466+ return StartPreview{}, ErrInvalidMessage
467+ }
468+ return candidate, nil
469+}
470+
471+func cancelPreviewToWire(m CancelPreview) (*wire.CancelPreview, error) {
472+ if m.Epoch == 0 {
473+ return nil, ErrInvalidMessage
474+ }
475+ if err := m.Connection.Validate(); err != nil {
476+ return nil, ErrInvalidMessage
477+ }
478+ if err := m.Generation.Validate(); err != nil {
479+ return nil, ErrInvalidMessage
480+ }
481+ return &wire.CancelPreview{
482+ Scope: scopeToWire(m.Epoch, m.Connection),
483+ Generation: uint64(m.Generation),
484+ }, nil
485+}
486+
487+func cancelPreviewFromWire(message *wire.CancelPreview) (CancelPreview, error) {
488+ var out CancelPreview
489+ if message == nil {
490+ return out, ErrInvalidMessage
491+ }
492+ epoch, connection, err := scopeFromWire(message.GetScope())
493+ if err != nil {
494+ return CancelPreview{}, ErrInvalidMessage
495+ }
496+ generation := ports.BrokerPreviewGeneration(message.GetGeneration())
497+ if err := generation.Validate(); err != nil {
498+ return CancelPreview{}, ErrInvalidMessage
499+ }
500+ return CancelPreview{Epoch: epoch, Connection: connection, Generation: generation}, nil
501+}
502+
503+func previewPublicationToWire(m PreviewPublication) (*wire.PreviewPublication, error) {
504+ if m.Epoch == 0 {
505+ return nil, ErrInvalidMessage
506+ }
507+ if err := m.Connection.Validate(); err != nil {
508+ return nil, ErrInvalidMessage
509+ }
510+ if err := m.Generation.Validate(); err != nil {
511+ return nil, ErrInvalidMessage
512+ }
513+ out := &wire.PreviewPublication{
514+ Scope: scopeToWire(m.Epoch, m.Connection),
515+ Generation: uint64(m.Generation),
516+ }
517+ // Exactly one of preview/error travels: a failed publication carries
518+ // only its typed error, a successful one only its preview.
519+ if m.HasError {
520+ if err := m.Error.validate(); err != nil {
521+ return nil, err
522+ }
523+ if previewCarriesViewport(m.Preview) {
524+ return nil, ErrInvalidMessage
525+ }
526+ out.Result = &wire.PreviewPublication_Error{Error: errorDetailToWire(m.Error)}
527+ return out, nil
528+ }
529+ if m.Error != (ErrorDetail{}) {
530+ return nil, ErrInvalidMessage
531+ }
532+ preview, err := remotePreviewToWire(m.Preview)
533+ if err != nil {
534+ return nil, err
535+ }
536+ out.Result = &wire.PreviewPublication_Preview{Preview: preview}
537+ return out, nil
538+}
539+
540+// previewCarriesViewport reports whether a preview value carries any
541+// viewport authority. A failed publication must carry none.
542+func previewCarriesViewport(preview protocol.RemotePreview) bool {
543+ return preview.Version != 0 || preview.Status != 0 ||
544+ preview.LifecycleID != (domain.SessionLifecycleID{}) || preview.TabID != "" ||
545+ preview.Revision != 0 || preview.Width != 0 || preview.Height != 0 ||
546+ len(preview.Cells) != 0
547+}
548+
549+func previewPublicationFromWire(message *wire.PreviewPublication) (PreviewPublication, error) {
550+ var out PreviewPublication
551+ if message == nil {
552+ return out, ErrInvalidMessage
553+ }
554+ epoch, connection, err := scopeFromWire(message.GetScope())
555+ if err != nil {
556+ return PreviewPublication{}, ErrInvalidMessage
557+ }
558+ generation := ports.BrokerPreviewGeneration(message.GetGeneration())
559+ if err := generation.Validate(); err != nil {
560+ return PreviewPublication{}, ErrInvalidMessage
561+ }
562+ result := PreviewPublication{Epoch: epoch, Connection: connection, Generation: generation}
563+ switch payload := message.GetResult().(type) {
564+ case *wire.PreviewPublication_Preview:
565+ if payload.Preview == nil {
566+ return PreviewPublication{}, ErrInvalidMessage
567+ }
568+ preview, err := remotePreviewFromWire(payload.Preview)
569+ if err != nil {
570+ return PreviewPublication{}, err
571+ }
572+ result.Preview = preview
573+ case *wire.PreviewPublication_Error:
574+ if payload.Error == nil {
575+ return PreviewPublication{}, ErrInvalidMessage
576+ }
577+ detail, err := errorDetailFromWire(payload.Error)
578+ if err != nil {
579+ return PreviewPublication{}, ErrInvalidMessage
580+ }
581+ result.Error = detail
582+ result.HasError = true
583+ default:
584+ // An absent result is malformed: exactly one member is required.
585+ return PreviewPublication{}, ErrInvalidMessage
586+ }
587+ return result, nil
588+}
@@ -0,0 +1,588 @@
1+package brokerwire
2+
3+// Preview wire converters (broker preview slice 1).
4+//
5+// StartPreview/CancelPreview travel client-to-server; PreviewPublication
6+// travels server-to-client with exactly one of its preview/error result
7+// members set. Conversion mirrors the port preview types losslessly and
8+// validates every narrowing numeric cast before it happens, following the
9+// shared converter patterns in convert.go: wire uint32 taxonomy fields
10+// never truncate into a smaller semantic enum, identity lengths are exact,
11+// and semantic validation failures map to ErrInvalidMessage.
12+//
13+// The preview viewport (RemotePreviewRequest/RemotePreview) reuses the
14+// terminal wire shapes owned by the session conversation; the broker
15+// conversation carries them as nested messages under its own envelope
16+// tags. Repeated authority is scope/generation plus the preview target
17+// identity response; route/request dimensions are retained by client
18+// subscription state and re-checked here through the ports preview
19+// contract.
20+
21+import (
22+ "errors"
23+ "math"
24+
25+ renderer "github.com/bnema/vev-vt"
26+ "github.com/bnema/vev/internal/domain"
27+ "github.com/bnema/vev/internal/ports"
28+ "github.com/bnema/vev/internal/protocol"
29+ "github.com/bnema/vev/internal/protocol/wire"
30+)
31+
32+func previewLifecycleToWire(id domain.SessionLifecycleID) *wire.LifecycleID {
33+ value := append([]byte(nil), id[:]...)
34+ return &wire.LifecycleID{Value: value}
35+}
36+
37+func previewLifecycleFromWire(message *wire.LifecycleID) (domain.SessionLifecycleID, error) {
38+ var id domain.SessionLifecycleID
39+ if message == nil || len(message.GetValue()) != len(id) {
40+ return id, errConvertRange
41+ }
42+ copy(id[:], message.GetValue())
43+ return id, nil
44+}
45+
46+func previewTabSelectorToWire(selector domain.TabSelector) *wire.TabSelector {
47+ return &wire.TabSelector{
48+ Kind: uint32(selector.Kind),
49+ StableId: string(selector.StableID),
50+ Ordinal: uint32(selector.Ordinal),
51+ RawName: selector.RawName,
52+ ExpectedCount: uint32(selector.ExpectedCount),
53+ }
54+}
55+
56+func previewTabSelectorFromWire(message *wire.TabSelector) (domain.TabSelector, error) {
57+ var selector domain.TabSelector
58+ if message == nil {
59+ return selector, nil
60+ }
61+ kind, err := brokerEnum8[domain.TabSelectorKind](message.GetKind())
62+ if err != nil {
63+ return domain.TabSelector{}, err
64+ }
65+ selector.Kind = kind
66+ selector.StableID = domain.TabStableID(message.GetStableId())
67+ ordinal, err := brokerEnum16[uint16](message.GetOrdinal())
68+ if err != nil {
69+ return domain.TabSelector{}, err
70+ }
71+ selector.Ordinal = ordinal
72+ selector.RawName = message.GetRawName()
73+ expected, err := brokerEnum16[uint16](message.GetExpectedCount())
74+ if err != nil {
75+ return domain.TabSelector{}, err
76+ }
77+ selector.ExpectedCount = expected
78+ return selector, nil
79+}
80+
81+func previewRemoteTargetToWire(target domain.RemoteSessionTarget) (*wire.RemoteTarget, error) {
82+ lifecycle := previewLifecycleToWire(target.LifecycleID)
83+ return &wire.RemoteTarget{
84+ Endpoint: target.Endpoint,
85+ DisplayOrigin: target.DisplayOrigin,
86+ LifecycleId: lifecycle,
87+ SessionName: target.SessionName,
88+ LiveTabId: string(target.LiveTabID),
89+ StoppedTab: previewTabSelectorToWire(target.StoppedTab),
90+ Stopped: target.Stopped,
91+ }, nil
92+}
93+
94+func previewRemoteTargetFromWire(message *wire.RemoteTarget) (domain.RemoteSessionTarget, error) {
95+ var target domain.RemoteSessionTarget
96+ if message == nil {
97+ return target, errConvertRange
98+ }
99+ lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
100+ if err != nil {
101+ return domain.RemoteSessionTarget{}, err
102+ }
103+ stopped, err := previewTabSelectorFromWire(message.GetStoppedTab())
104+ if err != nil {
105+ return domain.RemoteSessionTarget{}, err
106+ }
107+ return domain.RemoteSessionTarget{
108+ Endpoint: message.GetEndpoint(),
109+ DisplayOrigin: message.GetDisplayOrigin(),
110+ LifecycleID: lifecycle,
111+ SessionName: message.GetSessionName(),
112+ LiveTabID: domain.TabStableID(message.GetLiveTabId()),
113+ StoppedTab: stopped,
114+ Stopped: message.GetStopped(),
115+ }, nil
116+}
117+
118+func previewRGBToWire(color renderer.RGB) *wire.RGB {
119+ return &wire.RGB{R: uint32(color.R), G: uint32(color.G), B: uint32(color.B)}
120+}
121+
122+func previewRGBFromWire(message *wire.RGB) (renderer.RGB, error) {
123+ var color renderer.RGB
124+ if message == nil {
125+ return color, nil
126+ }
127+ r, err := brokerEnum8[uint8](message.GetR())
128+ if err != nil {
129+ return renderer.RGB{}, err
130+ }
131+ g, err := brokerEnum8[uint8](message.GetG())
132+ if err != nil {
133+ return renderer.RGB{}, err
134+ }
135+ b, err := brokerEnum8[uint8](message.GetB())
136+ if err != nil {
137+ return renderer.RGB{}, err
138+ }
139+ return renderer.RGB{R: r, G: g, B: b}, nil
140+}
141+
142+func previewCellStyleToWire(style renderer.Style) *wire.CellStyle {
143+ return &wire.CellStyle{
144+ Bold: style.Bold,
145+ Italic: style.Italic,
146+ Inverse: style.Inverse,
147+ Attrs: uint32(style.Attrs),
148+ Foreground: int32(style.Foreground),
149+ Background: int32(style.Background),
150+ HasForegroundRgb: style.HasForegroundRGB,
151+ HasBackgroundRgb: style.HasBackgroundRGB,
152+ HasUnderlineColor: style.HasUnderlineColor,
153+ HasUnderlineColorRgb: style.HasUnderlineColorRGB,
154+ UnderlineStyle: uint32(style.UnderlineStyle),
155+ UnderlineColor: int32(style.UnderlineColor),
156+ ForegroundRgb: previewRGBToWire(style.ForegroundRGB),
157+ BackgroundRgb: previewRGBToWire(style.BackgroundRGB),
158+ UnderlineColorRgb: previewRGBToWire(style.UnderlineColorRGB),
159+ }
160+}
161+
162+func previewCellStyleFromWire(message *wire.CellStyle) (renderer.Style, error) {
163+ var style renderer.Style
164+ if message == nil {
165+ return style, nil
166+ }
167+ foreground := message.GetForeground()
168+ if foreground < math.MinInt16 || foreground > math.MaxInt16 {
169+ return renderer.Style{}, errConvertRange
170+ }
171+ background := message.GetBackground()
172+ if background < math.MinInt16 || background > math.MaxInt16 {
173+ return renderer.Style{}, errConvertRange
174+ }
175+ underline := message.GetUnderlineColor()
176+ if underline < math.MinInt16 || underline > math.MaxInt16 {
177+ return renderer.Style{}, errConvertRange
178+ }
179+ underlineStyle, err := brokerEnum8[uint8](message.GetUnderlineStyle())
180+ if err != nil {
181+ return renderer.Style{}, err
182+ }
183+ attrs := message.GetAttrs()
184+ if attrs > math.MaxUint16 {
185+ return renderer.Style{}, errConvertRange
186+ }
187+ foregroundRGB, err := previewRGBFromWire(message.GetForegroundRgb())
188+ if err != nil {
189+ return renderer.Style{}, err
190+ }
191+ backgroundRGB, err := previewRGBFromWire(message.GetBackgroundRgb())
192+ if err != nil {
193+ return renderer.Style{}, err
194+ }
195+ underlineRGB, err := previewRGBFromWire(message.GetUnderlineColorRgb())
196+ if err != nil {
197+ return renderer.Style{}, err
198+ }
199+ return renderer.Style{
200+ Bold: message.GetBold(),
201+ Italic: message.GetItalic(),
202+ Inverse: message.GetInverse(),
203+ Attrs: renderer.StyleAttrs(attrs),
204+ Foreground: int(foreground),
205+ Background: int(background),
206+ HasForegroundRGB: message.GetHasForegroundRgb(),
207+ HasBackgroundRGB: message.GetHasBackgroundRgb(),
208+ HasUnderlineColor: message.GetHasUnderlineColor(),
209+ HasUnderlineColorRGB: message.GetHasUnderlineColorRgb(),
210+ UnderlineStyle: renderer.UnderlineStyle(underlineStyle),
211+ UnderlineColor: int(underline),
212+ ForegroundRGB: foregroundRGB,
213+ BackgroundRGB: backgroundRGB,
214+ UnderlineColorRGB: underlineRGB,
215+ }, nil
216+}
217+
218+func previewCellToWire(cell renderer.Cell) (*wire.PreviewCell, error) {
219+ if cell.Rune < 0 || cell.Rune > 0x10FFFF {
220+ return nil, errConvertRange
221+ }
222+ return &wire.PreviewCell{RuneValue: uint32(cell.Rune), Continuation: cell.Continuation, Style: previewCellStyleToWire(cell.Style)}, nil
223+}
224+
225+func previewCellFromWire(message *wire.PreviewCell) (renderer.Cell, error) {
226+ var cell renderer.Cell
227+ if message == nil {
228+ return cell, errConvertRange
229+ }
230+ style, err := previewCellStyleFromWire(message.GetStyle())
231+ if err != nil {
232+ return renderer.Cell{}, err
233+ }
234+ runeValue := message.GetRuneValue()
235+ if runeValue > 0x10FFFF {
236+ return renderer.Cell{}, errConvertRange
237+ }
238+ return renderer.Cell{Rune: rune(runeValue), Continuation: message.GetContinuation(), Style: style}, nil
239+}
240+
241+func previewCellsToWire(cells []renderer.Cell) ([]*wire.PreviewCell, error) {
242+ if len(cells) == 0 {
243+ return nil, nil
244+ }
245+ out := make([]*wire.PreviewCell, 0, len(cells))
246+ for _, cell := range cells {
247+ converted, err := previewCellToWire(cell)
248+ if err != nil {
249+ return nil, err
250+ }
251+ out = append(out, converted)
252+ }
253+ return out, nil
254+}
255+
256+func previewCellsFromWire(cells []*wire.PreviewCell) ([]renderer.Cell, error) {
257+ if len(cells) == 0 {
258+ return nil, nil
259+ }
260+ out := make([]renderer.Cell, 0, len(cells))
261+ for _, cell := range cells {
262+ converted, err := previewCellFromWire(cell)
263+ if err != nil {
264+ return nil, err
265+ }
266+ out = append(out, converted)
267+ }
268+ return out, nil
269+}
270+
271+func remotePreviewRequestToWire(message protocol.RemotePreviewRequest) (*wire.RemotePreviewRequest, error) {
272+ if err := protocol.ValidateRemotePreviewRequest(message); err != nil {
273+ return nil, ErrInvalidMessage
274+ }
275+ target, err := previewRemoteTargetToWire(message.Target)
276+ if err != nil {
277+ return nil, err
278+ }
279+ return &wire.RemotePreviewRequest{
280+ Version: uint32(message.Version),
281+ Target: target,
282+ Width: uint32(message.Width),
283+ Height: uint32(message.Height),
284+ }, nil
285+}
286+
287+func remotePreviewRequestFromWire(message *wire.RemotePreviewRequest) (protocol.RemotePreviewRequest, error) {
288+ var request protocol.RemotePreviewRequest
289+ if message == nil {
290+ return request, ErrInvalidMessage
291+ }
292+ version, err := brokerEnum16[uint16](message.GetVersion())
293+ if err != nil {
294+ return protocol.RemotePreviewRequest{}, errConvertRange
295+ }
296+ target, err := previewRemoteTargetFromWire(message.GetTarget())
297+ if err != nil {
298+ return protocol.RemotePreviewRequest{}, err
299+ }
300+ width, err := brokerEnum16[uint16](message.GetWidth())
301+ if err != nil {
302+ return protocol.RemotePreviewRequest{}, errConvertRange
303+ }
304+ height, err := brokerEnum16[uint16](message.GetHeight())
305+ if err != nil {
306+ return protocol.RemotePreviewRequest{}, errConvertRange
307+ }
308+ request = protocol.RemotePreviewRequest{Version: version, Target: target, Width: width, Height: height}
309+ if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
310+ return protocol.RemotePreviewRequest{}, ErrInvalidMessage
311+ }
312+ return request, nil
313+}
314+
315+func remotePreviewToWire(message protocol.RemotePreview) (*wire.RemotePreview, error) {
316+ if err := protocol.ValidateRemotePreview(message); err != nil {
317+ if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
318+ return nil, ErrTooLarge
319+ }
320+ return nil, ErrInvalidMessage
321+ }
322+ cells, err := previewCellsToWire(message.Cells)
323+ if err != nil {
324+ return nil, err
325+ }
326+ return &wire.RemotePreview{
327+ Version: uint32(message.Version),
328+ Status: uint32(message.Status),
329+ LifecycleId: previewLifecycleToWire(message.LifecycleID),
330+ TabId: string(message.TabID),
331+ Revision: message.Revision,
332+ Width: uint32(message.Width),
333+ Height: uint32(message.Height),
334+ Cells: cells,
335+ }, nil
336+}
337+
338+func remotePreviewFromWire(message *wire.RemotePreview) (protocol.RemotePreview, error) {
339+ var preview protocol.RemotePreview
340+ if message == nil {
341+ return preview, ErrInvalidMessage
342+ }
343+ version, err := brokerEnum16[uint16](message.GetVersion())
344+ if err != nil {
345+ return protocol.RemotePreview{}, errConvertRange
346+ }
347+ status, err := brokerEnum8[protocol.RemotePreviewStatus](message.GetStatus())
348+ if err != nil {
349+ return protocol.RemotePreview{}, errConvertRange
350+ }
351+ lifecycle, err := previewLifecycleFromWire(message.GetLifecycleId())
352+ if err != nil {
353+ return protocol.RemotePreview{}, err
354+ }
355+ width, err := brokerEnum16[uint16](message.GetWidth())
356+ if err != nil {
357+ return protocol.RemotePreview{}, errConvertRange
358+ }
359+ height, err := brokerEnum16[uint16](message.GetHeight())
360+ if err != nil {
361+ return protocol.RemotePreview{}, errConvertRange
362+ }
363+ cells, err := previewCellsFromWire(message.GetCells())
364+ if err != nil {
365+ return protocol.RemotePreview{}, err
366+ }
367+ preview = protocol.RemotePreview{
368+ Version: version,
369+ Status: status,
370+ LifecycleID: lifecycle,
371+ TabID: domain.TabStableID(message.GetTabId()),
372+ Revision: message.GetRevision(),
373+ Width: width,
374+ Height: height,
375+ Cells: cells,
376+ }
377+ if err := protocol.ValidateRemotePreview(preview); err != nil {
378+ if errors.Is(err, protocol.ErrRemotePreviewTooLarge) {
379+ return protocol.RemotePreview{}, ErrTooLarge
380+ }
381+ return protocol.RemotePreview{}, ErrInvalidMessage
382+ }
383+ return preview, nil
384+}
385+
386+// previewRouteRequest mirrors one StartPreview route as the ports stream
387+// request the preview contract gates, so encode and decode refuse exactly
388+// what ports.BrokerPreviewRequest.Validate refuses.
389+func previewRouteRequest(m StartPreview) ports.BrokerPreviewRequest {
390+ return ports.BrokerPreviewRequest{
391+ Epoch: m.Epoch,
392+ Connection: m.Connection,
393+ Generation: m.Generation,
394+ Route: ports.BrokerOpenStreamRequest{
395+ Epoch: m.Route.Epoch, Purpose: m.Route.Purpose,
396+ Admission: m.Route.Admission, Name: m.Route.Name, Local: m.Route.Local,
397+ Connection: m.Route.Connection, Stream: m.Route.Stream,
398+ Endpoint: m.Route.Endpoint, Registration: m.Route.Registration,
399+ Target: m.Route.Target, Env: m.Route.Env, Policy: m.Route.Policy,
400+ StartMode: m.Route.StartMode,
401+ },
402+ Preview: m.Preview,
403+ }
404+}
405+
406+func startPreviewToWire(m StartPreview) (*wire.StartPreview, error) {
407+ if m.Epoch == 0 {
408+ return nil, ErrInvalidMessage
409+ }
410+ if err := m.Connection.Validate(); err != nil {
411+ return nil, ErrInvalidMessage
412+ }
413+ if err := m.Generation.Validate(); err != nil {
414+ return nil, ErrInvalidMessage
415+ }
416+ // Authority repeats on the route: a route bound to another scope or a
417+ // non-observation purpose is refused before it travels.
418+ if m.Route.Epoch != m.Epoch || m.Route.Connection != m.Connection {
419+ return nil, ErrInvalidMessage
420+ }
421+ if m.Route.Purpose != ports.BrokerStreamObservation {
422+ return nil, ErrInvalidMessage
423+ }
424+ route, err := openStreamToWire(m.Route)
425+ if err != nil {
426+ return nil, err
427+ }
428+ preview, err := remotePreviewRequestToWire(m.Preview)
429+ if err != nil {
430+ return nil, err
431+ }
432+ if err := previewRouteRequest(m).Validate(); err != nil {
433+ return nil, ErrInvalidMessage
434+ }
435+ return &wire.StartPreview{
436+ Scope: scopeToWire(m.Epoch, m.Connection),
437+ Generation: uint64(m.Generation),
438+ Route: route,
439+ Preview: preview,
440+ }, nil
441+}
442+
443+func startPreviewFromWire(message *wire.StartPreview) (StartPreview, error) {
444+ var out StartPreview
445+ if message == nil {
446+ return out, ErrInvalidMessage
447+ }
448+ epoch, connection, err := scopeFromWire(message.GetScope())
449+ if err != nil {
450+ return StartPreview{}, ErrInvalidMessage
451+ }
452+ generation := ports.BrokerPreviewGeneration(message.GetGeneration())
453+ if err := generation.Validate(); err != nil {
454+ return StartPreview{}, ErrInvalidMessage
455+ }
456+ route, err := openStreamFromWire(message.GetRoute())
457+ if err != nil {
458+ return StartPreview{}, err
459+ }
460+ preview, err := remotePreviewRequestFromWire(message.GetPreview())
461+ if err != nil {
462+ return StartPreview{}, err
463+ }
464+ candidate := StartPreview{Epoch: epoch, Connection: connection, Generation: generation, Route: route, Preview: preview}
465+ if err := previewRouteRequest(candidate).Validate(); err != nil {
466+ return StartPreview{}, ErrInvalidMessage
467+ }
468+ return candidate, nil
469+}
470+
471+func cancelPreviewToWire(m CancelPreview) (*wire.CancelPreview, error) {
472+ if m.Epoch == 0 {
473+ return nil, ErrInvalidMessage
474+ }
475+ if err := m.Connection.Validate(); err != nil {
476+ return nil, ErrInvalidMessage
477+ }
478+ if err := m.Generation.Validate(); err != nil {
479+ return nil, ErrInvalidMessage
480+ }
481+ return &wire.CancelPreview{
482+ Scope: scopeToWire(m.Epoch, m.Connection),
483+ Generation: uint64(m.Generation),
484+ }, nil
485+}
486+
487+func cancelPreviewFromWire(message *wire.CancelPreview) (CancelPreview, error) {
488+ var out CancelPreview
489+ if message == nil {
490+ return out, ErrInvalidMessage
491+ }
492+ epoch, connection, err := scopeFromWire(message.GetScope())
493+ if err != nil {
494+ return CancelPreview{}, ErrInvalidMessage
495+ }
496+ generation := ports.BrokerPreviewGeneration(message.GetGeneration())
497+ if err := generation.Validate(); err != nil {
498+ return CancelPreview{}, ErrInvalidMessage
499+ }
500+ return CancelPreview{Epoch: epoch, Connection: connection, Generation: generation}, nil
501+}
502+
503+func previewPublicationToWire(m PreviewPublication) (*wire.PreviewPublication, error) {
504+ if m.Epoch == 0 {
505+ return nil, ErrInvalidMessage
506+ }
507+ if err := m.Connection.Validate(); err != nil {
508+ return nil, ErrInvalidMessage
509+ }
510+ if err := m.Generation.Validate(); err != nil {
511+ return nil, ErrInvalidMessage
512+ }
513+ out := &wire.PreviewPublication{
514+ Scope: scopeToWire(m.Epoch, m.Connection),
515+ Generation: uint64(m.Generation),
516+ }
517+ // Exactly one of preview/error travels: a failed publication carries
518+ // only its typed error, a successful one only its preview.
519+ if m.HasError {
520+ if err := m.Error.validate(); err != nil {
521+ return nil, err
522+ }
523+ if previewCarriesViewport(m.Preview) {
524+ return nil, ErrInvalidMessage
525+ }
526+ out.Result = &wire.PreviewPublication_Error{Error: errorDetailToWire(m.Error)}
527+ return out, nil
528+ }
529+ if m.Error != (ErrorDetail{}) {
530+ return nil, ErrInvalidMessage
531+ }
532+ preview, err := remotePreviewToWire(m.Preview)
533+ if err != nil {
534+ return nil, err
535+ }
536+ out.Result = &wire.PreviewPublication_Preview{Preview: preview}
537+ return out, nil
538+}
539+
540+// previewCarriesViewport reports whether a preview value carries any
541+// viewport authority. A failed publication must carry none.
542+func previewCarriesViewport(preview protocol.RemotePreview) bool {
543+ return preview.Version != 0 || preview.Status != 0 ||
544+ preview.LifecycleID != (domain.SessionLifecycleID{}) || preview.TabID != "" ||
545+ preview.Revision != 0 || preview.Width != 0 || preview.Height != 0 ||
546+ len(preview.Cells) != 0
547+}
548+
549+func previewPublicationFromWire(message *wire.PreviewPublication) (PreviewPublication, error) {
550+ var out PreviewPublication
551+ if message == nil {
552+ return out, ErrInvalidMessage
553+ }
554+ epoch, connection, err := scopeFromWire(message.GetScope())
555+ if err != nil {
556+ return PreviewPublication{}, ErrInvalidMessage
557+ }
558+ generation := ports.BrokerPreviewGeneration(message.GetGeneration())
559+ if err := generation.Validate(); err != nil {
560+ return PreviewPublication{}, ErrInvalidMessage
561+ }
562+ result := PreviewPublication{Epoch: epoch, Connection: connection, Generation: generation}
563+ switch payload := message.GetResult().(type) {
564+ case *wire.PreviewPublication_Preview:
565+ if payload.Preview == nil {
566+ return PreviewPublication{}, ErrInvalidMessage
567+ }
568+ preview, err := remotePreviewFromWire(payload.Preview)
569+ if err != nil {
570+ return PreviewPublication{}, err
571+ }
572+ result.Preview = preview
573+ case *wire.PreviewPublication_Error:
574+ if payload.Error == nil {
575+ return PreviewPublication{}, ErrInvalidMessage
576+ }
577+ detail, err := errorDetailFromWire(payload.Error)
578+ if err != nil {
579+ return PreviewPublication{}, ErrInvalidMessage
580+ }
581+ result.Error = detail
582+ result.HasError = true
583+ default:
584+ // An absent result is malformed: exactly one member is required.
585+ return PreviewPublication{}, ErrInvalidMessage
586+ }
587+ return result, nil
588+}
A internal/adapters/brokerwire/preview_test.go +351 -0
@@ -0,0 +1,351 @@
1+package brokerwire
2+
3+// Preview contract tests (broker preview slice 1).
4+//
5+// StartPreview carries tags 112 with scope, generation, an exact
6+// observation route, and the terminal preview request; CancelPreview
7+// carries tag 113 with scope and generation. PreviewPublication carries
8+// tag 210 with scope, generation, and exactly one of preview/error via
9+// its result oneof. These tests pin the stateless codec's half of that
10+// contract: frozen tags, byte round trips, the strict oneof presence
11+// rule, exact authority preservation, range-strict narrowing, and the
12+// ports preview validation parity (non-observation routes and
13+// scope-mismatched routes refused).
14+
15+import (
16+ "testing"
17+
18+ renderer "github.com/bnema/vev-vt"
19+ "google.golang.org/protobuf/encoding/protowire"
20+ "google.golang.org/protobuf/proto"
21+
22+ "github.com/bnema/vev/internal/domain"
23+ "github.com/bnema/vev/internal/ports"
24+ "github.com/bnema/vev/internal/protocol"
25+ "github.com/bnema/vev/internal/protocol/wire"
26+ "github.com/stretchr/testify/require"
27+)
28+
29+func testPreviewTarget() domain.RemoteSessionTarget {
30+ return domain.RemoteSessionTarget{
31+ Endpoint: "dev@host:22", DisplayOrigin: "dev@host",
32+ LifecycleID: domain.SessionLifecycleID{5, 6, 7},
33+ SessionName: "work", LiveTabID: "tab-1",
34+ }
35+}
36+
37+func testPreviewRequest() protocol.RemotePreviewRequest {
38+ return protocol.RemotePreviewRequest{
39+ Version: protocol.RemotePreviewSchemaVersion,
40+ Target: testPreviewTarget(),
41+ Width: 2, Height: 1,
42+ }
43+}
44+
45+func testPreviewRoute(connection ports.BrokerConnectionID) OpenStream {
46+ return OpenStream{
47+ Epoch: 7, Connection: connection, Stream: 9,
48+ Purpose: ports.BrokerStreamObservation, Local: true,
49+ Policy: testPolicy(), StartMode: ports.BrokerDaemonExistingOnly,
50+ }
51+}
52+
53+func testStartPreview(connection ports.BrokerConnectionID) StartPreview {
54+ return StartPreview{
55+ Epoch: 7, Connection: connection, Generation: 3,
56+ Route: testPreviewRoute(connection), Preview: testPreviewRequest(),
57+ }
58+}
59+
60+func testRemotePreview() protocol.RemotePreview {
61+ return protocol.RemotePreview{
62+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
63+ LifecycleID: testPreviewTarget().LifecycleID, TabID: testPreviewTarget().LiveTabID,
64+ Revision: 11, Width: 2, Height: 1,
65+ Cells: []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}},
66+ }
67+}
68+
69+func testPreviewPublication(connection ports.BrokerConnectionID) PreviewPublication {
70+ return PreviewPublication{
71+ Epoch: 7, Connection: connection, Generation: 3,
72+ Preview: testRemotePreview(),
73+ }
74+}
75+
76+// TestPreviewVariantTags pins the frozen envelope tags: start_preview 112,
77+// cancel_preview 113, preview_publication 210.
78+func TestPreviewVariantTags(t *testing.T) {
79+ connection := testConnectionID(0x61)
80+ raw := mustEncodeClient(t, testStartPreview(connection))
81+ number, wireType, consumed := protowire.ConsumeTag(raw)
82+ require.Greater(t, consumed, 0)
83+ require.Equal(t, protowire.Number(112), number)
84+ require.Equal(t, protowire.BytesType, wireType)
85+
86+ raw = mustEncodeClient(t, CancelPreview{Epoch: 7, Connection: connection, Generation: 3})
87+ number, _, _ = protowire.ConsumeTag(raw)
88+ require.Equal(t, protowire.Number(113), number)
89+
90+ raw = mustEncodeServer(t, testPreviewPublication(connection))
91+ number, wireType, consumed = protowire.ConsumeTag(raw)
92+ require.Greater(t, consumed, 0)
93+ require.Equal(t, protowire.Number(210), number)
94+ require.Equal(t, protowire.BytesType, wireType)
95+}
96+
97+// TestPreviewByteRoundTrip proves all three preview messages survive an
98+// encode/decode/re-encode cycle byte for byte, in both success and typed
99+// error publication shapes.
100+func TestPreviewByteRoundTrip(t *testing.T) {
101+ connection := testConnectionID(0x62)
102+ clients := []struct {
103+ name string
104+ message ClientMessage
105+ }{
106+ {"start_preview", testStartPreview(connection)},
107+ {"cancel_preview", CancelPreview{Epoch: 7, Connection: connection, Generation: 5}},
108+ }
109+ for _, tc := range clients {
110+ t.Run(tc.name, func(t *testing.T) {
111+ raw := mustEncodeClient(t, tc.message)
112+ decoded, err := DecodeClient(raw, testEnvelopeCeiling, testChunkCeiling)
113+ require.NoError(t, err)
114+ require.Equal(t, tc.message, decoded)
115+ again, err := EncodeClient(decoded, testEnvelopeCeiling, testChunkCeiling)
116+ require.NoError(t, err)
117+ require.Equal(t, raw, again, "re-encode must be byte-for-byte identical")
118+ })
119+ }
120+
121+ connection = testConnectionID(0x63)
122+ servers := []struct {
123+ name string
124+ message PreviewPublication
125+ }{
126+ {"success", testPreviewPublication(connection)},
127+ {"failed", PreviewPublication{
128+ Epoch: 7, Connection: connection, Generation: 4,
129+ Error: testErrorDetail(), HasError: true,
130+ }},
131+ }
132+ for _, tc := range servers {
133+ t.Run(tc.name, func(t *testing.T) {
134+ raw := mustEncodeServer(t, tc.message)
135+ decoded, err := DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
136+ require.NoError(t, err)
137+ require.Equal(t, tc.message, decoded)
138+ again, err := EncodeServer(decoded, testEnvelopeCeiling, testChunkCeiling)
139+ require.NoError(t, err)
140+ require.Equal(t, raw, again, "re-encode must be byte-for-byte identical")
141+ })
142+ }
143+}
144+
145+// TestPreviewRequestValidationParity proves the codec refuses exactly what
146+// ports.BrokerPreviewRequest.Validate refuses: a non-observation route, a
147+// route bound to another scope, a zero generation, and an invalid preview
148+// request. Scope-generation authority repeats on the route and the request.
149+func TestPreviewRequestValidationParity(t *testing.T) {
150+ connection := testConnectionID(0x64)
151+ base := testStartPreview(connection)
152+ cases := map[string]func() StartPreview{
153+ "attachment route": func() StartPreview {
154+ m := base
155+ m.Route.Purpose = ports.BrokerStreamAttachment
156+ m.Route.Admission = ports.BrokerAdmissionExact
157+ m.Route.Endpoint = "dev@host:22"
158+ m.Route.Registration = testRegistration()
159+ m.Route.Target = testTarget()
160+ m.Route.StartMode = ports.BrokerDaemonStartIfNeeded
161+ return m
162+ },
163+ "route epoch mismatch": func() StartPreview {
164+ m := base
165+ m.Route.Epoch++
166+ return m
167+ },
168+ "route connection mismatch": func() StartPreview {
169+ m := base
170+ m.Route.Connection = testConnectionID(0x99)
171+ return m
172+ },
173+ "zero generation": func() StartPreview {
174+ m := base
175+ m.Generation = 0
176+ return m
177+ },
178+ "invalid preview dimensions": func() StartPreview {
179+ m := base
180+ m.Preview.Width = 0
181+ return m
182+ },
183+ "stopped preview target": func() StartPreview {
184+ m := base
185+ m.Preview.Target.Stopped = true
186+ m.Preview.Target.LiveTabID = ""
187+ return m
188+ },
189+ }
190+ for name, mutate := range cases {
191+ t.Run(name, func(t *testing.T) {
192+ // The candidate must already fail the ports gate, so the codec
193+ // refusal is parity, not invention.
194+ require.Error(t, previewRouteRequest(mutate()).Validate())
195+ _, err := EncodeClient(mutate(), testEnvelopeCeiling, testChunkCeiling)
196+ require.ErrorIs(t, err, ErrInvalidMessage)
197+ })
198+ }
199+
200+ t.Run("mutated route purpose refused on decode", func(t *testing.T) {
201+ raw := mustEncodeClient(t, base)
202+ envelope := &wire.BrokerClientEnvelope{}
203+ require.NoError(t, wire.ScanEnvelope(envelope, raw))
204+ require.NoError(t, proto.Unmarshal(raw, envelope))
205+ envelope.GetStartPreview().GetRoute().Purpose = 1
206+ mutated, err := proto.Marshal(envelope)
207+ require.NoError(t, err)
208+ _, err = DecodeClient(mutated, testEnvelopeCeiling, testChunkCeiling)
209+ require.ErrorIs(t, err, ErrInvalidMessage)
210+ })
211+}
212+
213+// TestPreviewPublicationResultPresence proves exactly one of preview/error
214+// travels: a publication missing both results, setting a nil result
215+// member, or carrying a viewport alongside its error is refused, and a
216+// nonzero ErrorDetail without HasError is refused rather than dropped.
217+func TestPreviewPublicationResultPresence(t *testing.T) {
218+ connection := testConnectionID(0x65)
219+
220+ t.Run("absent result refused", func(t *testing.T) {
221+ message := &wire.PreviewPublication{
222+ Scope: &wire.BrokerScope{BrokerEpoch: 7, ConnectionId: connection[:]},
223+ Generation: 3,
224+ }
225+ raw, err := proto.Marshal(&wire.BrokerServerEnvelope{Payload: &wire.BrokerServerEnvelope_PreviewPublication{PreviewPublication: message}})
226+ require.NoError(t, err)
227+ _, err = DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
228+ require.ErrorIs(t, err, ErrInvalidMessage)
229+ })
230+
231+ t.Run("nil result members refused", func(t *testing.T) {
232+ // A nil oneof member fails structurally before semantic validation
233+ // selects its error: either refusal proves the payload never decodes.
234+ for _, message := range []*wire.PreviewPublication{
235+ {
236+ Scope: &wire.BrokerScope{BrokerEpoch: 7, ConnectionId: connection[:]},
237+ Generation: 3, Result: &wire.PreviewPublication_Preview{},
238+ },
239+ {
240+ Scope: &wire.BrokerScope{BrokerEpoch: 7, ConnectionId: connection[:]},
241+ Generation: 3, Result: &wire.PreviewPublication_Error{},
242+ },
243+ } {
244+ raw, err := proto.Marshal(&wire.BrokerServerEnvelope{Payload: &wire.BrokerServerEnvelope_PreviewPublication{PreviewPublication: message}})
245+ require.NoError(t, err)
246+ _, err = DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
247+ require.Error(t, err)
248+ }
249+ })
250+
251+ t.Run("error with viewport refused", func(t *testing.T) {
252+ published := PreviewPublication{
253+ Epoch: 7, Connection: connection, Generation: 3,
254+ Preview: testRemotePreview(), Error: testErrorDetail(), HasError: true,
255+ }
256+ _, err := EncodeServer(published, testEnvelopeCeiling, testChunkCeiling)
257+ require.ErrorIs(t, err, ErrInvalidMessage)
258+ })
259+
260+ t.Run("dropped detail refused", func(t *testing.T) {
261+ published := testPreviewPublication(connection)
262+ published.Error = testErrorDetail()
263+ _, err := EncodeServer(published, testEnvelopeCeiling, testChunkCeiling)
264+ require.ErrorIs(t, err, ErrInvalidMessage)
265+ })
266+}
267+
268+// TestPreviewNarrowingRefused proves every narrowing cast on the preview
269+// path is range-strict: values above uint16 aliasing a valid enum or
270+// dimension are refused before any cast.
271+func TestPreviewNarrowingRefused(t *testing.T) {
272+ connection := testConnectionID(0x66)
273+ base := testStartPreview(connection)
274+ raw := mustEncodeClient(t, base)
275+ envelope := &wire.BrokerClientEnvelope{}
276+ require.NoError(t, wire.ScanEnvelope(envelope, raw))
277+ require.NoError(t, proto.Unmarshal(raw, envelope))
278+
279+ mutate := func(apply func(*wire.BrokerClientEnvelope)) []byte {
280+ clone := proto.Clone(envelope).(*wire.BrokerClientEnvelope)
281+ apply(clone)
282+ mutated, err := proto.Marshal(clone)
283+ require.NoError(t, err)
284+ return mutated
285+ }
286+ for _, tc := range []struct {
287+ name string
288+ mutate func(*wire.BrokerClientEnvelope)
289+ }{
290+ // 257 aliases RemotePreviewUnavailable (1) under truncation.
291+ {"preview version", func(e *wire.BrokerClientEnvelope) { e.GetStartPreview().GetPreview().Version = 1<<16 + 1 }},
292+ {"preview width", func(e *wire.BrokerClientEnvelope) { e.GetStartPreview().GetPreview().Width = 1<<16 + 1 }},
293+ {"preview height", func(e *wire.BrokerClientEnvelope) { e.GetStartPreview().GetPreview().Height = 1<<16 + 80 }},
294+ } {
295+ t.Run(tc.name, func(t *testing.T) {
296+ _, err := DecodeClient(mutate(tc.mutate), testEnvelopeCeiling, testChunkCeiling)
297+ require.ErrorIs(t, err, errConvertRange)
298+ })
299+ }
300+
301+ published := mustEncodeServer(t, testPreviewPublication(connection))
302+ serverEnvelope := &wire.BrokerServerEnvelope{}
303+ require.NoError(t, wire.ScanEnvelope(serverEnvelope, published))
304+ require.NoError(t, proto.Unmarshal(published, serverEnvelope))
305+ serverEnvelope.GetPreviewPublication().GetPreview().Status = 1<<16 + 1
306+ mutated, err := proto.Marshal(serverEnvelope)
307+ require.NoError(t, err)
308+ _, err = DecodeServer(mutated, testEnvelopeCeiling, testChunkCeiling)
309+ require.ErrorIs(t, err, errConvertRange)
310+
311+ t.Run("truncated and trailing", func(t *testing.T) {
312+ for _, message := range []struct {
313+ name string
314+ encode func() []byte
315+ decode func([]byte) (any, error)
316+ }{
317+ {"start", func() []byte { return mustEncodeClient(t, base) }, func(raw []byte) (any, error) {
318+ return DecodeClient(raw, testEnvelopeCeiling, testChunkCeiling)
319+ }},
320+ {"publication", func() []byte { return mustEncodeServer(t, testPreviewPublication(connection)) }, func(raw []byte) (any, error) {
321+ return DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
322+ }},
323+ } {
324+ raw := message.encode()
325+ for size := range len(raw) {
326+ _, err := message.decode(raw[:size])
327+ require.Error(t, err, "%s prefix[:%d] accepted", message.name, size)
328+ }
329+ _, err := message.decode(append(append([]byte(nil), raw...), 0xFF))
330+ require.Error(t, err)
331+ }
332+ })
333+}
334+
335+// TestPreviewWrongDirection proves preview payloads fail closed across
336+// directions: client payloads never decode as server messages and the
337+// reverse.
338+func TestPreviewWrongDirection(t *testing.T) {
339+ connection := testConnectionID(0x67)
340+ _, err := EncodeServer(unknownServerMessage{}, testEnvelopeCeiling, testChunkCeiling)
341+ require.ErrorIs(t, err, ErrWrongDirection)
342+ _, err = EncodeClient(unknownClientMessage{}, testEnvelopeCeiling, testChunkCeiling)
343+ require.ErrorIs(t, err, ErrWrongDirection)
344+
345+ raw := mustEncodeClient(t, testStartPreview(connection))
346+ _, err = DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
347+ require.Error(t, err)
348+ raw = mustEncodeServer(t, testPreviewPublication(connection))
349+ _, err = DecodeClient(raw, testEnvelopeCeiling, testChunkCeiling)
350+ require.Error(t, err)
351+}
@@ -0,0 +1,351 @@
1+package brokerwire
2+
3+// Preview contract tests (broker preview slice 1).
4+//
5+// StartPreview carries tags 112 with scope, generation, an exact
6+// observation route, and the terminal preview request; CancelPreview
7+// carries tag 113 with scope and generation. PreviewPublication carries
8+// tag 210 with scope, generation, and exactly one of preview/error via
9+// its result oneof. These tests pin the stateless codec's half of that
10+// contract: frozen tags, byte round trips, the strict oneof presence
11+// rule, exact authority preservation, range-strict narrowing, and the
12+// ports preview validation parity (non-observation routes and
13+// scope-mismatched routes refused).
14+
15+import (
16+ "testing"
17+
18+ renderer "github.com/bnema/vev-vt"
19+ "google.golang.org/protobuf/encoding/protowire"
20+ "google.golang.org/protobuf/proto"
21+
22+ "github.com/bnema/vev/internal/domain"
23+ "github.com/bnema/vev/internal/ports"
24+ "github.com/bnema/vev/internal/protocol"
25+ "github.com/bnema/vev/internal/protocol/wire"
26+ "github.com/stretchr/testify/require"
27+)
28+
29+func testPreviewTarget() domain.RemoteSessionTarget {
30+ return domain.RemoteSessionTarget{
31+ Endpoint: "dev@host:22", DisplayOrigin: "dev@host",
32+ LifecycleID: domain.SessionLifecycleID{5, 6, 7},
33+ SessionName: "work", LiveTabID: "tab-1",
34+ }
35+}
36+
37+func testPreviewRequest() protocol.RemotePreviewRequest {
38+ return protocol.RemotePreviewRequest{
39+ Version: protocol.RemotePreviewSchemaVersion,
40+ Target: testPreviewTarget(),
41+ Width: 2, Height: 1,
42+ }
43+}
44+
45+func testPreviewRoute(connection ports.BrokerConnectionID) OpenStream {
46+ return OpenStream{
47+ Epoch: 7, Connection: connection, Stream: 9,
48+ Purpose: ports.BrokerStreamObservation, Local: true,
49+ Policy: testPolicy(), StartMode: ports.BrokerDaemonExistingOnly,
50+ }
51+}
52+
53+func testStartPreview(connection ports.BrokerConnectionID) StartPreview {
54+ return StartPreview{
55+ Epoch: 7, Connection: connection, Generation: 3,
56+ Route: testPreviewRoute(connection), Preview: testPreviewRequest(),
57+ }
58+}
59+
60+func testRemotePreview() protocol.RemotePreview {
61+ return protocol.RemotePreview{
62+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
63+ LifecycleID: testPreviewTarget().LifecycleID, TabID: testPreviewTarget().LiveTabID,
64+ Revision: 11, Width: 2, Height: 1,
65+ Cells: []renderer.Cell{{Rune: 'a'}, {Rune: 'b'}},
66+ }
67+}
68+
69+func testPreviewPublication(connection ports.BrokerConnectionID) PreviewPublication {
70+ return PreviewPublication{
71+ Epoch: 7, Connection: connection, Generation: 3,
72+ Preview: testRemotePreview(),
73+ }
74+}
75+
76+// TestPreviewVariantTags pins the frozen envelope tags: start_preview 112,
77+// cancel_preview 113, preview_publication 210.
78+func TestPreviewVariantTags(t *testing.T) {
79+ connection := testConnectionID(0x61)
80+ raw := mustEncodeClient(t, testStartPreview(connection))
81+ number, wireType, consumed := protowire.ConsumeTag(raw)
82+ require.Greater(t, consumed, 0)
83+ require.Equal(t, protowire.Number(112), number)
84+ require.Equal(t, protowire.BytesType, wireType)
85+
86+ raw = mustEncodeClient(t, CancelPreview{Epoch: 7, Connection: connection, Generation: 3})
87+ number, _, _ = protowire.ConsumeTag(raw)
88+ require.Equal(t, protowire.Number(113), number)
89+
90+ raw = mustEncodeServer(t, testPreviewPublication(connection))
91+ number, wireType, consumed = protowire.ConsumeTag(raw)
92+ require.Greater(t, consumed, 0)
93+ require.Equal(t, protowire.Number(210), number)
94+ require.Equal(t, protowire.BytesType, wireType)
95+}
96+
97+// TestPreviewByteRoundTrip proves all three preview messages survive an
98+// encode/decode/re-encode cycle byte for byte, in both success and typed
99+// error publication shapes.
100+func TestPreviewByteRoundTrip(t *testing.T) {
101+ connection := testConnectionID(0x62)
102+ clients := []struct {
103+ name string
104+ message ClientMessage
105+ }{
106+ {"start_preview", testStartPreview(connection)},
107+ {"cancel_preview", CancelPreview{Epoch: 7, Connection: connection, Generation: 5}},
108+ }
109+ for _, tc := range clients {
110+ t.Run(tc.name, func(t *testing.T) {
111+ raw := mustEncodeClient(t, tc.message)
112+ decoded, err := DecodeClient(raw, testEnvelopeCeiling, testChunkCeiling)
113+ require.NoError(t, err)
114+ require.Equal(t, tc.message, decoded)
115+ again, err := EncodeClient(decoded, testEnvelopeCeiling, testChunkCeiling)
116+ require.NoError(t, err)
117+ require.Equal(t, raw, again, "re-encode must be byte-for-byte identical")
118+ })
119+ }
120+
121+ connection = testConnectionID(0x63)
122+ servers := []struct {
123+ name string
124+ message PreviewPublication
125+ }{
126+ {"success", testPreviewPublication(connection)},
127+ {"failed", PreviewPublication{
128+ Epoch: 7, Connection: connection, Generation: 4,
129+ Error: testErrorDetail(), HasError: true,
130+ }},
131+ }
132+ for _, tc := range servers {
133+ t.Run(tc.name, func(t *testing.T) {
134+ raw := mustEncodeServer(t, tc.message)
135+ decoded, err := DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
136+ require.NoError(t, err)
137+ require.Equal(t, tc.message, decoded)
138+ again, err := EncodeServer(decoded, testEnvelopeCeiling, testChunkCeiling)
139+ require.NoError(t, err)
140+ require.Equal(t, raw, again, "re-encode must be byte-for-byte identical")
141+ })
142+ }
143+}
144+
145+// TestPreviewRequestValidationParity proves the codec refuses exactly what
146+// ports.BrokerPreviewRequest.Validate refuses: a non-observation route, a
147+// route bound to another scope, a zero generation, and an invalid preview
148+// request. Scope-generation authority repeats on the route and the request.
149+func TestPreviewRequestValidationParity(t *testing.T) {
150+ connection := testConnectionID(0x64)
151+ base := testStartPreview(connection)
152+ cases := map[string]func() StartPreview{
153+ "attachment route": func() StartPreview {
154+ m := base
155+ m.Route.Purpose = ports.BrokerStreamAttachment
156+ m.Route.Admission = ports.BrokerAdmissionExact
157+ m.Route.Endpoint = "dev@host:22"
158+ m.Route.Registration = testRegistration()
159+ m.Route.Target = testTarget()
160+ m.Route.StartMode = ports.BrokerDaemonStartIfNeeded
161+ return m
162+ },
163+ "route epoch mismatch": func() StartPreview {
164+ m := base
165+ m.Route.Epoch++
166+ return m
167+ },
168+ "route connection mismatch": func() StartPreview {
169+ m := base
170+ m.Route.Connection = testConnectionID(0x99)
171+ return m
172+ },
173+ "zero generation": func() StartPreview {
174+ m := base
175+ m.Generation = 0
176+ return m
177+ },
178+ "invalid preview dimensions": func() StartPreview {
179+ m := base
180+ m.Preview.Width = 0
181+ return m
182+ },
183+ "stopped preview target": func() StartPreview {
184+ m := base
185+ m.Preview.Target.Stopped = true
186+ m.Preview.Target.LiveTabID = ""
187+ return m
188+ },
189+ }
190+ for name, mutate := range cases {
191+ t.Run(name, func(t *testing.T) {
192+ // The candidate must already fail the ports gate, so the codec
193+ // refusal is parity, not invention.
194+ require.Error(t, previewRouteRequest(mutate()).Validate())
195+ _, err := EncodeClient(mutate(), testEnvelopeCeiling, testChunkCeiling)
196+ require.ErrorIs(t, err, ErrInvalidMessage)
197+ })
198+ }
199+
200+ t.Run("mutated route purpose refused on decode", func(t *testing.T) {
201+ raw := mustEncodeClient(t, base)
202+ envelope := &wire.BrokerClientEnvelope{}
203+ require.NoError(t, wire.ScanEnvelope(envelope, raw))
204+ require.NoError(t, proto.Unmarshal(raw, envelope))
205+ envelope.GetStartPreview().GetRoute().Purpose = 1
206+ mutated, err := proto.Marshal(envelope)
207+ require.NoError(t, err)
208+ _, err = DecodeClient(mutated, testEnvelopeCeiling, testChunkCeiling)
209+ require.ErrorIs(t, err, ErrInvalidMessage)
210+ })
211+}
212+
213+// TestPreviewPublicationResultPresence proves exactly one of preview/error
214+// travels: a publication missing both results, setting a nil result
215+// member, or carrying a viewport alongside its error is refused, and a
216+// nonzero ErrorDetail without HasError is refused rather than dropped.
217+func TestPreviewPublicationResultPresence(t *testing.T) {
218+ connection := testConnectionID(0x65)
219+
220+ t.Run("absent result refused", func(t *testing.T) {
221+ message := &wire.PreviewPublication{
222+ Scope: &wire.BrokerScope{BrokerEpoch: 7, ConnectionId: connection[:]},
223+ Generation: 3,
224+ }
225+ raw, err := proto.Marshal(&wire.BrokerServerEnvelope{Payload: &wire.BrokerServerEnvelope_PreviewPublication{PreviewPublication: message}})
226+ require.NoError(t, err)
227+ _, err = DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
228+ require.ErrorIs(t, err, ErrInvalidMessage)
229+ })
230+
231+ t.Run("nil result members refused", func(t *testing.T) {
232+ // A nil oneof member fails structurally before semantic validation
233+ // selects its error: either refusal proves the payload never decodes.
234+ for _, message := range []*wire.PreviewPublication{
235+ {
236+ Scope: &wire.BrokerScope{BrokerEpoch: 7, ConnectionId: connection[:]},
237+ Generation: 3, Result: &wire.PreviewPublication_Preview{},
238+ },
239+ {
240+ Scope: &wire.BrokerScope{BrokerEpoch: 7, ConnectionId: connection[:]},
241+ Generation: 3, Result: &wire.PreviewPublication_Error{},
242+ },
243+ } {
244+ raw, err := proto.Marshal(&wire.BrokerServerEnvelope{Payload: &wire.BrokerServerEnvelope_PreviewPublication{PreviewPublication: message}})
245+ require.NoError(t, err)
246+ _, err = DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
247+ require.Error(t, err)
248+ }
249+ })
250+
251+ t.Run("error with viewport refused", func(t *testing.T) {
252+ published := PreviewPublication{
253+ Epoch: 7, Connection: connection, Generation: 3,
254+ Preview: testRemotePreview(), Error: testErrorDetail(), HasError: true,
255+ }
256+ _, err := EncodeServer(published, testEnvelopeCeiling, testChunkCeiling)
257+ require.ErrorIs(t, err, ErrInvalidMessage)
258+ })
259+
260+ t.Run("dropped detail refused", func(t *testing.T) {
261+ published := testPreviewPublication(connection)
262+ published.Error = testErrorDetail()
263+ _, err := EncodeServer(published, testEnvelopeCeiling, testChunkCeiling)
264+ require.ErrorIs(t, err, ErrInvalidMessage)
265+ })
266+}
267+
268+// TestPreviewNarrowingRefused proves every narrowing cast on the preview
269+// path is range-strict: values above uint16 aliasing a valid enum or
270+// dimension are refused before any cast.
271+func TestPreviewNarrowingRefused(t *testing.T) {
272+ connection := testConnectionID(0x66)
273+ base := testStartPreview(connection)
274+ raw := mustEncodeClient(t, base)
275+ envelope := &wire.BrokerClientEnvelope{}
276+ require.NoError(t, wire.ScanEnvelope(envelope, raw))
277+ require.NoError(t, proto.Unmarshal(raw, envelope))
278+
279+ mutate := func(apply func(*wire.BrokerClientEnvelope)) []byte {
280+ clone := proto.Clone(envelope).(*wire.BrokerClientEnvelope)
281+ apply(clone)
282+ mutated, err := proto.Marshal(clone)
283+ require.NoError(t, err)
284+ return mutated
285+ }
286+ for _, tc := range []struct {
287+ name string
288+ mutate func(*wire.BrokerClientEnvelope)
289+ }{
290+ // 257 aliases RemotePreviewUnavailable (1) under truncation.
291+ {"preview version", func(e *wire.BrokerClientEnvelope) { e.GetStartPreview().GetPreview().Version = 1<<16 + 1 }},
292+ {"preview width", func(e *wire.BrokerClientEnvelope) { e.GetStartPreview().GetPreview().Width = 1<<16 + 1 }},
293+ {"preview height", func(e *wire.BrokerClientEnvelope) { e.GetStartPreview().GetPreview().Height = 1<<16 + 80 }},
294+ } {
295+ t.Run(tc.name, func(t *testing.T) {
296+ _, err := DecodeClient(mutate(tc.mutate), testEnvelopeCeiling, testChunkCeiling)
297+ require.ErrorIs(t, err, errConvertRange)
298+ })
299+ }
300+
301+ published := mustEncodeServer(t, testPreviewPublication(connection))
302+ serverEnvelope := &wire.BrokerServerEnvelope{}
303+ require.NoError(t, wire.ScanEnvelope(serverEnvelope, published))
304+ require.NoError(t, proto.Unmarshal(published, serverEnvelope))
305+ serverEnvelope.GetPreviewPublication().GetPreview().Status = 1<<16 + 1
306+ mutated, err := proto.Marshal(serverEnvelope)
307+ require.NoError(t, err)
308+ _, err = DecodeServer(mutated, testEnvelopeCeiling, testChunkCeiling)
309+ require.ErrorIs(t, err, errConvertRange)
310+
311+ t.Run("truncated and trailing", func(t *testing.T) {
312+ for _, message := range []struct {
313+ name string
314+ encode func() []byte
315+ decode func([]byte) (any, error)
316+ }{
317+ {"start", func() []byte { return mustEncodeClient(t, base) }, func(raw []byte) (any, error) {
318+ return DecodeClient(raw, testEnvelopeCeiling, testChunkCeiling)
319+ }},
320+ {"publication", func() []byte { return mustEncodeServer(t, testPreviewPublication(connection)) }, func(raw []byte) (any, error) {
321+ return DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
322+ }},
323+ } {
324+ raw := message.encode()
325+ for size := range len(raw) {
326+ _, err := message.decode(raw[:size])
327+ require.Error(t, err, "%s prefix[:%d] accepted", message.name, size)
328+ }
329+ _, err := message.decode(append(append([]byte(nil), raw...), 0xFF))
330+ require.Error(t, err)
331+ }
332+ })
333+}
334+
335+// TestPreviewWrongDirection proves preview payloads fail closed across
336+// directions: client payloads never decode as server messages and the
337+// reverse.
338+func TestPreviewWrongDirection(t *testing.T) {
339+ connection := testConnectionID(0x67)
340+ _, err := EncodeServer(unknownServerMessage{}, testEnvelopeCeiling, testChunkCeiling)
341+ require.ErrorIs(t, err, ErrWrongDirection)
342+ _, err = EncodeClient(unknownClientMessage{}, testEnvelopeCeiling, testChunkCeiling)
343+ require.ErrorIs(t, err, ErrWrongDirection)
344+
345+ raw := mustEncodeClient(t, testStartPreview(connection))
346+ _, err = DecodeServer(raw, testEnvelopeCeiling, testChunkCeiling)
347+ require.Error(t, err)
348+ raw = mustEncodeServer(t, testPreviewPublication(connection))
349+ _, err = DecodeClient(raw, testEnvelopeCeiling, testChunkCeiling)
350+ require.Error(t, err)
351+}
M internal/adapters/sessionwire/picker_interaction_test.go +0 -128
@@ -1,11 +1,8 @@
11 package sessionwire
22
33 import (
4- "math"
54 "testing"
65
7- "google.golang.org/protobuf/proto"
8-
96 renderer "github.com/bnema/vev-vt"
107 "github.com/bnema/vev/internal/protocol"
118 "github.com/bnema/vev/internal/protocol/wire"
@@ -54,40 +51,20 @@ func pickerWirePreview() protocol.PickerPreview {
5451 }
5552 }
5653
57-func pickerWireControlRequest() protocol.PickerControlRequest {
58- return protocol.PickerControlRequest{
59- Version: protocol.Version, RequestID: 4, Operation: protocol.PickerControlResolve,
60- SourceRevision: 2, SourceID: "serving", Key: "ab12/work#tab-1",
61- }
62-}
63-
64-func pickerWireControlResponse() protocol.PickerControlResponse {
65- snapshot := protocol.NormalizePickerSnapshot(pickerWireSnapshot())
66- return protocol.PickerControlResponse{
67- RequestID: 4, Operation: protocol.PickerControlSnapshot, Status: protocol.PickerSourceOK, Snapshot: &snapshot,
68- }
69-}
70-
7154 func TestPickerClientMessagesEncodeWithTypes(t *testing.T) {
72- begin := protocol.PickerBegin{RequestID: 3, Intent: protocol.PickerIntentNavigation}
7355 closeMessage := protocol.PickerClose{InteractionID: 7, RequestID: 3}
7456 selection := pickerWireSelection()
7557 previewRequest := pickerWirePreviewRequest()
76- controlRequest := pickerWireControlRequest()
7758 tests := []struct {
7859 name string
7960 message protocol.ClientMessage
8061 }{
81- {name: "begin", message: begin},
82- {name: "begin pointer", message: &begin},
8362 {name: "close", message: closeMessage},
8463 {name: "close pointer", message: &closeMessage},
8564 {name: "selection", message: selection},
8665 {name: "selection pointer", message: &selection},
8766 {name: "preview request", message: previewRequest},
8867 {name: "preview request pointer", message: &previewRequest},
89- {name: "control request", message: controlRequest},
90- {name: "control request pointer", message: &controlRequest},
9168 }
9269 for _, tt := range tests {
9370 t.Run(tt.name, func(t *testing.T) {
@@ -96,16 +73,12 @@ func TestPickerClientMessagesEncodeWithTypes(t *testing.T) {
9673 got, err := DecodeClientEnvelope(mustSingleAppPayload(t, raw))
9774 require.NoError(t, err)
9875 switch message := tt.message.(type) {
99- case *protocol.PickerBegin:
100- require.Equal(t, *message, got)
10176 case *protocol.PickerClose:
10277 require.Equal(t, *message, got)
10378 case *protocol.PickerSelection:
10479 require.Equal(t, *message, got)
10580 case *protocol.PickerPreviewRequest:
10681 require.Equal(t, *message, got)
107- case *protocol.PickerControlRequest:
108- require.Equal(t, *message, got)
10982 default:
11083 require.Equal(t, tt.message, got)
11184 }
@@ -178,105 +151,14 @@ func TestPickerSnapshotNormalizesIntoProjections(t *testing.T) {
178151 }
179152 }
180153
181-func TestPickerControlMessagesRoundTripThroughConnections(t *testing.T) {
182- request := pickerWireControlRequest()
183- response := pickerWireControlResponse()
184- snapshot := pickerWireSnapshot()
185- wanted := protocol.PickerControlResponse{
186- RequestID: response.RequestID, Operation: response.Operation, Status: response.Status,
187- Snapshot: &protocol.PickerSnapshot{
188- InteractionID: snapshot.InteractionID, SourceID: snapshot.SourceID,
189- SourceRevision: snapshot.SourceRevision, Status: snapshot.Status,
190- Lines: snapshot.Lines, Cursor: snapshot.Cursor,
191- Recent: snapshot.Recent, Grouped: snapshot.Grouped,
192- },
193- }
194-
195- for _, tc := range []struct {
196- name string
197- message protocol.ClientMessage
198- }{
199- {name: "request", message: request},
200- {name: "request pointer", message: &request},
201- } {
202- t.Run(tc.name, func(t *testing.T) {
203- clientRaw := &scriptedTransport{recv: []wire.Envelope{mustPreambleResponse(t)}}
204- require.NoError(t, NewClientConnection(clientRaw).SendClient(tc.message))
205- serverRaw := &scriptedTransport{recv: mustServerPreambleQueue(t, mustSingleAppPayload(t, clientRaw))}
206- got, err := NewServerConnection(serverRaw).ReceiveClient()
207- require.NoError(t, err)
208- require.Equal(t, request, got)
209- })
210- }
211-
212- for _, tc := range []struct {
213- name string
214- message protocol.ServerMessage
215- }{
216- {name: "response", message: response},
217- {name: "response pointer", message: &response},
218- } {
219- t.Run(tc.name, func(t *testing.T) {
220- serverRaw := &scriptedTransport{}
221- require.NoError(t, NewServerConnection(serverRaw).SendServer(tc.message))
222- require.Equal(t, 1, serverRaw.sentLen())
223- clientRaw := &scriptedTransport{recv: mustClientPreambleQueue(t, serverRaw.sentPayload(0))}
224- got, err := NewClientConnection(clientRaw).ReceiveServer()
225- require.NoError(t, err)
226- require.Equal(t, wanted, got)
227- })
228- }
229-}
230-
231-func TestPickerControlRequestMalformedPreHandshakeClassification(t *testing.T) {
232- converted, err := pickerControlRequestToWire(pickerWireControlRequest())
233- require.NoError(t, err)
234- valid, err := proto.Marshal(&wire.ClientEnvelope{
235- Payload: &wire.ClientEnvelope_PickerControlRequest{PickerControlRequest: converted},
236- })
237- require.NoError(t, err)
238- tests := []struct {
239- name string
240- payload []byte
241- version uint16
242- }{
243- {name: "truncated version", payload: valid[:1]},
244- {name: "oversized version", payload: pickerControlRequestWithVersion(t, math.MaxUint16+1)},
245- }
246- for _, tt := range tests {
247- t.Run(tt.name, func(t *testing.T) {
248- raw := &scriptedTransport{recv: mustServerPreambleQueue(t, tt.payload)}
249- _, err := NewServerConnection(raw).ReceiveClient()
250- var failure *protocol.DecodeFailure
251- require.ErrorAs(t, err, &failure)
252- require.Equal(t, protocol.DecodeMalformed, failure.Category)
253- require.Equal(t, tt.version, failure.Version)
254- })
255- }
256-}
257-
258-func pickerControlRequestWithVersion(t *testing.T, version uint32) []byte {
259- t.Helper()
260- converted, err := pickerControlRequestToWire(pickerWireControlRequest())
261- require.NoError(t, err)
262- converted.Version = version
263- raw, err := proto.Marshal(&wire.ClientEnvelope{
264- Payload: &wire.ClientEnvelope_PickerControlRequest{PickerControlRequest: converted},
265- })
266- require.NoError(t, err)
267- return raw
268-}
269-
270154 func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {
271155 offer := pickerWireOffer()
272156 snapshot := pickerWireSnapshot()
273157 selection := pickerWireSelection()
274158 clientMessages := []protocol.ClientMessage{
275- protocol.PickerBegin{RequestID: 1, Intent: protocol.PickerIntentNavigation},
276159 protocol.PickerClose{InteractionID: 1},
277160 selection,
278161 pickerWirePreviewRequest(),
279- pickerWireControlRequest(),
280162 }
281163 serverMessages := []protocol.ServerMessage{
282164 offer,
@@ -285,7 +167,6 @@ func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {
285167 protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "a/b", Action: protocol.PickerActionKill},
286168 protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionKill, Code: protocol.PickerUnknownKey},
287169 pickerWirePreview(),
288- pickerWireControlResponse(),
289170 }
290171 // Cross-direction decodability is structural, not directional: field
291172 // numbers collide across envelopes, so a client payload may parse as
@@ -315,15 +196,6 @@ func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {
315196 require.Equal(t, snapshot.Grouped, decoded.Grouped)
316197 continue
317198 }
318- if response, ok := message.(protocol.PickerControlResponse); ok {
319- decoded, ok := got.(protocol.PickerControlResponse)
320- require.True(t, ok, "got %T", got)
321- require.Equal(t, response.RequestID, decoded.RequestID)
322- require.Equal(t, response.Operation, decoded.Operation)
323- require.Equal(t, response.Status, decoded.Status)
324- require.NotNil(t, decoded.Snapshot)
325- continue
326- }
327199 require.Equal(t, message, got)
328200 }
329201 }
@@ -1,11 +1,8 @@
1 package sessionwire1 package sessionwire
2 2
3 import (3 import (
4- "math"
5 "testing"4 "testing"
6 5
7- "google.golang.org/protobuf/proto"
8-
9 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
10 "github.com/bnema/vev/internal/protocol"7 "github.com/bnema/vev/internal/protocol"
11 "github.com/bnema/vev/internal/protocol/wire"8 "github.com/bnema/vev/internal/protocol/wire"
@@ -54,40 +51,20 @@ func pickerWirePreview() protocol.PickerPreview {
54 }51 }
55 }52 }
56 53
57-func pickerWireControlRequest() protocol.PickerControlRequest {
58- return protocol.PickerControlRequest{
59- Version: protocol.Version, RequestID: 4, Operation: protocol.PickerControlResolve,
60- SourceRevision: 2, SourceID: "serving", Key: "ab12/work#tab-1",
61- }
62-}
63-
64-func pickerWireControlResponse() protocol.PickerControlResponse {
65- snapshot := protocol.NormalizePickerSnapshot(pickerWireSnapshot())
66- return protocol.PickerControlResponse{
67- RequestID: 4, Operation: protocol.PickerControlSnapshot, Status: protocol.PickerSourceOK, Snapshot: &snapshot,
68- }
69-}
70-
71 func TestPickerClientMessagesEncodeWithTypes(t *testing.T) {54 func TestPickerClientMessagesEncodeWithTypes(t *testing.T) {
72- begin := protocol.PickerBegin{RequestID: 3, Intent: protocol.PickerIntentNavigation}
73 closeMessage := protocol.PickerClose{InteractionID: 7, RequestID: 3}55 closeMessage := protocol.PickerClose{InteractionID: 7, RequestID: 3}
74 selection := pickerWireSelection()56 selection := pickerWireSelection()
75 previewRequest := pickerWirePreviewRequest()57 previewRequest := pickerWirePreviewRequest()
76- controlRequest := pickerWireControlRequest()
77 tests := []struct {58 tests := []struct {
78 name string59 name string
79 message protocol.ClientMessage60 message protocol.ClientMessage
80 }{61 }{
81- {name: "begin", message: begin},
82- {name: "begin pointer", message: &begin},
83 {name: "close", message: closeMessage},62 {name: "close", message: closeMessage},
84 {name: "close pointer", message: &closeMessage},63 {name: "close pointer", message: &closeMessage},
85 {name: "selection", message: selection},64 {name: "selection", message: selection},
86 {name: "selection pointer", message: &selection},65 {name: "selection pointer", message: &selection},
87 {name: "preview request", message: previewRequest},66 {name: "preview request", message: previewRequest},
88 {name: "preview request pointer", message: &previewRequest},67 {name: "preview request pointer", message: &previewRequest},
89- {name: "control request", message: controlRequest},
90- {name: "control request pointer", message: &controlRequest},
91 }68 }
92 for _, tt := range tests {69 for _, tt := range tests {
93 t.Run(tt.name, func(t *testing.T) {70 t.Run(tt.name, func(t *testing.T) {
@@ -96,16 +73,12 @@ func TestPickerClientMessagesEncodeWithTypes(t *testing.T) {
96 got, err := DecodeClientEnvelope(mustSingleAppPayload(t, raw))73 got, err := DecodeClientEnvelope(mustSingleAppPayload(t, raw))
97 require.NoError(t, err)74 require.NoError(t, err)
98 switch message := tt.message.(type) {75 switch message := tt.message.(type) {
99- case *protocol.PickerBegin:
100- require.Equal(t, *message, got)
101 case *protocol.PickerClose:76 case *protocol.PickerClose:
102 require.Equal(t, *message, got)77 require.Equal(t, *message, got)
103 case *protocol.PickerSelection:78 case *protocol.PickerSelection:
104 require.Equal(t, *message, got)79 require.Equal(t, *message, got)
105 case *protocol.PickerPreviewRequest:80 case *protocol.PickerPreviewRequest:
106 require.Equal(t, *message, got)81 require.Equal(t, *message, got)
107- case *protocol.PickerControlRequest:
108- require.Equal(t, *message, got)
109 default:82 default:
110 require.Equal(t, tt.message, got)83 require.Equal(t, tt.message, got)
111 }84 }
@@ -178,105 +151,14 @@ func TestPickerSnapshotNormalizesIntoProjections(t *testing.T) {
178 }151 }
179 }152 }
180 153
181-func TestPickerControlMessagesRoundTripThroughConnections(t *testing.T) {
182- request := pickerWireControlRequest()
183- response := pickerWireControlResponse()
184- snapshot := pickerWireSnapshot()
185- wanted := protocol.PickerControlResponse{
186- RequestID: response.RequestID, Operation: response.Operation, Status: response.Status,
187- Snapshot: &protocol.PickerSnapshot{
188- InteractionID: snapshot.InteractionID, SourceID: snapshot.SourceID,
189- SourceRevision: snapshot.SourceRevision, Status: snapshot.Status,
190- Lines: snapshot.Lines, Cursor: snapshot.Cursor,
191- Recent: snapshot.Recent, Grouped: snapshot.Grouped,
192- },
193- }
194-
195- for _, tc := range []struct {
196- name string
197- message protocol.ClientMessage
198- }{
199- {name: "request", message: request},
200- {name: "request pointer", message: &request},
201- } {
202- t.Run(tc.name, func(t *testing.T) {
203- clientRaw := &scriptedTransport{recv: []wire.Envelope{mustPreambleResponse(t)}}
204- require.NoError(t, NewClientConnection(clientRaw).SendClient(tc.message))
205- serverRaw := &scriptedTransport{recv: mustServerPreambleQueue(t, mustSingleAppPayload(t, clientRaw))}
206- got, err := NewServerConnection(serverRaw).ReceiveClient()
207- require.NoError(t, err)
208- require.Equal(t, request, got)
209- })
210- }
211-
212- for _, tc := range []struct {
213- name string
214- message protocol.ServerMessage
215- }{
216- {name: "response", message: response},
217- {name: "response pointer", message: &response},
218- } {
219- t.Run(tc.name, func(t *testing.T) {
220- serverRaw := &scriptedTransport{}
221- require.NoError(t, NewServerConnection(serverRaw).SendServer(tc.message))
222- require.Equal(t, 1, serverRaw.sentLen())
223- clientRaw := &scriptedTransport{recv: mustClientPreambleQueue(t, serverRaw.sentPayload(0))}
224- got, err := NewClientConnection(clientRaw).ReceiveServer()
225- require.NoError(t, err)
226- require.Equal(t, wanted, got)
227- })
228- }
229-}
230-
231-func TestPickerControlRequestMalformedPreHandshakeClassification(t *testing.T) {
232- converted, err := pickerControlRequestToWire(pickerWireControlRequest())
233- require.NoError(t, err)
234- valid, err := proto.Marshal(&wire.ClientEnvelope{
235- Payload: &wire.ClientEnvelope_PickerControlRequest{PickerControlRequest: converted},
236- })
237- require.NoError(t, err)
238- tests := []struct {
239- name string
240- payload []byte
241- version uint16
242- }{
243- {name: "truncated version", payload: valid[:1]},
244- {name: "oversized version", payload: pickerControlRequestWithVersion(t, math.MaxUint16+1)},
245- }
246- for _, tt := range tests {
247- t.Run(tt.name, func(t *testing.T) {
248- raw := &scriptedTransport{recv: mustServerPreambleQueue(t, tt.payload)}
249- _, err := NewServerConnection(raw).ReceiveClient()
250- var failure *protocol.DecodeFailure
251- require.ErrorAs(t, err, &failure)
252- require.Equal(t, protocol.DecodeMalformed, failure.Category)
253- require.Equal(t, tt.version, failure.Version)
254- })
255- }
256-}
257-
258-func pickerControlRequestWithVersion(t *testing.T, version uint32) []byte {
259- t.Helper()
260- converted, err := pickerControlRequestToWire(pickerWireControlRequest())
261- require.NoError(t, err)
262- converted.Version = version
263- raw, err := proto.Marshal(&wire.ClientEnvelope{
264- Payload: &wire.ClientEnvelope_PickerControlRequest{PickerControlRequest: converted},
265- })
266- require.NoError(t, err)
267- return raw
268-}
269-
270 func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {154 func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {
271 offer := pickerWireOffer()155 offer := pickerWireOffer()
272 snapshot := pickerWireSnapshot()156 snapshot := pickerWireSnapshot()
273 selection := pickerWireSelection()157 selection := pickerWireSelection()
274 clientMessages := []protocol.ClientMessage{158 clientMessages := []protocol.ClientMessage{
275- protocol.PickerBegin{RequestID: 1, Intent: protocol.PickerIntentNavigation},
276 protocol.PickerClose{InteractionID: 1},159 protocol.PickerClose{InteractionID: 1},
277 selection,160 selection,
278 pickerWirePreviewRequest(),161 pickerWirePreviewRequest(),
279- pickerWireControlRequest(),
280 }162 }
281 serverMessages := []protocol.ServerMessage{163 serverMessages := []protocol.ServerMessage{
282 offer,164 offer,
@@ -285,7 +167,6 @@ func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {
285 protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "a/b", Action: protocol.PickerActionKill},167 protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "a/b", Action: protocol.PickerActionKill},
286 protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionKill, Code: protocol.PickerUnknownKey},168 protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionKill, Code: protocol.PickerUnknownKey},
287 pickerWirePreview(),169 pickerWirePreview(),
288- pickerWireControlResponse(),
289 }170 }
290 // Cross-direction decodability is structural, not directional: field171 // Cross-direction decodability is structural, not directional: field
291 // numbers collide across envelopes, so a client payload may parse as172 // numbers collide across envelopes, so a client payload may parse as
@@ -315,15 +196,6 @@ func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) {
315 require.Equal(t, snapshot.Grouped, decoded.Grouped)196 require.Equal(t, snapshot.Grouped, decoded.Grouped)
316 continue197 continue
317 }198 }
318- if response, ok := message.(protocol.PickerControlResponse); ok {
319- decoded, ok := got.(protocol.PickerControlResponse)
320- require.True(t, ok, "got %T", got)
321- require.Equal(t, response.RequestID, decoded.RequestID)
322- require.Equal(t, response.Operation, decoded.Operation)
323- require.Equal(t, response.Status, decoded.Status)
324- require.NotNil(t, decoded.Snapshot)
325- continue
326- }
327 require.Equal(t, message, got)199 require.Equal(t, message, got)
328 }200 }
329 }201 }
M internal/adapters/sessionwire/proto_convert_navigation.go +0 -50
@@ -375,53 +375,3 @@ func pickerSnapshotFromWire(message *wire.PickerSnapshot) (protocol.PickerSnapsh
375375 snapshot.Cursor = snapshot.Recent.Cursor
376376 return snapshot, nil
377377 }
378-
379-func pickerControlRequestToWire(message protocol.PickerControlRequest) (*wire.PickerControlRequest, error) {
380- if err := protocol.ValidatePickerControlRequest(message); err != nil {
381- return nil, err
382- }
383- out := &wire.PickerControlRequest{
384- Version: uint32(message.Version),
385- RequestId: message.RequestID,
386- Operation: uint32(message.Operation),
387- SourceRevision: message.SourceRevision,
388- SourceId: message.SourceID,
389- Key: message.Key,
390- }
391- for _, target := range message.Targets {
392- target := target
393- out.Targets = append(out.Targets, exactTargetToWire(&target))
394- }
395- return out, nil
396-}
397-
398-func pickerControlRequestFromWire(message *wire.PickerControlRequest) (protocol.PickerControlRequest, error) {
399- var request protocol.PickerControlRequest
400- if message == nil {
401- return request, protocol.ErrInvalidNavigation
402- }
403- version, err := mustUint16(message.GetVersion())
404- if err != nil {
405- return protocol.PickerControlRequest{}, protocol.ErrInvalidNavigation
406- }
407- request.Version = version
408- request.RequestID = message.GetRequestId()
409- request.Operation, err = enum8[protocol.PickerControlOperation](message.GetOperation())
410- if err != nil {
411- return protocol.PickerControlRequest{}, protocol.ErrInvalidNavigation
412- }
413- request.SourceRevision = message.GetSourceRevision()
414- request.SourceID = message.GetSourceId()
415- request.Key = message.GetKey()
416- for _, target := range message.GetTargets() {
417- converted, err := exactTargetFromWire(target)
418- if err != nil || converted == nil {
419- return protocol.PickerControlRequest{}, protocol.ErrInvalidNavigation
420- }
421- request.Targets = append(request.Targets, *converted)
422- }
423- if err := protocol.ValidatePickerControlRequest(request); err != nil {
424- return protocol.PickerControlRequest{}, err
425- }
426- return request, nil
427-}
@@ -375,53 +375,3 @@ func pickerSnapshotFromWire(message *wire.PickerSnapshot) (protocol.PickerSnapsh
375 snapshot.Cursor = snapshot.Recent.Cursor375 snapshot.Cursor = snapshot.Recent.Cursor
376 return snapshot, nil376 return snapshot, nil
377 }377 }
378-
379-func pickerControlRequestToWire(message protocol.PickerControlRequest) (*wire.PickerControlRequest, error) {
380- if err := protocol.ValidatePickerControlRequest(message); err != nil {
381- return nil, err
382- }
383- out := &wire.PickerControlRequest{
384- Version: uint32(message.Version),
385- RequestId: message.RequestID,
386- Operation: uint32(message.Operation),
387- SourceRevision: message.SourceRevision,
388- SourceId: message.SourceID,
389- Key: message.Key,
390- }
391- for _, target := range message.Targets {
392- target := target
393- out.Targets = append(out.Targets, exactTargetToWire(&target))
394- }
395- return out, nil
396-}
397-
398-func pickerControlRequestFromWire(message *wire.PickerControlRequest) (protocol.PickerControlRequest, error) {
399- var request protocol.PickerControlRequest
400- if message == nil {
401- return request, protocol.ErrInvalidNavigation
402- }
403- version, err := mustUint16(message.GetVersion())
404- if err != nil {
405- return protocol.PickerControlRequest{}, protocol.ErrInvalidNavigation
406- }
407- request.Version = version
408- request.RequestID = message.GetRequestId()
409- request.Operation, err = enum8[protocol.PickerControlOperation](message.GetOperation())
410- if err != nil {
411- return protocol.PickerControlRequest{}, protocol.ErrInvalidNavigation
412- }
413- request.SourceRevision = message.GetSourceRevision()
414- request.SourceID = message.GetSourceId()
415- request.Key = message.GetKey()
416- for _, target := range message.GetTargets() {
417- converted, err := exactTargetFromWire(target)
418- if err != nil || converted == nil {
419- return protocol.PickerControlRequest{}, protocol.ErrInvalidNavigation
420- }
421- request.Targets = append(request.Targets, *converted)
422- }
423- if err := protocol.ValidatePickerControlRequest(request); err != nil {
424- return protocol.PickerControlRequest{}, err
425- }
426- return request, nil
427-}
M internal/adapters/sessionwire/proto_convert_picker.go +0 -100
@@ -45,28 +45,6 @@ func pickerOfferFromWire(message *wire.PickerOffer) (protocol.PickerOffer, error
4545 return offer, nil
4646 }
4747
48-func pickerBeginToWire(message protocol.PickerBegin) (*wire.PickerBegin, error) {
49- if err := protocol.ValidatePickerBegin(message); err != nil {
50- return nil, err
51- }
52- return &wire.PickerBegin{RequestId: message.RequestID, Intent: uint32(message.Intent)}, nil
53-}
54-
55-func pickerBeginFromWire(message *wire.PickerBegin) (protocol.PickerBegin, error) {
56- if message == nil {
57- return protocol.PickerBegin{}, protocol.ErrInvalidNavigation
58- }
59- intent, err := enum8[protocol.PickerIntent](message.GetIntent())
60- if err != nil {
61- return protocol.PickerBegin{}, err
62- }
63- begin := protocol.PickerBegin{RequestID: message.GetRequestId(), Intent: intent}
64- if err := protocol.ValidatePickerBegin(begin); err != nil {
65- return protocol.PickerBegin{}, err
66- }
67- return begin, nil
68-}
69-
7048 func pickerCloseToWire(message protocol.PickerClose) (*wire.PickerClose, error) {
7149 if err := protocol.ValidatePickerClose(message); err != nil {
7250 return nil, err
@@ -218,81 +196,3 @@ func pickerFailureFromWire(message *wire.PickerFailure) (protocol.PickerFailure,
218196 }
219197 return failure, nil
220198 }
221-
222-func pickerControlResponseToWire(message protocol.PickerControlResponse) (*wire.PickerControlResponse, error) {
223- if err := protocol.ValidatePickerControlResponse(message); err != nil {
224- return nil, err
225- }
226- out := &wire.PickerControlResponse{RequestId: message.RequestID, Operation: uint32(message.Operation), Status: uint32(message.Status)}
227- if message.Snapshot != nil {
228- snapshot, err := pickerSnapshotToWire(*message.Snapshot)
229- if err != nil {
230- return nil, err
231- }
232- out.Snapshot = snapshot
233- }
234- if message.Resolved != nil {
235- resolved, err := attachTargetToWire(*message.Resolved)
236- if err != nil {
237- return nil, err
238- }
239- out.Resolved = resolved
240- }
241- for _, observation := range message.Observations {
242- observation := observation
243- target := exactTargetToWire(&observation.Target)
244- out.Observations = append(out.Observations, &wire.PickerRouteObservation{Target: target, Presence: uint32(observation.Presence), Attention: observation.Attention})
245- }
246- return out, nil
247-}
248-
249-func pickerControlResponseFromWire(message *wire.PickerControlResponse) (protocol.PickerControlResponse, error) {
250- var response protocol.PickerControlResponse
251- if message == nil {
252- return response, protocol.ErrInvalidNavigation
253- }
254- response.RequestID = message.GetRequestId()
255- operation, err := enum8[protocol.PickerControlOperation](message.GetOperation())
256- if err != nil {
257- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
258- }
259- response.Operation = operation
260- status, err := enum8[protocol.PickerSourceStatus](message.GetStatus())
261- if err != nil {
262- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
263- }
264- response.Status = status
265- if message.GetSnapshot() != nil {
266- snapshot, err := pickerSnapshotFromWire(message.GetSnapshot())
267- if err != nil {
268- return protocol.PickerControlResponse{}, err
269- }
270- response.Snapshot = &snapshot
271- }
272- if message.GetResolved() != nil {
273- resolved, err := attachTargetFromWire(message.GetResolved())
274- if err != nil {
275- return protocol.PickerControlResponse{}, err
276- }
277- response.Resolved = &resolved
278- }
279- for _, observation := range message.GetObservations() {
280- target, err := exactTargetFromWire(observation.GetTarget())
281- if err != nil || target == nil {
282- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
283- }
284- presence, err := enum8[protocol.PickerRoutePresence](observation.GetPresence())
285- if err != nil {
286- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
287- }
288- response.Observations = append(response.Observations, protocol.PickerRouteObservation{
289- Target: *target,
290- Presence: presence,
291- Attention: observation.GetAttention(),
292- })
293- }
294- if err := protocol.ValidatePickerControlResponse(response); err != nil {
295- return protocol.PickerControlResponse{}, err
296- }
297- return response, nil
298-}
@@ -45,28 +45,6 @@ func pickerOfferFromWire(message *wire.PickerOffer) (protocol.PickerOffer, error
45 return offer, nil45 return offer, nil
46 }46 }
47 47
48-func pickerBeginToWire(message protocol.PickerBegin) (*wire.PickerBegin, error) {
49- if err := protocol.ValidatePickerBegin(message); err != nil {
50- return nil, err
51- }
52- return &wire.PickerBegin{RequestId: message.RequestID, Intent: uint32(message.Intent)}, nil
53-}
54-
55-func pickerBeginFromWire(message *wire.PickerBegin) (protocol.PickerBegin, error) {
56- if message == nil {
57- return protocol.PickerBegin{}, protocol.ErrInvalidNavigation
58- }
59- intent, err := enum8[protocol.PickerIntent](message.GetIntent())
60- if err != nil {
61- return protocol.PickerBegin{}, err
62- }
63- begin := protocol.PickerBegin{RequestID: message.GetRequestId(), Intent: intent}
64- if err := protocol.ValidatePickerBegin(begin); err != nil {
65- return protocol.PickerBegin{}, err
66- }
67- return begin, nil
68-}
69-
70 func pickerCloseToWire(message protocol.PickerClose) (*wire.PickerClose, error) {48 func pickerCloseToWire(message protocol.PickerClose) (*wire.PickerClose, error) {
71 if err := protocol.ValidatePickerClose(message); err != nil {49 if err := protocol.ValidatePickerClose(message); err != nil {
72 return nil, err50 return nil, err
@@ -218,81 +196,3 @@ func pickerFailureFromWire(message *wire.PickerFailure) (protocol.PickerFailure,
218 }196 }
219 return failure, nil197 return failure, nil
220 }198 }
221-
222-func pickerControlResponseToWire(message protocol.PickerControlResponse) (*wire.PickerControlResponse, error) {
223- if err := protocol.ValidatePickerControlResponse(message); err != nil {
224- return nil, err
225- }
226- out := &wire.PickerControlResponse{RequestId: message.RequestID, Operation: uint32(message.Operation), Status: uint32(message.Status)}
227- if message.Snapshot != nil {
228- snapshot, err := pickerSnapshotToWire(*message.Snapshot)
229- if err != nil {
230- return nil, err
231- }
232- out.Snapshot = snapshot
233- }
234- if message.Resolved != nil {
235- resolved, err := attachTargetToWire(*message.Resolved)
236- if err != nil {
237- return nil, err
238- }
239- out.Resolved = resolved
240- }
241- for _, observation := range message.Observations {
242- observation := observation
243- target := exactTargetToWire(&observation.Target)
244- out.Observations = append(out.Observations, &wire.PickerRouteObservation{Target: target, Presence: uint32(observation.Presence), Attention: observation.Attention})
245- }
246- return out, nil
247-}
248-
249-func pickerControlResponseFromWire(message *wire.PickerControlResponse) (protocol.PickerControlResponse, error) {
250- var response protocol.PickerControlResponse
251- if message == nil {
252- return response, protocol.ErrInvalidNavigation
253- }
254- response.RequestID = message.GetRequestId()
255- operation, err := enum8[protocol.PickerControlOperation](message.GetOperation())
256- if err != nil {
257- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
258- }
259- response.Operation = operation
260- status, err := enum8[protocol.PickerSourceStatus](message.GetStatus())
261- if err != nil {
262- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
263- }
264- response.Status = status
265- if message.GetSnapshot() != nil {
266- snapshot, err := pickerSnapshotFromWire(message.GetSnapshot())
267- if err != nil {
268- return protocol.PickerControlResponse{}, err
269- }
270- response.Snapshot = &snapshot
271- }
272- if message.GetResolved() != nil {
273- resolved, err := attachTargetFromWire(message.GetResolved())
274- if err != nil {
275- return protocol.PickerControlResponse{}, err
276- }
277- response.Resolved = &resolved
278- }
279- for _, observation := range message.GetObservations() {
280- target, err := exactTargetFromWire(observation.GetTarget())
281- if err != nil || target == nil {
282- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
283- }
284- presence, err := enum8[protocol.PickerRoutePresence](observation.GetPresence())
285- if err != nil {
286- return protocol.PickerControlResponse{}, protocol.ErrInvalidNavigation
287- }
288- response.Observations = append(response.Observations, protocol.PickerRouteObservation{
289- Target: *target,
290- Presence: presence,
291- Attention: observation.GetAttention(),
292- })
293- }
294- if err := protocol.ValidatePickerControlResponse(response); err != nil {
295- return protocol.PickerControlResponse{}, err
296- }
297- return response, nil
298-}
M internal/adapters/sessionwire/proto_convert_range_test.go +0 -8
@@ -229,14 +229,6 @@ func TestNarrowingConversionsRejectOverflow(t *testing.T) {
229229 },
230230 wantErr: errProtoConvertRange,
231231 },
232- {
233- name: "picker control request operation",
234- run: func(t *testing.T) error {
235- _, err := pickerControlRequestFromWire(&wire.PickerControlRequest{Operation: 257})
236- return err
237- },
238- wantErr: protocol.ErrInvalidNavigation,
239- },
240232 {
241233 name: "remote preview status",
242234 run: func(t *testing.T) error {
@@ -229,14 +229,6 @@ func TestNarrowingConversionsRejectOverflow(t *testing.T) {
229 },229 },
230 wantErr: errProtoConvertRange,230 wantErr: errProtoConvertRange,
231 },231 },
232- {
233- name: "picker control request operation",
234- run: func(t *testing.T) error {
235- _, err := pickerControlRequestFromWire(&wire.PickerControlRequest{Operation: 257})
236- return err
237- },
238- wantErr: protocol.ErrInvalidNavigation,
239- },
240 {232 {
241 name: "remote preview status",233 name: "remote preview status",
242 run: func(t *testing.T) error {234 run: func(t *testing.T) error {
M internal/adapters/sessionwire/proto_convert_test.go +0 -3
@@ -59,11 +59,9 @@ func TestProtoClientRoundTrips(t *testing.T) {
5959 protocol.NavigationInventoryRequest{Version: protocol.Version, RequestID: 1, Operation: protocol.NavigationInventorySnapshot},
6060 protocol.NavigationInventoryPublication{InteractionGeneration: 1, PublicationGeneration: 1},
6161 protocol.NavigationInventoryFailure{CauseActionID: 1, InteractionGeneration: 1, SourceKey: "local", EntryKey: "a", Code: protocol.NavigationInventoryStaleIdentity},
62- protocol.PickerBegin{RequestID: 1, Intent: protocol.PickerIntentNavigation},
6362 protocol.PickerClose{InteractionID: 1},
6463 protocol.PickerSelection{InteractionID: 1, SourceID: "serving", SourceRevision: 1, Key: "k", Action: protocol.PickerActionNavigate},
6564 protocol.PickerPreviewRequest{Version: protocol.PickerPreviewSchemaVersion, InteractionID: 1, SourceID: "serving", Key: "k", Width: 1, Height: 1},
66- protocol.PickerControlRequest{Version: protocol.Version, RequestID: 1, Operation: protocol.PickerControlSnapshot},
6765 }
6866 for _, message := range messages {
6967 t.Run(protoMessageName(message), func(t *testing.T) {
@@ -116,7 +114,6 @@ func TestProtoServerRoundTrips(t *testing.T) {
116114 protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "k", Action: protocol.PickerActionNavigate},
117115 protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionNavigate, Code: protocol.PickerStaleRevision},
118116 protocol.PickerPreview{Version: protocol.PickerPreviewSchemaVersion, InteractionID: 1, SourceID: "serving", Key: "k", Status: protocol.PickerPreviewUnavailable},
119- protocol.PickerControlResponse{RequestID: 1, Operation: protocol.PickerControlSnapshot, Status: protocol.PickerSourceUnavailable},
120117 }
121118 for _, message := range messages {
122119 t.Run(protoMessageName(message), func(t *testing.T) {
@@ -59,11 +59,9 @@ func TestProtoClientRoundTrips(t *testing.T) {
59 protocol.NavigationInventoryRequest{Version: protocol.Version, RequestID: 1, Operation: protocol.NavigationInventorySnapshot},59 protocol.NavigationInventoryRequest{Version: protocol.Version, RequestID: 1, Operation: protocol.NavigationInventorySnapshot},
60 protocol.NavigationInventoryPublication{InteractionGeneration: 1, PublicationGeneration: 1},60 protocol.NavigationInventoryPublication{InteractionGeneration: 1, PublicationGeneration: 1},
61 protocol.NavigationInventoryFailure{CauseActionID: 1, InteractionGeneration: 1, SourceKey: "local", EntryKey: "a", Code: protocol.NavigationInventoryStaleIdentity},61 protocol.NavigationInventoryFailure{CauseActionID: 1, InteractionGeneration: 1, SourceKey: "local", EntryKey: "a", Code: protocol.NavigationInventoryStaleIdentity},
62- protocol.PickerBegin{RequestID: 1, Intent: protocol.PickerIntentNavigation},
63 protocol.PickerClose{InteractionID: 1},62 protocol.PickerClose{InteractionID: 1},
64 protocol.PickerSelection{InteractionID: 1, SourceID: "serving", SourceRevision: 1, Key: "k", Action: protocol.PickerActionNavigate},63 protocol.PickerSelection{InteractionID: 1, SourceID: "serving", SourceRevision: 1, Key: "k", Action: protocol.PickerActionNavigate},
65 protocol.PickerPreviewRequest{Version: protocol.PickerPreviewSchemaVersion, InteractionID: 1, SourceID: "serving", Key: "k", Width: 1, Height: 1},64 protocol.PickerPreviewRequest{Version: protocol.PickerPreviewSchemaVersion, InteractionID: 1, SourceID: "serving", Key: "k", Width: 1, Height: 1},
66- protocol.PickerControlRequest{Version: protocol.Version, RequestID: 1, Operation: protocol.PickerControlSnapshot},
67 }65 }
68 for _, message := range messages {66 for _, message := range messages {
69 t.Run(protoMessageName(message), func(t *testing.T) {67 t.Run(protoMessageName(message), func(t *testing.T) {
@@ -116,7 +114,6 @@ func TestProtoServerRoundTrips(t *testing.T) {
116 protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "k", Action: protocol.PickerActionNavigate},114 protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "k", Action: protocol.PickerActionNavigate},
117 protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionNavigate, Code: protocol.PickerStaleRevision},115 protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionNavigate, Code: protocol.PickerStaleRevision},
118 protocol.PickerPreview{Version: protocol.PickerPreviewSchemaVersion, InteractionID: 1, SourceID: "serving", Key: "k", Status: protocol.PickerPreviewUnavailable},116 protocol.PickerPreview{Version: protocol.PickerPreviewSchemaVersion, InteractionID: 1, SourceID: "serving", Key: "k", Status: protocol.PickerPreviewUnavailable},
119- protocol.PickerControlResponse{RequestID: 1, Operation: protocol.PickerControlSnapshot, Status: protocol.PickerSourceUnavailable},
120 }117 }
121 for _, message := range messages {118 for _, message := range messages {
122 t.Run(protoMessageName(message), func(t *testing.T) {119 t.Run(protoMessageName(message), func(t *testing.T) {
M internal/adapters/sessionwire/proto_envelope.go +0 -39
@@ -221,17 +221,6 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er
221221 return nil, ErrInvalidMessage
222222 }
223223 return encodeProtoClient(*m)
224- case protocol.PickerBegin:
225- converted, err := pickerBeginToWire(m)
226- if err != nil {
227- return nil, err
228- }
229- return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_PickerBegin{PickerBegin: converted}}, nil
230- case *protocol.PickerBegin:
231- if m == nil {
232- return nil, ErrInvalidMessage
233- }
234- return encodeProtoClient(*m)
235224 case protocol.PickerClose:
236225 converted, err := pickerCloseToWire(m)
237226 if err != nil {
@@ -254,17 +243,6 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er
254243 return nil, ErrInvalidMessage
255244 }
256245 return encodeProtoClient(*m)
257- case protocol.PickerControlRequest:
258- converted, err := pickerControlRequestToWire(m)
259- if err != nil {
260- return nil, err
261- }
262- return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_PickerControlRequest{PickerControlRequest: converted}}, nil
263- case *protocol.PickerControlRequest:
264- if m == nil {
265- return nil, ErrInvalidMessage
266- }
267- return encodeProtoClient(*m)
268246 case protocol.PickerPreviewRequest:
269247 converted, err := pickerPreviewRequestToWire(m)
270248 if err != nil {
@@ -630,17 +608,6 @@ func encodeProtoServer(message protocol.ServerMessage) (*wire.ServerEnvelope, er
630608 return nil, ErrInvalidMessage
631609 }
632610 return encodeProtoServer(*m)
633- case protocol.PickerControlResponse:
634- converted, err := pickerControlResponseToWire(m)
635- if err != nil {
636- return nil, err
637- }
638- return &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_PickerControlResponse{PickerControlResponse: converted}}, nil
639- case *protocol.PickerControlResponse:
640- if m == nil {
641- return nil, ErrInvalidMessage
642- }
643- return encodeProtoServer(*m)
644611 default:
645612 return nil, ErrWrongDirection
646613 }
@@ -703,16 +670,12 @@ func decodeProtoClient(envelope *wire.ClientEnvelope) (protocol.ClientMessage, e
703670 return inventoryPublicationFromWire(payload.NavigationInventoryPublication)
704671 case *wire.ClientEnvelope_NavigationInventoryFailure:
705672 return inventoryFailureFromWire(payload.NavigationInventoryFailure)
706- case *wire.ClientEnvelope_PickerBegin:
707- return pickerBeginFromWire(payload.PickerBegin)
708673 case *wire.ClientEnvelope_PickerClose:
709674 return pickerCloseFromWire(payload.PickerClose)
710675 case *wire.ClientEnvelope_PickerSelection:
711676 return pickerSelectionFromWire(payload.PickerSelection)
712677 case *wire.ClientEnvelope_PickerPreviewRequest:
713678 return pickerPreviewRequestFromWire(payload.PickerPreviewRequest)
714- case *wire.ClientEnvelope_PickerControlRequest:
715- return pickerControlRequestFromWire(payload.PickerControlRequest)
716679 default:
717680 return nil, ErrWrongDirection
718681 }
@@ -785,8 +748,6 @@ func decodeProtoServer(envelope *wire.ServerEnvelope) (protocol.ServerMessage, e
785748 return pickerFailureFromWire(payload.PickerFailure)
786749 case *wire.ServerEnvelope_PickerPreview:
787750 return pickerPreviewFromWire(payload.PickerPreview)
788- case *wire.ServerEnvelope_PickerControlResponse:
789- return pickerControlResponseFromWire(payload.PickerControlResponse)
790751 default:
791752 return nil, ErrWrongDirection
792753 }
@@ -221,17 +221,6 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er
221 return nil, ErrInvalidMessage221 return nil, ErrInvalidMessage
222 }222 }
223 return encodeProtoClient(*m)223 return encodeProtoClient(*m)
224- case protocol.PickerBegin:
225- converted, err := pickerBeginToWire(m)
226- if err != nil {
227- return nil, err
228- }
229- return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_PickerBegin{PickerBegin: converted}}, nil
230- case *protocol.PickerBegin:
231- if m == nil {
232- return nil, ErrInvalidMessage
233- }
234- return encodeProtoClient(*m)
235 case protocol.PickerClose:224 case protocol.PickerClose:
236 converted, err := pickerCloseToWire(m)225 converted, err := pickerCloseToWire(m)
237 if err != nil {226 if err != nil {
@@ -254,17 +243,6 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er
254 return nil, ErrInvalidMessage243 return nil, ErrInvalidMessage
255 }244 }
256 return encodeProtoClient(*m)245 return encodeProtoClient(*m)
257- case protocol.PickerControlRequest:
258- converted, err := pickerControlRequestToWire(m)
259- if err != nil {
260- return nil, err
261- }
262- return &wire.ClientEnvelope{Payload: &wire.ClientEnvelope_PickerControlRequest{PickerControlRequest: converted}}, nil
263- case *protocol.PickerControlRequest:
264- if m == nil {
265- return nil, ErrInvalidMessage
266- }
267- return encodeProtoClient(*m)
268 case protocol.PickerPreviewRequest:246 case protocol.PickerPreviewRequest:
269 converted, err := pickerPreviewRequestToWire(m)247 converted, err := pickerPreviewRequestToWire(m)
270 if err != nil {248 if err != nil {
@@ -630,17 +608,6 @@ func encodeProtoServer(message protocol.ServerMessage) (*wire.ServerEnvelope, er
630 return nil, ErrInvalidMessage608 return nil, ErrInvalidMessage
631 }609 }
632 return encodeProtoServer(*m)610 return encodeProtoServer(*m)
633- case protocol.PickerControlResponse:
634- converted, err := pickerControlResponseToWire(m)
635- if err != nil {
636- return nil, err
637- }
638- return &wire.ServerEnvelope{Payload: &wire.ServerEnvelope_PickerControlResponse{PickerControlResponse: converted}}, nil
639- case *protocol.PickerControlResponse:
640- if m == nil {
641- return nil, ErrInvalidMessage
642- }
643- return encodeProtoServer(*m)
644 default:611 default:
645 return nil, ErrWrongDirection612 return nil, ErrWrongDirection
646 }613 }
@@ -703,16 +670,12 @@ func decodeProtoClient(envelope *wire.ClientEnvelope) (protocol.ClientMessage, e
703 return inventoryPublicationFromWire(payload.NavigationInventoryPublication)670 return inventoryPublicationFromWire(payload.NavigationInventoryPublication)
704 case *wire.ClientEnvelope_NavigationInventoryFailure:671 case *wire.ClientEnvelope_NavigationInventoryFailure:
705 return inventoryFailureFromWire(payload.NavigationInventoryFailure)672 return inventoryFailureFromWire(payload.NavigationInventoryFailure)
706- case *wire.ClientEnvelope_PickerBegin:
707- return pickerBeginFromWire(payload.PickerBegin)
708 case *wire.ClientEnvelope_PickerClose:673 case *wire.ClientEnvelope_PickerClose:
709 return pickerCloseFromWire(payload.PickerClose)674 return pickerCloseFromWire(payload.PickerClose)
710 case *wire.ClientEnvelope_PickerSelection:675 case *wire.ClientEnvelope_PickerSelection:
711 return pickerSelectionFromWire(payload.PickerSelection)676 return pickerSelectionFromWire(payload.PickerSelection)
712 case *wire.ClientEnvelope_PickerPreviewRequest:677 case *wire.ClientEnvelope_PickerPreviewRequest:
713 return pickerPreviewRequestFromWire(payload.PickerPreviewRequest)678 return pickerPreviewRequestFromWire(payload.PickerPreviewRequest)
714- case *wire.ClientEnvelope_PickerControlRequest:
715- return pickerControlRequestFromWire(payload.PickerControlRequest)
716 default:679 default:
717 return nil, ErrWrongDirection680 return nil, ErrWrongDirection
718 }681 }
@@ -785,8 +748,6 @@ func decodeProtoServer(envelope *wire.ServerEnvelope) (protocol.ServerMessage, e
785 return pickerFailureFromWire(payload.PickerFailure)748 return pickerFailureFromWire(payload.PickerFailure)
786 case *wire.ServerEnvelope_PickerPreview:749 case *wire.ServerEnvelope_PickerPreview:
787 return pickerPreviewFromWire(payload.PickerPreview)750 return pickerPreviewFromWire(payload.PickerPreview)
788- case *wire.ServerEnvelope_PickerControlResponse:
789- return pickerControlResponseFromWire(payload.PickerControlResponse)
790 default:751 default:
791 return nil, ErrWrongDirection752 return nil, ErrWrongDirection
792 }753 }
M internal/adapters/sessionwire/server.go +0 -4
@@ -515,10 +515,6 @@ func clientFailureFor(envelope *wire.ClientEnvelope, err error) *protocol.Decode
515515 failure.RequestID = payload.NavigationInventoryRequest.GetRequestId()
516516 failure.HasRequestID = true
517517 }
518- case *wire.ClientEnvelope_PickerControlRequest:
519- if payload, ok := envelope.Payload.(*wire.ClientEnvelope_PickerControlRequest); ok && payload.PickerControlRequest != nil {
520- failure.Version = uint16(payload.PickerControlRequest.GetVersion())
521- }
522518 }
523519 return failure
524520 }
@@ -515,10 +515,6 @@ func clientFailureFor(envelope *wire.ClientEnvelope, err error) *protocol.Decode
515 failure.RequestID = payload.NavigationInventoryRequest.GetRequestId()515 failure.RequestID = payload.NavigationInventoryRequest.GetRequestId()
516 failure.HasRequestID = true516 failure.HasRequestID = true
517 }517 }
518- case *wire.ClientEnvelope_PickerControlRequest:
519- if payload, ok := envelope.Payload.(*wire.ClientEnvelope_PickerControlRequest); ok && payload.PickerControlRequest != nil {
520- failure.Version = uint16(payload.PickerControlRequest.GetVersion())
521- }
522 }518 }
523 return failure519 return failure
524 }520 }
M internal/protocol/messages.go +0 -3
@@ -29,11 +29,9 @@ func (UIFence) clientMessage() {}
2929 func (NavigationInventoryRequest) clientMessage() {}
3030 func (NavigationInventoryPublication) clientMessage() {}
3131 func (NavigationInventoryFailure) clientMessage() {}
32-func (PickerBegin) clientMessage() {}
3332 func (PickerClose) clientMessage() {}
3433 func (PickerSelection) clientMessage() {}
3534 func (PickerPreviewRequest) clientMessage() {}
36-func (PickerControlRequest) clientMessage() {}
3735
3836 func (Welcome) serverMessage() {}
3937 func (ErrorMsg) serverMessage() {}
@@ -63,4 +61,3 @@ func (PickerClosed) serverMessage() {}
6361 func (PickerResult) serverMessage() {}
6462 func (PickerFailure) serverMessage() {}
6563 func (PickerPreview) serverMessage() {}
66-func (PickerControlResponse) serverMessage() {}
@@ -29,11 +29,9 @@ func (UIFence) clientMessage() {}
29 func (NavigationInventoryRequest) clientMessage() {}29 func (NavigationInventoryRequest) clientMessage() {}
30 func (NavigationInventoryPublication) clientMessage() {}30 func (NavigationInventoryPublication) clientMessage() {}
31 func (NavigationInventoryFailure) clientMessage() {}31 func (NavigationInventoryFailure) clientMessage() {}
32-func (PickerBegin) clientMessage() {}
33 func (PickerClose) clientMessage() {}32 func (PickerClose) clientMessage() {}
34 func (PickerSelection) clientMessage() {}33 func (PickerSelection) clientMessage() {}
35 func (PickerPreviewRequest) clientMessage() {}34 func (PickerPreviewRequest) clientMessage() {}
36-func (PickerControlRequest) clientMessage() {}
37 35
38 func (Welcome) serverMessage() {}36 func (Welcome) serverMessage() {}
39 func (ErrorMsg) serverMessage() {}37 func (ErrorMsg) serverMessage() {}
@@ -63,4 +61,3 @@ func (PickerClosed) serverMessage() {}
63 func (PickerResult) serverMessage() {}61 func (PickerResult) serverMessage() {}
64 func (PickerFailure) serverMessage() {}62 func (PickerFailure) serverMessage() {}
65 func (PickerPreview) serverMessage() {}63 func (PickerPreview) serverMessage() {}
66-func (PickerControlResponse) serverMessage() {}
M internal/protocol/picker_interaction.go +3 -198
@@ -125,9 +125,8 @@ type PickerCursor struct {
125125 // PickerOffer opens one interaction. The daemon sends it for intents it
126126 // initiates (a session-picker command or a move request); it carries the
127127 // acquisition barrier the client must display before owning the terminal and
128-// the opaque identity of the captured move source. RequestID echoes the
129-// PickerBegin that asked for the interaction, and is zero for daemon-driven
130-// opens.
128+// the opaque identity of the captured move source. RequestID correlates a
129+// client-owned interaction request, and is zero for daemon-driven opens.
131130 type PickerOffer struct {
132131 InteractionID uint64
133132 RequestID uint64
@@ -139,77 +138,6 @@ type PickerOffer struct {
139138 Title string
140139 }
141140
142-// PickerBegin asks the serving daemon to open one interaction. Startup
143-// pickers and client-driven navigation requests use it; a daemon that already
144-// executed a picker command sends PickerOffer directly.
145-type PickerBegin struct {
146- RequestID uint64
147- Intent PickerIntent
148-}
149-
150-// PickerControlOperation selects a stateless request to the client's stable
151-// local picker authority. It never creates an attachment.
152-type PickerControlOperation uint8
153-
154-const (
155- PickerControlSnapshot PickerControlOperation = iota + 1
156- PickerControlResolve
157- PickerControlObserve
158-)
159-
160-// PickerControlMaxTargets bounds one observation request. A periodic Runner
161-// polls a small fixed working set of exact targets per query; a full directory
162-// belongs to navigation inventory, not to this stateless probe.
163-const PickerControlMaxTargets = 64
164-
165-// PickerRoutePresence reports whether an exact target still resolves to one
166-// live route. Unknown is a first-class answer: an unanswerable probe must
167-// never be reported as Absent, because the caller would otherwise act on a
168-// false negative.
169-type PickerRoutePresence uint8
170-
171-const (
172- PickerRoutePresent PickerRoutePresence = iota + 1
173- PickerRouteAbsent
174- PickerRouteUnknown
175-)
176-
177-// PickerRouteObservation is one exact-target answer. Attention is meaningful
178-// only for Present targets: Absent and Unknown observations always carry
179-// false, so a stale flag can never be mistaken for live attention.
180-type PickerRouteObservation struct {
181- Target ExactSessionTarget
182- Presence PickerRoutePresence
183- Attention bool
184-}
185-
186-// PickerControlRequest queries, resolves, or observes the navigation picker
187-// owned by the client's local daemon. Version stays first for strict
188-// pre-handshake peeking. The operation payloads are exclusive: snapshot and
189-// resolve carry source/key identity, observe carries one bounded exact-target
190-// list, and every field an operation does not own stays zero.
191-type PickerControlRequest struct {
192- Version uint16
193- RequestID uint64
194- Operation PickerControlOperation
195- SourceRevision uint64
196- SourceID string
197- Key string
198- Targets []ExactSessionTarget
199-}
200-
201-// PickerControlResponse returns one complete authoritative snapshot, one
202-// revalidated attach target, or the observation list for an observe request.
203-// The operation payloads are exclusive.
204-type PickerControlResponse struct {
205- RequestID uint64
206- Operation PickerControlOperation
207- Status PickerSourceStatus
208- Snapshot *PickerSnapshot
209- Resolved *AttachTarget
210- Observations []PickerRouteObservation
211-}
212-
213141 // PickerProjection is one complete daemon-authorised presentation. Recent and
214142 // Grouped contain the same destination keys and actions but may differ in
215143 // order, sections, and display labels.
@@ -237,8 +165,7 @@ type PickerSnapshot struct {
237165
238166 // PickerSelection commits the opaque row key displayed at SourceRevision.
239167 // CauseActionID correlates ui-driver input like NavigationInventorySelection.
240-// RequestID echoes the PickerBegin that opened a client-initiated
241-// interaction, and is zero otherwise.
168+// RequestID correlates a client-initiated interaction, and is zero otherwise.
242169 type PickerSelection struct {
243170 CauseActionID uint64
244171 RequestID uint64
@@ -300,120 +227,6 @@ type PickerClosed struct {
300227 BarrierState uint64
301228 }
302229
303-func validPickerControlOperation(operation PickerControlOperation) bool {
304- return operation == PickerControlSnapshot || operation == PickerControlResolve || operation == PickerControlObserve
305-}
306-
307-func validPickerRoutePresence(presence PickerRoutePresence) bool {
308- switch presence {
309- case PickerRoutePresent, PickerRouteAbsent, PickerRouteUnknown:
310- return true
311- default:
312- return false
313- }
314-}
315-
316-// ValidatePickerControlRequest enforces explicit, exclusive operation payloads.
317-// Snapshot and resolve must carry no targets; observe must carry 1..
318-// PickerControlMaxTargets distinct valid exact targets and no source/key
319-// identity, so a query can never smuggle a second payload shape.
320-func ValidatePickerControlRequest(request PickerControlRequest) error {
321- if request.Version == 0 || request.RequestID == 0 || !validPickerControlOperation(request.Operation) {
322- return ErrInvalidNavigation
323- }
324- switch request.Operation {
325- case PickerControlSnapshot:
326- if request.SourceRevision != 0 || request.SourceID != "" || request.Key != "" || len(request.Targets) != 0 {
327- return ErrInvalidNavigation
328- }
329- return nil
330- case PickerControlResolve:
331- if len(request.Targets) != 0 {
332- return ErrInvalidNavigation
333- }
334- if request.SourceRevision == 0 || !validPickerSourceID(request.SourceID) || !validPickerKey(request.Key) {
335- return ErrInvalidNavigation
336- }
337- return nil
338- default:
339- if request.SourceRevision != 0 || request.SourceID != "" || request.Key != "" {
340- return ErrInvalidNavigation
341- }
342- if len(request.Targets) == 0 || len(request.Targets) > PickerControlMaxTargets {
343- return ErrInvalidNavigation
344- }
345- seen := make(map[ExactSessionTarget]struct{}, len(request.Targets))
346- for _, target := range request.Targets {
347- if err := target.Validate(); err != nil {
348- return ErrInvalidNavigation
349- }
350- if _, dup := seen[target]; dup {
351- return ErrInvalidNavigation
352- }
353- seen[target] = struct{}{}
354- }
355- return nil
356- }
357-}
358-
359-// validatePickerRouteObservations requires one bounded, distinct observation
360-// per requested target and refuses an attention flag on anything but a present
361-// target.
362-func validatePickerRouteObservations(observations []PickerRouteObservation) error {
363- if len(observations) == 0 || len(observations) > PickerControlMaxTargets {
364- return ErrInvalidNavigation
365- }
366- seen := make(map[ExactSessionTarget]struct{}, len(observations))
367- for _, observation := range observations {
368- if !validPickerRoutePresence(observation.Presence) {
369- return ErrInvalidNavigation
370- }
371- if err := observation.Target.Validate(); err != nil {
372- return ErrInvalidNavigation
373- }
374- if observation.Presence != PickerRoutePresent && observation.Attention {
375- return ErrInvalidNavigation
376- }
377- if _, dup := seen[observation.Target]; dup {
378- return ErrInvalidNavigation
379- }
380- seen[observation.Target] = struct{}{}
381- }
382- return nil
383-}
384-
385-// ValidatePickerControlResponse enforces the exclusive response union: exactly
386-// the payload owned by the operation and status is present, and a failed
387-// status never carries payload bytes at all.
388-func ValidatePickerControlResponse(response PickerControlResponse) error {
389- if response.RequestID == 0 || !validPickerControlOperation(response.Operation) || !validPickerSourceStatus(response.Status) {
390- return ErrInvalidNavigation
391- }
392- if response.Status != PickerSourceOK {
393- if response.Snapshot != nil || response.Resolved != nil || len(response.Observations) != 0 {
394- return ErrInvalidNavigation
395- }
396- return nil
397- }
398- switch response.Operation {
399- case PickerControlSnapshot:
400- if response.Snapshot == nil || response.Resolved != nil || len(response.Observations) != 0 {
401- return ErrInvalidNavigation
402- }
403- return ValidatePickerSnapshot(*response.Snapshot)
404- case PickerControlResolve:
405- if response.Resolved == nil || response.Snapshot != nil || len(response.Observations) != 0 {
406- return ErrInvalidNavigation
407- }
408- return ValidateAttachTarget(*response.Resolved)
409- default:
410- if response.Snapshot != nil || response.Resolved != nil {
411- return ErrInvalidNavigation
412- }
413- return validatePickerRouteObservations(response.Observations)
414- }
415-}
416-
417230 func validPickerIntent(intent PickerIntent) bool {
418231 switch intent {
419232 case PickerIntentNavigation, PickerIntentMovePane, PickerIntentMoveTab:
@@ -502,14 +315,6 @@ func ValidatePickerOffer(offer PickerOffer) error {
502315 return nil
503316 }
504317
505-// ValidatePickerBegin enforces a nonzero request ID and a known intent.
506-func ValidatePickerBegin(begin PickerBegin) error {
507- if begin.RequestID == 0 || !validPickerIntent(begin.Intent) {
508- return ErrInvalidNavigation
509- }
510- return nil
511-}
512-
513318 // ValidatePickerSnapshot enforces a nonzero interaction/source revision, a
514319 // bounded source identity, and unique opaque keys with display safety. Order
515320 // is preserved verbatim: lines are never re-sorted here.
@@ -125,9 +125,8 @@ type PickerCursor struct {
125 // PickerOffer opens one interaction. The daemon sends it for intents it125 // PickerOffer opens one interaction. The daemon sends it for intents it
126 // initiates (a session-picker command or a move request); it carries the126 // initiates (a session-picker command or a move request); it carries the
127 // acquisition barrier the client must display before owning the terminal and127 // acquisition barrier the client must display before owning the terminal and
128-// the opaque identity of the captured move source. RequestID echoes the128+// the opaque identity of the captured move source. RequestID correlates a
129-// PickerBegin that asked for the interaction, and is zero for daemon-driven129+// client-owned interaction request, and is zero for daemon-driven opens.
130-// opens.
131 type PickerOffer struct {130 type PickerOffer struct {
132 InteractionID uint64131 InteractionID uint64
133 RequestID uint64132 RequestID uint64
@@ -139,77 +138,6 @@ type PickerOffer struct {
139 Title string138 Title string
140 }139 }
141 140
142-// PickerBegin asks the serving daemon to open one interaction. Startup
143-// pickers and client-driven navigation requests use it; a daemon that already
144-// executed a picker command sends PickerOffer directly.
145-type PickerBegin struct {
146- RequestID uint64
147- Intent PickerIntent
148-}
149-
150-// PickerControlOperation selects a stateless request to the client's stable
151-// local picker authority. It never creates an attachment.
152-type PickerControlOperation uint8
153-
154-const (
155- PickerControlSnapshot PickerControlOperation = iota + 1
156- PickerControlResolve
157- PickerControlObserve
158-)
159-
160-// PickerControlMaxTargets bounds one observation request. A periodic Runner
161-// polls a small fixed working set of exact targets per query; a full directory
162-// belongs to navigation inventory, not to this stateless probe.
163-const PickerControlMaxTargets = 64
164-
165-// PickerRoutePresence reports whether an exact target still resolves to one
166-// live route. Unknown is a first-class answer: an unanswerable probe must
167-// never be reported as Absent, because the caller would otherwise act on a
168-// false negative.
169-type PickerRoutePresence uint8
170-
171-const (
172- PickerRoutePresent PickerRoutePresence = iota + 1
173- PickerRouteAbsent
174- PickerRouteUnknown
175-)
176-
177-// PickerRouteObservation is one exact-target answer. Attention is meaningful
178-// only for Present targets: Absent and Unknown observations always carry
179-// false, so a stale flag can never be mistaken for live attention.
180-type PickerRouteObservation struct {
181- Target ExactSessionTarget
182- Presence PickerRoutePresence
183- Attention bool
184-}
185-
186-// PickerControlRequest queries, resolves, or observes the navigation picker
187-// owned by the client's local daemon. Version stays first for strict
188-// pre-handshake peeking. The operation payloads are exclusive: snapshot and
189-// resolve carry source/key identity, observe carries one bounded exact-target
190-// list, and every field an operation does not own stays zero.
191-type PickerControlRequest struct {
192- Version uint16
193- RequestID uint64
194- Operation PickerControlOperation
195- SourceRevision uint64
196- SourceID string
197- Key string
198- Targets []ExactSessionTarget
199-}
200-
201-// PickerControlResponse returns one complete authoritative snapshot, one
202-// revalidated attach target, or the observation list for an observe request.
203-// The operation payloads are exclusive.
204-type PickerControlResponse struct {
205- RequestID uint64
206- Operation PickerControlOperation
207- Status PickerSourceStatus
208- Snapshot *PickerSnapshot
209- Resolved *AttachTarget
210- Observations []PickerRouteObservation
211-}
212-
213 // PickerProjection is one complete daemon-authorised presentation. Recent and141 // PickerProjection is one complete daemon-authorised presentation. Recent and
214 // Grouped contain the same destination keys and actions but may differ in142 // Grouped contain the same destination keys and actions but may differ in
215 // order, sections, and display labels.143 // order, sections, and display labels.
@@ -237,8 +165,7 @@ type PickerSnapshot struct {
237 165
238 // PickerSelection commits the opaque row key displayed at SourceRevision.166 // PickerSelection commits the opaque row key displayed at SourceRevision.
239 // CauseActionID correlates ui-driver input like NavigationInventorySelection.167 // CauseActionID correlates ui-driver input like NavigationInventorySelection.
240-// RequestID echoes the PickerBegin that opened a client-initiated168+// RequestID correlates a client-initiated interaction, and is zero otherwise.
241-// interaction, and is zero otherwise.
242 type PickerSelection struct {169 type PickerSelection struct {
243 CauseActionID uint64170 CauseActionID uint64
244 RequestID uint64171 RequestID uint64
@@ -300,120 +227,6 @@ type PickerClosed struct {
300 BarrierState uint64227 BarrierState uint64
301 }228 }
302 229
303-func validPickerControlOperation(operation PickerControlOperation) bool {
304- return operation == PickerControlSnapshot || operation == PickerControlResolve || operation == PickerControlObserve
305-}
306-
307-func validPickerRoutePresence(presence PickerRoutePresence) bool {
308- switch presence {
309- case PickerRoutePresent, PickerRouteAbsent, PickerRouteUnknown:
310- return true
311- default:
312- return false
313- }
314-}
315-
316-// ValidatePickerControlRequest enforces explicit, exclusive operation payloads.
317-// Snapshot and resolve must carry no targets; observe must carry 1..
318-// PickerControlMaxTargets distinct valid exact targets and no source/key
319-// identity, so a query can never smuggle a second payload shape.
320-func ValidatePickerControlRequest(request PickerControlRequest) error {
321- if request.Version == 0 || request.RequestID == 0 || !validPickerControlOperation(request.Operation) {
322- return ErrInvalidNavigation
323- }
324- switch request.Operation {
325- case PickerControlSnapshot:
326- if request.SourceRevision != 0 || request.SourceID != "" || request.Key != "" || len(request.Targets) != 0 {
327- return ErrInvalidNavigation
328- }
329- return nil
330- case PickerControlResolve:
331- if len(request.Targets) != 0 {
332- return ErrInvalidNavigation
333- }
334- if request.SourceRevision == 0 || !validPickerSourceID(request.SourceID) || !validPickerKey(request.Key) {
335- return ErrInvalidNavigation
336- }
337- return nil
338- default:
339- if request.SourceRevision != 0 || request.SourceID != "" || request.Key != "" {
340- return ErrInvalidNavigation
341- }
342- if len(request.Targets) == 0 || len(request.Targets) > PickerControlMaxTargets {
343- return ErrInvalidNavigation
344- }
345- seen := make(map[ExactSessionTarget]struct{}, len(request.Targets))
346- for _, target := range request.Targets {
347- if err := target.Validate(); err != nil {
348- return ErrInvalidNavigation
349- }
350- if _, dup := seen[target]; dup {
351- return ErrInvalidNavigation
352- }
353- seen[target] = struct{}{}
354- }
355- return nil
356- }
357-}
358-
359-// validatePickerRouteObservations requires one bounded, distinct observation
360-// per requested target and refuses an attention flag on anything but a present
361-// target.
362-func validatePickerRouteObservations(observations []PickerRouteObservation) error {
363- if len(observations) == 0 || len(observations) > PickerControlMaxTargets {
364- return ErrInvalidNavigation
365- }
366- seen := make(map[ExactSessionTarget]struct{}, len(observations))
367- for _, observation := range observations {
368- if !validPickerRoutePresence(observation.Presence) {
369- return ErrInvalidNavigation
370- }
371- if err := observation.Target.Validate(); err != nil {
372- return ErrInvalidNavigation
373- }
374- if observation.Presence != PickerRoutePresent && observation.Attention {
375- return ErrInvalidNavigation
376- }
377- if _, dup := seen[observation.Target]; dup {
378- return ErrInvalidNavigation
379- }
380- seen[observation.Target] = struct{}{}
381- }
382- return nil
383-}
384-
385-// ValidatePickerControlResponse enforces the exclusive response union: exactly
386-// the payload owned by the operation and status is present, and a failed
387-// status never carries payload bytes at all.
388-func ValidatePickerControlResponse(response PickerControlResponse) error {
389- if response.RequestID == 0 || !validPickerControlOperation(response.Operation) || !validPickerSourceStatus(response.Status) {
390- return ErrInvalidNavigation
391- }
392- if response.Status != PickerSourceOK {
393- if response.Snapshot != nil || response.Resolved != nil || len(response.Observations) != 0 {
394- return ErrInvalidNavigation
395- }
396- return nil
397- }
398- switch response.Operation {
399- case PickerControlSnapshot:
400- if response.Snapshot == nil || response.Resolved != nil || len(response.Observations) != 0 {
401- return ErrInvalidNavigation
402- }
403- return ValidatePickerSnapshot(*response.Snapshot)
404- case PickerControlResolve:
405- if response.Resolved == nil || response.Snapshot != nil || len(response.Observations) != 0 {
406- return ErrInvalidNavigation
407- }
408- return ValidateAttachTarget(*response.Resolved)
409- default:
410- if response.Snapshot != nil || response.Resolved != nil {
411- return ErrInvalidNavigation
412- }
413- return validatePickerRouteObservations(response.Observations)
414- }
415-}
416-
417 func validPickerIntent(intent PickerIntent) bool {230 func validPickerIntent(intent PickerIntent) bool {
418 switch intent {231 switch intent {
419 case PickerIntentNavigation, PickerIntentMovePane, PickerIntentMoveTab:232 case PickerIntentNavigation, PickerIntentMovePane, PickerIntentMoveTab:
@@ -502,14 +315,6 @@ func ValidatePickerOffer(offer PickerOffer) error {
502 return nil315 return nil
503 }316 }
504 317
505-// ValidatePickerBegin enforces a nonzero request ID and a known intent.
506-func ValidatePickerBegin(begin PickerBegin) error {
507- if begin.RequestID == 0 || !validPickerIntent(begin.Intent) {
508- return ErrInvalidNavigation
509- }
510- return nil
511-}
512-
513 // ValidatePickerSnapshot enforces a nonzero interaction/source revision, a318 // ValidatePickerSnapshot enforces a nonzero interaction/source revision, a
514 // bounded source identity, and unique opaque keys with display safety. Order319 // bounded source identity, and unique opaque keys with display safety. Order
515 // is preserved verbatim: lines are never re-sorted here.320 // is preserved verbatim: lines are never re-sorted here.
D internal/protocol/picker_interaction_test.go +0 -180
@@ -1,180 +0,0 @@
1-package protocol
2-
3-import (
4- "testing"
5-
6- "github.com/bnema/vev/internal/domain"
7- "github.com/stretchr/testify/require"
8-)
9-
10-func pickerObserveTargets(seed, count int) []ExactSessionTarget {
11- targets := make([]ExactSessionTarget, 0, count)
12- for i := range count {
13- targets = append(targets, ExactSessionTarget{
14- LifecycleID: domain.SessionLifecycleID{byte(seed + i)},
15- SessionName: "work",
16- })
17- }
18- return targets
19-}
20-
21-func TestValidatePickerControlObserveRequest(t *testing.T) {
22- targets := pickerObserveTargets(1, 2)
23- target := targets[0]
24-
25- valid := PickerControlRequest{Version: 1, RequestID: 9, Operation: PickerControlObserve, Targets: targets}
26- require.NoError(t, ValidatePickerControlRequest(valid))
27-
28- // The pre-existing exclusive payloads keep working.
29- require.NoError(t, ValidatePickerControlRequest(PickerControlRequest{Version: 1, RequestID: 9, Operation: PickerControlSnapshot}))
30- require.NoError(t, ValidatePickerControlRequest(PickerControlRequest{
31- Version: 1, RequestID: 9, Operation: PickerControlResolve,
32- SourceRevision: 1, SourceID: PickerHomeSourceID, Key: "a/b",
33- }))
34-
35- atLimit := PickerControlRequest{
36- Version: 1, RequestID: 9, Operation: PickerControlObserve,
37- Targets: pickerObserveTargets(1, PickerControlMaxTargets),
38- }
39- require.NoError(t, ValidatePickerControlRequest(atLimit))
40-
41- cases := map[string]PickerControlRequest{
42- "no targets": {Version: 1, RequestID: 9, Operation: PickerControlObserve},
43- "duplicate target": {
44- Version: 1, RequestID: 9, Operation: PickerControlObserve,
45- Targets: []ExactSessionTarget{target, target},
46- },
47- "unvalidated target": {
48- Version: 1, RequestID: 9, Operation: PickerControlObserve,
49- Targets: []ExactSessionTarget{{SessionName: "work"}},
50- },
51- "over the target bound": {
52- Version: 1, RequestID: 9, Operation: PickerControlObserve,
53- Targets: pickerObserveTargets(1, PickerControlMaxTargets+1),
54- },
55- "observe with source identity": {
56- Version: 1, RequestID: 9, Operation: PickerControlObserve,
57- SourceID: PickerHomeSourceID, Targets: targets,
58- },
59- "observe with key": {
60- Version: 1, RequestID: 9, Operation: PickerControlObserve,
61- Key: "a/b", Targets: targets,
62- },
63- "observe with revision": {
64- Version: 1, RequestID: 9, Operation: PickerControlObserve,
65- SourceRevision: 2, Targets: targets,
66- },
67- "snapshot with targets": {Version: 1, RequestID: 9, Operation: PickerControlSnapshot, Targets: targets},
68- "resolve with targets": {
69- Version: 1, RequestID: 9, Operation: PickerControlResolve,
70- SourceRevision: 1, SourceID: PickerHomeSourceID, Key: "a/b", Targets: targets,
71- },
72- "unknown operation": {Version: 1, RequestID: 9, Operation: 9, Targets: targets},
73- "zero version": {RequestID: 9, Operation: PickerControlObserve, Targets: targets},
74- "zero request id": {Version: 1, Operation: PickerControlObserve, Targets: targets},
75- }
76- for name, request := range cases {
77- t.Run(name, func(t *testing.T) {
78- require.ErrorIs(t, ValidatePickerControlRequest(request), ErrInvalidNavigation)
79- })
80- }
81-}
82-
83-func TestValidatePickerControlObserveResponse(t *testing.T) {
84- targets := pickerObserveTargets(1, 2)
85-
86- valid := PickerControlResponse{
87- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
88- Observations: []PickerRouteObservation{
89- {Target: targets[0], Presence: PickerRoutePresent, Attention: true},
90- {Target: targets[1], Presence: PickerRouteAbsent},
91- },
92- }
93- require.NoError(t, ValidatePickerControlResponse(valid))
94-
95- // A failed observe carries no payload at all.
96- require.NoError(t, ValidatePickerControlResponse(PickerControlResponse{
97- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceUnavailable,
98- }))
99-
100- atLimit := PickerControlResponse{
101- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
102- Observations: pickerObservationsForTest(1, PickerControlMaxTargets, PickerRoutePresent),
103- }
104- require.NoError(t, ValidatePickerControlResponse(atLimit))
105-
106- cases := map[string]PickerControlResponse{
107- "no observations": {
108- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
109- },
110- "over the observation bound": {
111- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
112- Observations: pickerObservationsForTest(1, PickerControlMaxTargets+1, PickerRoutePresent),
113- },
114- "duplicate target": {
115- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
116- Observations: []PickerRouteObservation{
117- {Target: targets[0], Presence: PickerRoutePresent},
118- {Target: targets[0], Presence: PickerRouteAbsent},
119- },
120- },
121- "zero presence": {
122- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
123- Observations: []PickerRouteObservation{{Target: targets[0]}},
124- },
125- "unknown presence value": {
126- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
127- Observations: []PickerRouteObservation{{Target: targets[0], Presence: 9}},
128- },
129- "unvalidated target": {
130- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
131- Observations: []PickerRouteObservation{{Target: ExactSessionTarget{SessionName: "work"}, Presence: PickerRoutePresent}},
132- },
133- "attention on absent": {
134- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
135- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRouteAbsent, Attention: true}},
136- },
137- "attention on unknown": {
138- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
139- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRouteUnknown, Attention: true}},
140- },
141- "observe carries snapshot": {
142- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
143- Snapshot: &PickerSnapshot{},
144- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
145- },
146- "observe carries resolved": {
147- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
148- Resolved: &AttachTarget{},
149- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
150- },
151- "failed observe carries observations": {
152- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceUnavailable,
153- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
154- },
155- "snapshot carries observations": {
156- RequestID: 9, Operation: PickerControlSnapshot, Status: PickerSourceOK,
157- Snapshot: &PickerSnapshot{},
158- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
159- },
160- "resolve carries observations": {
161- RequestID: 9, Operation: PickerControlResolve, Status: PickerSourceOK,
162- Resolved: &AttachTarget{},
163- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
164- },
165- }
166- for name, response := range cases {
167- t.Run(name, func(t *testing.T) {
168- require.ErrorIs(t, ValidatePickerControlResponse(response), ErrInvalidNavigation)
169- })
170- }
171-}
172-
173-func pickerObservationsForTest(seed, count int, presence PickerRoutePresence) []PickerRouteObservation {
174- targets := pickerObserveTargets(seed, count)
175- observations := make([]PickerRouteObservation, 0, count)
176- for _, target := range targets {
177- observations = append(observations, PickerRouteObservation{Target: target, Presence: presence})
178- }
179- return observations
180-}
@@ -1,180 +0,0 @@
1-package protocol
2-
3-import (
4- "testing"
5-
6- "github.com/bnema/vev/internal/domain"
7- "github.com/stretchr/testify/require"
8-)
9-
10-func pickerObserveTargets(seed, count int) []ExactSessionTarget {
11- targets := make([]ExactSessionTarget, 0, count)
12- for i := range count {
13- targets = append(targets, ExactSessionTarget{
14- LifecycleID: domain.SessionLifecycleID{byte(seed + i)},
15- SessionName: "work",
16- })
17- }
18- return targets
19-}
20-
21-func TestValidatePickerControlObserveRequest(t *testing.T) {
22- targets := pickerObserveTargets(1, 2)
23- target := targets[0]
24-
25- valid := PickerControlRequest{Version: 1, RequestID: 9, Operation: PickerControlObserve, Targets: targets}
26- require.NoError(t, ValidatePickerControlRequest(valid))
27-
28- // The pre-existing exclusive payloads keep working.
29- require.NoError(t, ValidatePickerControlRequest(PickerControlRequest{Version: 1, RequestID: 9, Operation: PickerControlSnapshot}))
30- require.NoError(t, ValidatePickerControlRequest(PickerControlRequest{
31- Version: 1, RequestID: 9, Operation: PickerControlResolve,
32- SourceRevision: 1, SourceID: PickerHomeSourceID, Key: "a/b",
33- }))
34-
35- atLimit := PickerControlRequest{
36- Version: 1, RequestID: 9, Operation: PickerControlObserve,
37- Targets: pickerObserveTargets(1, PickerControlMaxTargets),
38- }
39- require.NoError(t, ValidatePickerControlRequest(atLimit))
40-
41- cases := map[string]PickerControlRequest{
42- "no targets": {Version: 1, RequestID: 9, Operation: PickerControlObserve},
43- "duplicate target": {
44- Version: 1, RequestID: 9, Operation: PickerControlObserve,
45- Targets: []ExactSessionTarget{target, target},
46- },
47- "unvalidated target": {
48- Version: 1, RequestID: 9, Operation: PickerControlObserve,
49- Targets: []ExactSessionTarget{{SessionName: "work"}},
50- },
51- "over the target bound": {
52- Version: 1, RequestID: 9, Operation: PickerControlObserve,
53- Targets: pickerObserveTargets(1, PickerControlMaxTargets+1),
54- },
55- "observe with source identity": {
56- Version: 1, RequestID: 9, Operation: PickerControlObserve,
57- SourceID: PickerHomeSourceID, Targets: targets,
58- },
59- "observe with key": {
60- Version: 1, RequestID: 9, Operation: PickerControlObserve,
61- Key: "a/b", Targets: targets,
62- },
63- "observe with revision": {
64- Version: 1, RequestID: 9, Operation: PickerControlObserve,
65- SourceRevision: 2, Targets: targets,
66- },
67- "snapshot with targets": {Version: 1, RequestID: 9, Operation: PickerControlSnapshot, Targets: targets},
68- "resolve with targets": {
69- Version: 1, RequestID: 9, Operation: PickerControlResolve,
70- SourceRevision: 1, SourceID: PickerHomeSourceID, Key: "a/b", Targets: targets,
71- },
72- "unknown operation": {Version: 1, RequestID: 9, Operation: 9, Targets: targets},
73- "zero version": {RequestID: 9, Operation: PickerControlObserve, Targets: targets},
74- "zero request id": {Version: 1, Operation: PickerControlObserve, Targets: targets},
75- }
76- for name, request := range cases {
77- t.Run(name, func(t *testing.T) {
78- require.ErrorIs(t, ValidatePickerControlRequest(request), ErrInvalidNavigation)
79- })
80- }
81-}
82-
83-func TestValidatePickerControlObserveResponse(t *testing.T) {
84- targets := pickerObserveTargets(1, 2)
85-
86- valid := PickerControlResponse{
87- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
88- Observations: []PickerRouteObservation{
89- {Target: targets[0], Presence: PickerRoutePresent, Attention: true},
90- {Target: targets[1], Presence: PickerRouteAbsent},
91- },
92- }
93- require.NoError(t, ValidatePickerControlResponse(valid))
94-
95- // A failed observe carries no payload at all.
96- require.NoError(t, ValidatePickerControlResponse(PickerControlResponse{
97- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceUnavailable,
98- }))
99-
100- atLimit := PickerControlResponse{
101- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
102- Observations: pickerObservationsForTest(1, PickerControlMaxTargets, PickerRoutePresent),
103- }
104- require.NoError(t, ValidatePickerControlResponse(atLimit))
105-
106- cases := map[string]PickerControlResponse{
107- "no observations": {
108- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
109- },
110- "over the observation bound": {
111- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
112- Observations: pickerObservationsForTest(1, PickerControlMaxTargets+1, PickerRoutePresent),
113- },
114- "duplicate target": {
115- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
116- Observations: []PickerRouteObservation{
117- {Target: targets[0], Presence: PickerRoutePresent},
118- {Target: targets[0], Presence: PickerRouteAbsent},
119- },
120- },
121- "zero presence": {
122- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
123- Observations: []PickerRouteObservation{{Target: targets[0]}},
124- },
125- "unknown presence value": {
126- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
127- Observations: []PickerRouteObservation{{Target: targets[0], Presence: 9}},
128- },
129- "unvalidated target": {
130- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
131- Observations: []PickerRouteObservation{{Target: ExactSessionTarget{SessionName: "work"}, Presence: PickerRoutePresent}},
132- },
133- "attention on absent": {
134- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
135- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRouteAbsent, Attention: true}},
136- },
137- "attention on unknown": {
138- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
139- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRouteUnknown, Attention: true}},
140- },
141- "observe carries snapshot": {
142- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
143- Snapshot: &PickerSnapshot{},
144- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
145- },
146- "observe carries resolved": {
147- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceOK,
148- Resolved: &AttachTarget{},
149- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
150- },
151- "failed observe carries observations": {
152- RequestID: 9, Operation: PickerControlObserve, Status: PickerSourceUnavailable,
153- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
154- },
155- "snapshot carries observations": {
156- RequestID: 9, Operation: PickerControlSnapshot, Status: PickerSourceOK,
157- Snapshot: &PickerSnapshot{},
158- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
159- },
160- "resolve carries observations": {
161- RequestID: 9, Operation: PickerControlResolve, Status: PickerSourceOK,
162- Resolved: &AttachTarget{},
163- Observations: []PickerRouteObservation{{Target: targets[0], Presence: PickerRoutePresent}},
164- },
165- }
166- for name, response := range cases {
167- t.Run(name, func(t *testing.T) {
168- require.ErrorIs(t, ValidatePickerControlResponse(response), ErrInvalidNavigation)
169- })
170- }
171-}
172-
173-func pickerObservationsForTest(seed, count int, presence PickerRoutePresence) []PickerRouteObservation {
174- targets := pickerObserveTargets(seed, count)
175- observations := make([]PickerRouteObservation, 0, count)
176- for _, target := range targets {
177- observations = append(observations, PickerRouteObservation{Target: target, Presence: presence})
178- }
179- return observations
180-}
M internal/protocol/session.go +3 -1
@@ -93,6 +93,8 @@ const (
9393 ReasonSessionKilled uint8 = 1
9494 ReasonServerShutdown uint8 = 2
9595 ReasonReplaced uint8 = 3
96+ // ReasonDetachToPicker transfers terminal ownership to the client picker.
97+ ReasonDetachToPicker uint8 = 4
9698 )
9799
98100 // Hello is sent by the client immediately after connecting.
@@ -364,7 +366,7 @@ func validClientNoticeAction(action uint8) bool {
364366
365367 func validDetachedReason(reason uint8) bool {
366368 switch reason {
367- case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
369+ case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced, ReasonDetachToPicker:
368370 return true
369371 default:
370372 return false
@@ -93,6 +93,8 @@ const (
93 ReasonSessionKilled uint8 = 193 ReasonSessionKilled uint8 = 1
94 ReasonServerShutdown uint8 = 294 ReasonServerShutdown uint8 = 2
95 ReasonReplaced uint8 = 395 ReasonReplaced uint8 = 3
96+ // ReasonDetachToPicker transfers terminal ownership to the client picker.
97+ ReasonDetachToPicker uint8 = 4
96 )98 )
97 99
98 // Hello is sent by the client immediately after connecting.100 // Hello is sent by the client immediately after connecting.
@@ -364,7 +366,7 @@ func validClientNoticeAction(action uint8) bool {
364 366
365 func validDetachedReason(reason uint8) bool {367 func validDetachedReason(reason uint8) bool {
366 switch reason {368 switch reason {
367- case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:369+ case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced, ReasonDetachToPicker:
368 return true370 return true
369 default:371 default:
370 return false372 return false
M internal/protocol/wire/broker.pb.go +399 -77 generated file: collapsed
M internal/protocol/wire/broker_test.go +29 -0
@@ -20,6 +20,7 @@ var brokerClientTags = map[string]protoreflect.FieldNumber{
2020 "register": 101, "subscribe": 102, "resync": 103, "unsubscribe": 104,
2121 "add_host": 105, "remove_host": 106, "reconcile": 107, "open_stream": 108,
2222 "client_stream_data": 109, "close_stream": 110, "update_host_policy": 111,
23+ "start_preview": 112, "cancel_preview": 113,
2324 }
2425
2526 // brokerServerTags is the frozen server tag inventory.
@@ -27,6 +28,7 @@ var brokerServerTags = map[string]protoreflect.FieldNumber{
2728 "registered": 201, "snapshot_part": 202, "operation_result": 203,
2829 "stream_opened": 204, "server_stream_data": 205, "stream_closed": 206,
2930 "progress": 207, "broker_error_message": 208, "shutdown": 209,
31+ "preview_publication": 210,
3032 }
3133
3234 func oneofTags(t *testing.T, message proto.Message) map[string]protoreflect.FieldNumber {
@@ -174,6 +176,29 @@ func brokerClientSamples() map[string]*BrokerClientEnvelope {
174176 Ref: brokerRef(), Data: bytes.Repeat([]byte{0x66}, 24),
175177 }}},
176178 "close_stream": {Payload: &BrokerClientEnvelope_CloseStream{CloseStream: &CloseStream{Ref: brokerRef()}}},
179+ "start_preview": {Payload: &BrokerClientEnvelope_StartPreview{StartPreview: &StartPreview{
180+ Scope: brokerScope(), Generation: 3,
181+ Route: &OpenStream{
182+ Ref: brokerRef(), Purpose: 3,
183+ Policy: brokerPolicy(),
184+ StartMode: 1, Local: true,
185+ // Note: route validation per ports requires observation+existing-only;
186+ // the scanner sample only needs structural coverage, so keep
187+ // purpose/observation fields populated.
188+ },
189+ Preview: &RemotePreviewRequest{
190+ Version: 1,
191+ Target: &RemoteTarget{
192+ Endpoint: "dev@host:22", DisplayOrigin: "dev@host",
193+ LifecycleId: &LifecycleID{Value: bytes.Repeat([]byte{0x55}, 16)},
194+ SessionName: "work", LiveTabId: "tab-1",
195+ },
196+ Width: 80, Height: 24,
197+ },
198+ }}},
199+ "cancel_preview": {Payload: &BrokerClientEnvelope_CancelPreview{CancelPreview: &CancelPreview{
200+ Scope: brokerScope(), Generation: 3,
201+ }}},
177202 }
178203 }
179204
@@ -217,6 +242,10 @@ func brokerServerSamples() map[string]*BrokerServerEnvelope {
217242 "shutdown": {Payload: &BrokerServerEnvelope_Shutdown{Shutdown: &Shutdown{
218243 Scope: brokerScope(), Reason: 1, Text: "idle exit",
219244 }}},
245+ "preview_publication": {Payload: &BrokerServerEnvelope_PreviewPublication{PreviewPublication: &PreviewPublication{
246+ Scope: brokerScope(), Generation: 3,
247+ Result: &PreviewPublication_Error{Error: &BrokerErrorDetail{Code: 1, Text: "unavailable"}},
248+ }}},
220249 }
221250 }
222251
@@ -20,6 +20,7 @@ var brokerClientTags = map[string]protoreflect.FieldNumber{
20 "register": 101, "subscribe": 102, "resync": 103, "unsubscribe": 104,20 "register": 101, "subscribe": 102, "resync": 103, "unsubscribe": 104,
21 "add_host": 105, "remove_host": 106, "reconcile": 107, "open_stream": 108,21 "add_host": 105, "remove_host": 106, "reconcile": 107, "open_stream": 108,
22 "client_stream_data": 109, "close_stream": 110, "update_host_policy": 111,22 "client_stream_data": 109, "close_stream": 110, "update_host_policy": 111,
23+ "start_preview": 112, "cancel_preview": 113,
23 }24 }
24 25
25 // brokerServerTags is the frozen server tag inventory.26 // brokerServerTags is the frozen server tag inventory.
@@ -27,6 +28,7 @@ var brokerServerTags = map[string]protoreflect.FieldNumber{
27 "registered": 201, "snapshot_part": 202, "operation_result": 203,28 "registered": 201, "snapshot_part": 202, "operation_result": 203,
28 "stream_opened": 204, "server_stream_data": 205, "stream_closed": 206,29 "stream_opened": 204, "server_stream_data": 205, "stream_closed": 206,
29 "progress": 207, "broker_error_message": 208, "shutdown": 209,30 "progress": 207, "broker_error_message": 208, "shutdown": 209,
31+ "preview_publication": 210,
30 }32 }
31 33
32 func oneofTags(t *testing.T, message proto.Message) map[string]protoreflect.FieldNumber {34 func oneofTags(t *testing.T, message proto.Message) map[string]protoreflect.FieldNumber {
@@ -174,6 +176,29 @@ func brokerClientSamples() map[string]*BrokerClientEnvelope {
174 Ref: brokerRef(), Data: bytes.Repeat([]byte{0x66}, 24),176 Ref: brokerRef(), Data: bytes.Repeat([]byte{0x66}, 24),
175 }}},177 }}},
176 "close_stream": {Payload: &BrokerClientEnvelope_CloseStream{CloseStream: &CloseStream{Ref: brokerRef()}}},178 "close_stream": {Payload: &BrokerClientEnvelope_CloseStream{CloseStream: &CloseStream{Ref: brokerRef()}}},
179+ "start_preview": {Payload: &BrokerClientEnvelope_StartPreview{StartPreview: &StartPreview{
180+ Scope: brokerScope(), Generation: 3,
181+ Route: &OpenStream{
182+ Ref: brokerRef(), Purpose: 3,
183+ Policy: brokerPolicy(),
184+ StartMode: 1, Local: true,
185+ // Note: route validation per ports requires observation+existing-only;
186+ // the scanner sample only needs structural coverage, so keep
187+ // purpose/observation fields populated.
188+ },
189+ Preview: &RemotePreviewRequest{
190+ Version: 1,
191+ Target: &RemoteTarget{
192+ Endpoint: "dev@host:22", DisplayOrigin: "dev@host",
193+ LifecycleId: &LifecycleID{Value: bytes.Repeat([]byte{0x55}, 16)},
194+ SessionName: "work", LiveTabId: "tab-1",
195+ },
196+ Width: 80, Height: 24,
197+ },
198+ }}},
199+ "cancel_preview": {Payload: &BrokerClientEnvelope_CancelPreview{CancelPreview: &CancelPreview{
200+ Scope: brokerScope(), Generation: 3,
201+ }}},
177 }202 }
178 }203 }
179 204
@@ -217,6 +242,10 @@ func brokerServerSamples() map[string]*BrokerServerEnvelope {
217 "shutdown": {Payload: &BrokerServerEnvelope_Shutdown{Shutdown: &Shutdown{242 "shutdown": {Payload: &BrokerServerEnvelope_Shutdown{Shutdown: &Shutdown{
218 Scope: brokerScope(), Reason: 1, Text: "idle exit",243 Scope: brokerScope(), Reason: 1, Text: "idle exit",
219 }}},244 }}},
245+ "preview_publication": {Payload: &BrokerServerEnvelope_PreviewPublication{PreviewPublication: &PreviewPublication{
246+ Scope: brokerScope(), Generation: 3,
247+ Result: &PreviewPublication_Error{Error: &BrokerErrorDetail{Code: 1, Text: "unavailable"}},
248+ }}},
220 }249 }
221 }250 }
222 251
M internal/protocol/wire/envelope.pb.go +78 -138 generated file: collapsed
M internal/protocol/wire/envelope_inventory_test.go +5 -5
@@ -39,8 +39,8 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
3939 "route_navigation_failure": true, "session_creation_failure": true,
4040 "ui_fence": true, "navigation_inventory_request": true,
4141 "navigation_inventory_publication": true, "navigation_inventory_failure": true,
42- "picker_begin": true, "picker_close": true, "picker_selection": true,
43- "picker_preview_request": true, "picker_control_request": true,
42+ "picker_close": true, "picker_selection": true,
43+ "picker_preview_request": true,
4444 }
4545 serverSemantic := map[string]bool{
4646 "attachment_suspended": true, "attachment_activated": true,
@@ -51,9 +51,9 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
5151 "route_navigation_failure": true, "route_position": true, "route_retired": true,
5252 "same_peer_switch_failure": true, "ui_receipt": true, "ui_view_update": true,
5353 "navigation_inventory_response": true, "navigation_inventory_demand": true,
54- "navigation_inventory_selection": true, "picker_offer": true,
55- "picker_snapshot": true, "picker_closed": true, "picker_result": true,
56- "picker_failure": true, "picker_preview": true, "picker_control_response": true,
54+ "navigation_inventory_selection": true,
55+ "picker_offer": true, "picker_snapshot": true, "picker_closed": true,
56+ "picker_result": true, "picker_failure": true, "picker_preview": true,
5757 "kill_result": true,
5858 }
5959
@@ -39,8 +39,8 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
39 "route_navigation_failure": true, "session_creation_failure": true,39 "route_navigation_failure": true, "session_creation_failure": true,
40 "ui_fence": true, "navigation_inventory_request": true,40 "ui_fence": true, "navigation_inventory_request": true,
41 "navigation_inventory_publication": true, "navigation_inventory_failure": true,41 "navigation_inventory_publication": true, "navigation_inventory_failure": true,
42- "picker_begin": true, "picker_close": true, "picker_selection": true,42+ "picker_close": true, "picker_selection": true,
43- "picker_preview_request": true, "picker_control_request": true,43+ "picker_preview_request": true,
44 }44 }
45 serverSemantic := map[string]bool{45 serverSemantic := map[string]bool{
46 "attachment_suspended": true, "attachment_activated": true,46 "attachment_suspended": true, "attachment_activated": true,
@@ -51,9 +51,9 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) {
51 "route_navigation_failure": true, "route_position": true, "route_retired": 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,52 "same_peer_switch_failure": true, "ui_receipt": true, "ui_view_update": true,
53 "navigation_inventory_response": true, "navigation_inventory_demand": true,53 "navigation_inventory_response": true, "navigation_inventory_demand": true,
54- "navigation_inventory_selection": true, "picker_offer": true,54+ "navigation_inventory_selection": true,
55- "picker_snapshot": true, "picker_closed": true, "picker_result": true,55+ "picker_offer": true, "picker_snapshot": true, "picker_closed": true,
56- "picker_failure": true, "picker_preview": true, "picker_control_response": true,56+ "picker_result": true, "picker_failure": true, "picker_preview": true,
57 "kill_result": true,57 "kill_result": true,
58 }58 }
59 59
M internal/protocol/wire/navigation.pb.go +16 -293 generated file: collapsed
M internal/protocol/wire/picker.pb.go +23 -81 generated file: collapsed
M internal/protocol/wire/schema/broker.proto +36 -1
@@ -1,7 +1,7 @@
11 // Broker wire schema: the client/server broker conversations.
22 //
33 // The broker is an independently versioned conversation from the session
4-// protocol: client tags 101-111 and server tags 201-209 are disjoint from
4+// protocol: client tags 101-113 and server tags 201-210 are disjoint from
55 // every session tag, and each direction is its own closed oneof union.
66 // Scope (broker epoch plus client connection) is carried explicitly on
77 // every payload so stream IDs and events never cross a scope. Semantic
@@ -12,6 +12,7 @@ syntax = "proto3";
1212 package vev.wire.v1;
1313
1414 import "common.proto";
15+import "terminal.proto";
1516
1617 option go_package = "github.com/bnema/vev/internal/protocol/wire";
1718
@@ -86,6 +87,8 @@ message BrokerClientEnvelope {
8687 ClientStreamData client_stream_data = 109;
8788 CloseStream close_stream = 110;
8889 UpdateHostPolicy update_host_policy = 111;
90+ StartPreview start_preview = 112;
91+ CancelPreview cancel_preview = 113;
8992 }
9093 }
9194
@@ -102,6 +105,7 @@ message BrokerServerEnvelope {
102105 Progress progress = 207;
103106 BrokerErrorMessage broker_error_message = 208;
104107 Shutdown shutdown = 209;
108+ PreviewPublication preview_publication = 210;
105109 }
106110 }
107111
@@ -356,6 +360,37 @@ message Shutdown {
356360 string text = 3;
357361 }
358362
363+// StartPreview starts or replaces one connection-scoped live preview.
364+// scope carries the broker epoch plus client connection; generation is the
365+// connection-scoped preview authority. route is the exact observation
366+// stream authority and preview is the bounded viewport request sent to the
367+// daemon. Route/request dimensions are retained by client subscription
368+// state; repeated authority is scope/generation plus the preview target
369+// identity response.
370+message StartPreview {
371+ BrokerScope scope = 1;
372+ uint64 generation = 2;
373+ OpenStream route = 3;
374+ RemotePreviewRequest preview = 4;
375+}
376+
377+// CancelPreview cancels one connection-scoped live preview.
378+message CancelPreview {
379+ BrokerScope scope = 1;
380+ uint64 generation = 2;
381+}
382+
383+// PreviewPublication is the newest result for one preview subscription.
384+// Exactly one of preview/error is set via the result oneof.
385+message PreviewPublication {
386+ BrokerScope scope = 1;
387+ uint64 generation = 2;
388+ oneof result {
389+ RemotePreview preview = 3;
390+ BrokerErrorDetail error = 4;
391+ }
392+}
393+
359394 // BrokerCatalogSession mirrors catalogue.RemoteCatalogSession exactly: the
360395 // lifecycle identity is a fixed 16-byte value, the state is a closed
361396 // taxonomy, and the tab list keeps its presence separately from its
@@ -1,7 +1,7 @@
1 // Broker wire schema: the client/server broker conversations.1 // Broker wire schema: the client/server broker conversations.
2 //2 //
3 // The broker is an independently versioned conversation from the session3 // The broker is an independently versioned conversation from the session
4-// protocol: client tags 101-111 and server tags 201-209 are disjoint from4+// protocol: client tags 101-113 and server tags 201-210 are disjoint from
5 // every session tag, and each direction is its own closed oneof union.5 // every session tag, and each direction is its own closed oneof union.
6 // Scope (broker epoch plus client connection) is carried explicitly on6 // Scope (broker epoch plus client connection) is carried explicitly on
7 // every payload so stream IDs and events never cross a scope. Semantic7 // every payload so stream IDs and events never cross a scope. Semantic
@@ -12,6 +12,7 @@ syntax = "proto3";
12 package vev.wire.v1;12 package vev.wire.v1;
13 13
14 import "common.proto";14 import "common.proto";
15+import "terminal.proto";
15 16
16 option go_package = "github.com/bnema/vev/internal/protocol/wire";17 option go_package = "github.com/bnema/vev/internal/protocol/wire";
17 18
@@ -86,6 +87,8 @@ message BrokerClientEnvelope {
86 ClientStreamData client_stream_data = 109;87 ClientStreamData client_stream_data = 109;
87 CloseStream close_stream = 110;88 CloseStream close_stream = 110;
88 UpdateHostPolicy update_host_policy = 111;89 UpdateHostPolicy update_host_policy = 111;
90+ StartPreview start_preview = 112;
91+ CancelPreview cancel_preview = 113;
89 }92 }
90 }93 }
91 94
@@ -102,6 +105,7 @@ message BrokerServerEnvelope {
102 Progress progress = 207;105 Progress progress = 207;
103 BrokerErrorMessage broker_error_message = 208;106 BrokerErrorMessage broker_error_message = 208;
104 Shutdown shutdown = 209;107 Shutdown shutdown = 209;
108+ PreviewPublication preview_publication = 210;
105 }109 }
106 }110 }
107 111
@@ -356,6 +360,37 @@ message Shutdown {
356 string text = 3;360 string text = 3;
357 }361 }
358 362
363+// StartPreview starts or replaces one connection-scoped live preview.
364+// scope carries the broker epoch plus client connection; generation is the
365+// connection-scoped preview authority. route is the exact observation
366+// stream authority and preview is the bounded viewport request sent to the
367+// daemon. Route/request dimensions are retained by client subscription
368+// state; repeated authority is scope/generation plus the preview target
369+// identity response.
370+message StartPreview {
371+ BrokerScope scope = 1;
372+ uint64 generation = 2;
373+ OpenStream route = 3;
374+ RemotePreviewRequest preview = 4;
375+}
376+
377+// CancelPreview cancels one connection-scoped live preview.
378+message CancelPreview {
379+ BrokerScope scope = 1;
380+ uint64 generation = 2;
381+}
382+
383+// PreviewPublication is the newest result for one preview subscription.
384+// Exactly one of preview/error is set via the result oneof.
385+message PreviewPublication {
386+ BrokerScope scope = 1;
387+ uint64 generation = 2;
388+ oneof result {
389+ RemotePreview preview = 3;
390+ BrokerErrorDetail error = 4;
391+ }
392+}
393+
359 // BrokerCatalogSession mirrors catalogue.RemoteCatalogSession exactly: the394 // BrokerCatalogSession mirrors catalogue.RemoteCatalogSession exactly: the
360 // lifecycle identity is a fixed 16-byte value, the state is a closed395 // lifecycle identity is a fixed 16-byte value, the state is a closed
361 // taxonomy, and the tab list keeps its presence separately from its396 // taxonomy, and the tab list keeps its presence separately from its
M internal/protocol/wire/schema/envelope.proto +0 -3
@@ -91,11 +91,9 @@ message ClientEnvelope {
9191 NavigationInventoryRequest navigation_inventory_request = 21;
9292 NavigationInventoryPublication navigation_inventory_publication = 22;
9393 NavigationInventoryFailure navigation_inventory_failure = 23;
94- PickerBegin picker_begin = 24;
9594 PickerClose picker_close = 25;
9695 PickerSelection picker_selection = 26;
9796 PickerPreviewRequest picker_preview_request = 27;
98- PickerControlRequest picker_control_request = 28;
9997 SuspendAttachment suspend_attachment = 29;
10098 ActivateAttachment activate_attachment = 30;
10199 }
@@ -135,7 +133,6 @@ message ServerEnvelope {
135133 PickerResult picker_result = 25;
136134 PickerFailure picker_failure = 26;
137135 PickerPreview picker_preview = 27;
138- PickerControlResponse picker_control_response = 28;
139136 AttachmentSuspended attachment_suspended = 31;
140137 AttachmentActivated attachment_activated = 32;
141138 KillResult kill_result = 33;
@@ -91,11 +91,9 @@ message ClientEnvelope {
91 NavigationInventoryRequest navigation_inventory_request = 21;91 NavigationInventoryRequest navigation_inventory_request = 21;
92 NavigationInventoryPublication navigation_inventory_publication = 22;92 NavigationInventoryPublication navigation_inventory_publication = 22;
93 NavigationInventoryFailure navigation_inventory_failure = 23;93 NavigationInventoryFailure navigation_inventory_failure = 23;
94- PickerBegin picker_begin = 24;
95 PickerClose picker_close = 25;94 PickerClose picker_close = 25;
96 PickerSelection picker_selection = 26;95 PickerSelection picker_selection = 26;
97 PickerPreviewRequest picker_preview_request = 27;96 PickerPreviewRequest picker_preview_request = 27;
98- PickerControlRequest picker_control_request = 28;
99 SuspendAttachment suspend_attachment = 29;97 SuspendAttachment suspend_attachment = 29;
100 ActivateAttachment activate_attachment = 30;98 ActivateAttachment activate_attachment = 30;
101 }99 }
@@ -135,7 +133,6 @@ message ServerEnvelope {
135 PickerResult picker_result = 25;133 PickerResult picker_result = 25;
136 PickerFailure picker_failure = 26;134 PickerFailure picker_failure = 26;
137 PickerPreview picker_preview = 27;135 PickerPreview picker_preview = 27;
138- PickerControlResponse picker_control_response = 28;
139 AttachmentSuspended attachment_suspended = 31;136 AttachmentSuspended attachment_suspended = 31;
140 AttachmentActivated attachment_activated = 32;137 AttachmentActivated attachment_activated = 32;
141 KillResult kill_result = 33;138 KillResult kill_result = 33;
M internal/protocol/wire/schema/navigation.proto +0 -35
@@ -4,7 +4,6 @@ syntax = "proto3";
44 package vev.wire.v1;
55
66 import "common.proto";
7-import "picker.proto";
87 import "session.proto";
98
109 option go_package = "github.com/bnema/vev/internal/protocol/wire";
@@ -83,37 +82,3 @@ message NavigationInventoryFailure {
8382 // 6 = restore failed.
8483 uint32 code = 5;
8584 }
86-
87-// PickerRouteObservation is one exact-target probe answer.
88-message PickerRouteObservation {
89- ExactTarget target = 1;
90- // Closed presence taxonomy: 1 = present, 2 = absent, 3 = unknown.
91- uint32 presence = 2;
92- bool attention = 3;
93-}
94-
95-// PickerControlRequest queries the client's local picker authority.
96-// Operation payloads are exclusive: snapshot/resolve carry source/key
97-// identity, observe carries one bounded target list.
98-message PickerControlRequest {
99- uint32 version = 1;
100- uint64 request_id = 2;
101- // Closed operation taxonomy: 1 = snapshot, 2 = resolve, 3 = observe.
102- uint32 operation = 3;
103- uint64 source_revision = 4;
104- string source_id = 5;
105- string key = 6;
106- repeated ExactTarget targets = 7;
107-}
108-
109-// PickerControlResponse returns one snapshot, one revalidated target, or
110-// the observation list. Failed statuses carry no payload.
111-message PickerControlResponse {
112- uint64 request_id = 1;
113- uint32 operation = 2;
114- // Closed source-status taxonomy: 1 = ok, 2 = unavailable, 3 = too large.
115- uint32 status = 3;
116- PickerSnapshot snapshot = 4; // optional pointer
117- AttachTarget resolved = 5; // optional pointer
118- repeated PickerRouteObservation observations = 6;
119-}
@@ -4,7 +4,6 @@ syntax = "proto3";
4 package vev.wire.v1;4 package vev.wire.v1;
5 5
6 import "common.proto";6 import "common.proto";
7-import "picker.proto";
8 import "session.proto";7 import "session.proto";
9 8
10 option go_package = "github.com/bnema/vev/internal/protocol/wire";9 option go_package = "github.com/bnema/vev/internal/protocol/wire";
@@ -83,37 +82,3 @@ message NavigationInventoryFailure {
83 // 6 = restore failed.82 // 6 = restore failed.
84 uint32 code = 5;83 uint32 code = 5;
85 }84 }
86-
87-// PickerRouteObservation is one exact-target probe answer.
88-message PickerRouteObservation {
89- ExactTarget target = 1;
90- // Closed presence taxonomy: 1 = present, 2 = absent, 3 = unknown.
91- uint32 presence = 2;
92- bool attention = 3;
93-}
94-
95-// PickerControlRequest queries the client's local picker authority.
96-// Operation payloads are exclusive: snapshot/resolve carry source/key
97-// identity, observe carries one bounded target list.
98-message PickerControlRequest {
99- uint32 version = 1;
100- uint64 request_id = 2;
101- // Closed operation taxonomy: 1 = snapshot, 2 = resolve, 3 = observe.
102- uint32 operation = 3;
103- uint64 source_revision = 4;
104- string source_id = 5;
105- string key = 6;
106- repeated ExactTarget targets = 7;
107-}
108-
109-// PickerControlResponse returns one snapshot, one revalidated target, or
110-// the observation list. Failed statuses carry no payload.
111-message PickerControlResponse {
112- uint64 request_id = 1;
113- uint32 operation = 2;
114- // Closed source-status taxonomy: 1 = ok, 2 = unavailable, 3 = too large.
115- uint32 status = 3;
116- PickerSnapshot snapshot = 4; // optional pointer
117- AttachTarget resolved = 5; // optional pointer
118- repeated PickerRouteObservation observations = 6;
119-}
M internal/protocol/wire/schema/picker.proto +0 -6
@@ -61,12 +61,6 @@ message PickerOffer {
6161 string title = 8;
6262 }
6363
64-// PickerBegin asks the daemon to open one interaction.
65-message PickerBegin {
66- uint64 request_id = 1;
67- uint32 intent = 2; // same taxonomy as PickerOffer.intent
68-}
69-
7064 // PickerSelection commits the opaque row key at a revision.
7165 message PickerSelection {
7266 uint64 cause_action_id = 1;
@@ -61,12 +61,6 @@ message PickerOffer {
61 string title = 8;61 string title = 8;
62 }62 }
63 63
64-// PickerBegin asks the daemon to open one interaction.
65-message PickerBegin {
66- uint64 request_id = 1;
67- uint32 intent = 2; // same taxonomy as PickerOffer.intent
68-}
69-
70 // PickerSelection commits the opaque row key at a revision.64 // PickerSelection commits the opaque row key at a revision.
71 message PickerSelection {65 message PickerSelection {
72 uint64 cause_action_id = 1;66 uint64 cause_action_id = 1;

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 12

Tags 11

↑↓ moveenter openesc close