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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 | |||
| 237 | 237 | return nil, ErrInvalidMessage | |
| 238 | 238 | } | |
| 239 | 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 | 262 | default: | |
| 241 | 263 | return nil, ErrWrongDirection | |
| 242 | 264 | } | |
| @@ -279,6 +301,10 @@ func decodeClientEnvelope(envelope *wire.BrokerClientEnvelope, maxChunkBytes uin | |||
| 279 | 301 | return nil, ErrInvalidMessage | |
| 280 | 302 | } | |
| 281 | 303 | 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 | 308 | default: | |
| 283 | 309 | return nil, ErrWrongDirection | |
| 284 | 310 | } | |
| @@ -867,6 +893,17 @@ func encodeServerEnvelope(message ServerMessage, maxChunkBytes uint64) (*wire.Br | |||
| 867 | 893 | return nil, ErrInvalidMessage | |
| 868 | 894 | } | |
| 869 | 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 | 907 | default: | |
| 871 | 908 | return nil, ErrWrongDirection | |
| 872 | 909 | } | |
| @@ -920,6 +957,8 @@ func decodeServerEnvelope(envelope *wire.BrokerServerEnvelope, maxChunkBytes uin | |||
| 920 | 957 | return BrokerErrorMessage{Epoch: epoch, Connection: connection, Error: detail}, nil | |
| 921 | 958 | case *wire.BrokerServerEnvelope_Shutdown: | |
| 922 | 959 | return shutdownFromWire(payload.Shutdown) | |
| 960 | + | case *wire.BrokerServerEnvelope_PreviewPublication: | |
| 961 | + | return previewPublicationFromWire(payload.PreviewPublication) | |
| 923 | 962 | default: | |
| 924 | 963 | return nil, ErrWrongDirection | |
| 925 | 964 | } | |
| @@ -237,6 +237,28 @@ func encodeClientEnvelope(message ClientMessage, maxChunkBytes uint64) (*wire.Br | |||||
| 237 | return nil, ErrInvalidMessage | 237 | 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, ErrWrongDirection | 263 | return nil, ErrWrongDirection | ||
| 242 | } | 264 | } | ||
| @@ -279,6 +301,10 @@ func decodeClientEnvelope(envelope *wire.BrokerClientEnvelope, maxChunkBytes uin | |||||
| 279 | return nil, ErrInvalidMessage | 301 | return nil, ErrInvalidMessage | ||
| 280 | } | 302 | } | ||
| 281 | return CloseStream{Epoch: epoch, Connection: connection, Stream: stream}, nil | 303 | 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, ErrWrongDirection | 309 | return nil, ErrWrongDirection | ||
| 284 | } | 310 | } | ||
| @@ -867,6 +893,17 @@ func encodeServerEnvelope(message ServerMessage, maxChunkBytes uint64) (*wire.Br | |||||
| 867 | return nil, ErrInvalidMessage | 893 | 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, ErrWrongDirection | 908 | 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}, nil | 957 | 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, ErrWrongDirection | 963 | return nil, ErrWrongDirection | ||
| 925 | } | 964 | } | ||
M internal/adapters/brokerwire/codec_test.go +5 -1
| @@ -358,8 +358,10 @@ func TestBrokerClientVariants(t *testing.T) { | |||
| 358 | 358 | {"client_stream_data", ClientStreamData{Epoch: 7, Connection: connection, Stream: 3, Data: []byte("frame")}, 109}, | |
| 359 | 359 | {"close_stream", CloseStream{Epoch: 7, Connection: connection, Stream: 3}, 110}, | |
| 360 | 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 | 365 | for _, tc := range messages { | |
| 364 | 366 | t.Run(tc.name, func(t *testing.T) { | |
| 365 | 367 | raw := mustEncodeClient(t, tc.message) | |
| @@ -602,6 +604,8 @@ func TestBrokerServerVariants(t *testing.T) { | |||
| 602 | 604 | {"progress", Progress{Epoch: 7, Connection: connection, Stream: 3, Phase: BrokerProgressProbing, Text: "probing"}, 207}, | |
| 603 | 605 | {"broker_error", BrokerErrorMessage{Epoch: 7, Connection: connection, Error: testErrorDetail()}, 208}, | |
| 604 | 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 | 610 | for _, tc := range messages { | |
| 607 | 611 | 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 @@ | |||
| 1 | 1 | // Package brokerwire adapts typed broker messages to Protobuf envelopes. | |
| 2 | 2 | // | |
| 3 | 3 | // 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 | 5 | // and each direction is its own closed oneof union. Every connection starts | |
| 6 | 6 | // with the bounded broker preamble (roles 3/4); magic, epoch, exact | |
| 7 | 7 | // 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: client | 3 | // 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 starts | 5 | // and each direction is its own closed oneof union. Every connection starts | ||
| 6 | // with the bounded broker preamble (roles 3/4); magic, epoch, exact | 6 | // with the bounded broker preamble (roles 3/4); magic, epoch, exact | ||
| 7 | // protocol version, and negotiated limits are validated before application | 7 | // protocol version, and negotiated limits are validated before application | ||
M internal/adapters/brokerwire/messages.go +40 -0
| @@ -162,6 +162,30 @@ type CloseStream struct { | |||
| 162 | 162 | ||
| 163 | 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 | 189 | // Registered confirms one accepted client connection and its assigned scope. | |
| 166 | 190 | type Registered struct { | |
| 167 | 191 | Epoch ports.BrokerEpoch | |
| @@ -452,6 +476,22 @@ type BrokerErrorMessage struct { | |||
| 452 | 476 | ||
| 453 | 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 | 495 | // MaxBrokerAdmissionCode is the highest admission-refusal code carried on | |
| 456 | 496 | // ErrorDetail. The taxonomy is closed: 0 is none, 1 limit, 2 closed, 3 stale | |
| 457 | 497 | // 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.BrokerEpoch | 191 | 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 on | 495 | // MaxBrokerAdmissionCode is the highest admission-refusal code carried on | ||
| 456 | // ErrorDetail. The taxonomy is closed: 0 is none, 1 limit, 2 closed, 3 stale | 496 | // ErrorDetail. The taxonomy is closed: 0 is none, 1 limit, 2 closed, 3 stale | ||
| 457 | // connection or stream, 4 invalid request (mirroring | 497 | // 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 @@ | |||
| 1 | 1 | package sessionwire | |
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | - | "math" | |
| 5 | 4 | "testing" | |
| 6 | 5 | ||
| 7 | - | "google.golang.org/protobuf/proto" | |
| 8 | - | ||
| 9 | 6 | renderer "github.com/bnema/vev-vt" | |
| 10 | 7 | "github.com/bnema/vev/internal/protocol" | |
| 11 | 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 | 54 | func TestPickerClientMessagesEncodeWithTypes(t *testing.T) { | |
| 72 | - | begin := protocol.PickerBegin{RequestID: 3, Intent: protocol.PickerIntentNavigation} | |
| 73 | 55 | closeMessage := protocol.PickerClose{InteractionID: 7, RequestID: 3} | |
| 74 | 56 | selection := pickerWireSelection() | |
| 75 | 57 | previewRequest := pickerWirePreviewRequest() | |
| 76 | - | controlRequest := pickerWireControlRequest() | |
| 77 | 58 | tests := []struct { | |
| 78 | 59 | name string | |
| 79 | 60 | message protocol.ClientMessage | |
| 80 | 61 | }{ | |
| 81 | - | {name: "begin", message: begin}, | |
| 82 | - | {name: "begin pointer", message: &begin}, | |
| 83 | 62 | {name: "close", message: closeMessage}, | |
| 84 | 63 | {name: "close pointer", message: &closeMessage}, | |
| 85 | 64 | {name: "selection", message: selection}, | |
| 86 | 65 | {name: "selection pointer", message: &selection}, | |
| 87 | 66 | {name: "preview request", message: previewRequest}, | |
| 88 | 67 | {name: "preview request pointer", message: &previewRequest}, | |
| 89 | - | {name: "control request", message: controlRequest}, | |
| 90 | - | {name: "control request pointer", message: &controlRequest}, | |
| 91 | 68 | } | |
| 92 | 69 | for _, tt := range tests { | |
| 93 | 70 | t.Run(tt.name, func(t *testing.T) { | |
| @@ -96,16 +73,12 @@ func TestPickerClientMessagesEncodeWithTypes(t *testing.T) { | |||
| 96 | 73 | got, err := DecodeClientEnvelope(mustSingleAppPayload(t, raw)) | |
| 97 | 74 | require.NoError(t, err) | |
| 98 | 75 | switch message := tt.message.(type) { | |
| 99 | - | case *protocol.PickerBegin: | |
| 100 | - | require.Equal(t, *message, got) | |
| 101 | 76 | case *protocol.PickerClose: | |
| 102 | 77 | require.Equal(t, *message, got) | |
| 103 | 78 | case *protocol.PickerSelection: | |
| 104 | 79 | require.Equal(t, *message, got) | |
| 105 | 80 | case *protocol.PickerPreviewRequest: | |
| 106 | 81 | require.Equal(t, *message, got) | |
| 107 | - | case *protocol.PickerControlRequest: | |
| 108 | - | require.Equal(t, *message, got) | |
| 109 | 82 | default: | |
| 110 | 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 | 154 | func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) { | |
| 271 | 155 | offer := pickerWireOffer() | |
| 272 | 156 | snapshot := pickerWireSnapshot() | |
| 273 | 157 | selection := pickerWireSelection() | |
| 274 | 158 | clientMessages := []protocol.ClientMessage{ | |
| 275 | - | protocol.PickerBegin{RequestID: 1, Intent: protocol.PickerIntentNavigation}, | |
| 276 | 159 | protocol.PickerClose{InteractionID: 1}, | |
| 277 | 160 | selection, | |
| 278 | 161 | pickerWirePreviewRequest(), | |
| 279 | - | pickerWireControlRequest(), | |
| 280 | 162 | } | |
| 281 | 163 | serverMessages := []protocol.ServerMessage{ | |
| 282 | 164 | offer, | |
| @@ -285,7 +167,6 @@ func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) { | |||
| 285 | 167 | protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "a/b", Action: protocol.PickerActionKill}, | |
| 286 | 168 | protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionKill, Code: protocol.PickerUnknownKey}, | |
| 287 | 169 | pickerWirePreview(), | |
| 288 | - | pickerWireControlResponse(), | |
| 289 | 170 | } | |
| 290 | 171 | // Cross-direction decodability is structural, not directional: field | |
| 291 | 172 | // numbers collide across envelopes, so a client payload may parse as | |
| @@ -315,15 +196,6 @@ func TestPickerMessagesDecodeInCorrectDirection(t *testing.T) { | |||
| 315 | 196 | require.Equal(t, snapshot.Grouped, decoded.Grouped) | |
| 316 | 197 | 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 | 199 | require.Equal(t, message, got) | |
| 328 | 200 | } | |
| 329 | 201 | } | |
| @@ -1,11 +1,8 @@ | |||||
| 1 | package sessionwire | 1 | 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 string | 59 | name string | ||
| 79 | message protocol.ClientMessage | 60 | 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: field | 171 | // Cross-direction decodability is structural, not directional: field | ||
| 291 | // numbers collide across envelopes, so a client payload may parse as | 172 | // 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 | continue | 197 | 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 | |||
| 375 | 375 | snapshot.Cursor = snapshot.Recent.Cursor | |
| 376 | 376 | 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 | - | } | |
| @@ -375,53 +375,3 @@ func pickerSnapshotFromWire(message *wire.PickerSnapshot) (protocol.PickerSnapsh | |||||
| 375 | snapshot.Cursor = snapshot.Recent.Cursor | 375 | snapshot.Cursor = snapshot.Recent.Cursor | ||
| 376 | return snapshot, nil | 376 | 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 | |||
| 45 | 45 | 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 | 48 | func pickerCloseToWire(message protocol.PickerClose) (*wire.PickerClose, error) { | |
| 71 | 49 | if err := protocol.ValidatePickerClose(message); err != nil { | |
| 72 | 50 | return nil, err | |
| @@ -218,81 +196,3 @@ func pickerFailureFromWire(message *wire.PickerFailure) (protocol.PickerFailure, | |||
| 218 | 196 | } | |
| 219 | 197 | 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 | - | } | |
| @@ -45,28 +45,6 @@ func pickerOfferFromWire(message *wire.PickerOffer) (protocol.PickerOffer, error | |||||
| 45 | return offer, nil | 45 | 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, err | 50 | return nil, err | ||
| @@ -218,81 +196,3 @@ func pickerFailureFromWire(message *wire.PickerFailure) (protocol.PickerFailure, | |||||
| 218 | } | 196 | } | ||
| 219 | return failure, nil | 197 | 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) { | |||
| 229 | 229 | }, | |
| 230 | 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 | 233 | name: "remote preview status", | |
| 242 | 234 | 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) { | |||
| 59 | 59 | protocol.NavigationInventoryRequest{Version: protocol.Version, RequestID: 1, Operation: protocol.NavigationInventorySnapshot}, | |
| 60 | 60 | protocol.NavigationInventoryPublication{InteractionGeneration: 1, PublicationGeneration: 1}, | |
| 61 | 61 | protocol.NavigationInventoryFailure{CauseActionID: 1, InteractionGeneration: 1, SourceKey: "local", EntryKey: "a", Code: protocol.NavigationInventoryStaleIdentity}, | |
| 62 | - | protocol.PickerBegin{RequestID: 1, Intent: protocol.PickerIntentNavigation}, | |
| 63 | 62 | protocol.PickerClose{InteractionID: 1}, | |
| 64 | 63 | protocol.PickerSelection{InteractionID: 1, SourceID: "serving", SourceRevision: 1, Key: "k", Action: protocol.PickerActionNavigate}, | |
| 65 | 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 | 66 | for _, message := range messages { | |
| 69 | 67 | t.Run(protoMessageName(message), func(t *testing.T) { | |
| @@ -116,7 +114,6 @@ func TestProtoServerRoundTrips(t *testing.T) { | |||
| 116 | 114 | protocol.PickerResult{InteractionID: 1, SourceID: "serving", Key: "k", Action: protocol.PickerActionNavigate}, | |
| 117 | 115 | protocol.PickerFailure{InteractionID: 1, Action: protocol.PickerActionNavigate, Code: protocol.PickerStaleRevision}, | |
| 118 | 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 | 118 | for _, message := range messages { | |
| 122 | 119 | 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 | |||
| 221 | 221 | return nil, ErrInvalidMessage | |
| 222 | 222 | } | |
| 223 | 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 | 224 | case protocol.PickerClose: | |
| 236 | 225 | converted, err := pickerCloseToWire(m) | |
| 237 | 226 | if err != nil { | |
| @@ -254,17 +243,6 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er | |||
| 254 | 243 | return nil, ErrInvalidMessage | |
| 255 | 244 | } | |
| 256 | 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 | 246 | case protocol.PickerPreviewRequest: | |
| 269 | 247 | converted, err := pickerPreviewRequestToWire(m) | |
| 270 | 248 | if err != nil { | |
| @@ -630,17 +608,6 @@ func encodeProtoServer(message protocol.ServerMessage) (*wire.ServerEnvelope, er | |||
| 630 | 608 | return nil, ErrInvalidMessage | |
| 631 | 609 | } | |
| 632 | 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 | 611 | default: | |
| 645 | 612 | return nil, ErrWrongDirection | |
| 646 | 613 | } | |
| @@ -703,16 +670,12 @@ func decodeProtoClient(envelope *wire.ClientEnvelope) (protocol.ClientMessage, e | |||
| 703 | 670 | return inventoryPublicationFromWire(payload.NavigationInventoryPublication) | |
| 704 | 671 | case *wire.ClientEnvelope_NavigationInventoryFailure: | |
| 705 | 672 | return inventoryFailureFromWire(payload.NavigationInventoryFailure) | |
| 706 | - | case *wire.ClientEnvelope_PickerBegin: | |
| 707 | - | return pickerBeginFromWire(payload.PickerBegin) | |
| 708 | 673 | case *wire.ClientEnvelope_PickerClose: | |
| 709 | 674 | return pickerCloseFromWire(payload.PickerClose) | |
| 710 | 675 | case *wire.ClientEnvelope_PickerSelection: | |
| 711 | 676 | return pickerSelectionFromWire(payload.PickerSelection) | |
| 712 | 677 | case *wire.ClientEnvelope_PickerPreviewRequest: | |
| 713 | 678 | return pickerPreviewRequestFromWire(payload.PickerPreviewRequest) | |
| 714 | - | case *wire.ClientEnvelope_PickerControlRequest: | |
| 715 | - | return pickerControlRequestFromWire(payload.PickerControlRequest) | |
| 716 | 679 | default: | |
| 717 | 680 | return nil, ErrWrongDirection | |
| 718 | 681 | } | |
| @@ -785,8 +748,6 @@ func decodeProtoServer(envelope *wire.ServerEnvelope) (protocol.ServerMessage, e | |||
| 785 | 748 | return pickerFailureFromWire(payload.PickerFailure) | |
| 786 | 749 | case *wire.ServerEnvelope_PickerPreview: | |
| 787 | 750 | return pickerPreviewFromWire(payload.PickerPreview) | |
| 788 | - | case *wire.ServerEnvelope_PickerControlResponse: | |
| 789 | - | return pickerControlResponseFromWire(payload.PickerControlResponse) | |
| 790 | 751 | default: | |
| 791 | 752 | return nil, ErrWrongDirection | |
| 792 | 753 | } | |
| @@ -221,17 +221,6 @@ func encodeProtoClient(message protocol.ClientMessage) (*wire.ClientEnvelope, er | |||||
| 221 | return nil, ErrInvalidMessage | 221 | 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, ErrInvalidMessage | 243 | 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, ErrInvalidMessage | 608 | 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, ErrWrongDirection | 612 | 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, ErrWrongDirection | 680 | 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, ErrWrongDirection | 752 | 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 | |||
| 515 | 515 | failure.RequestID = payload.NavigationInventoryRequest.GetRequestId() | |
| 516 | 516 | 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 | 519 | return failure | |
| 524 | 520 | } | |
| @@ -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 = true | 516 | 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 failure | 519 | return failure | ||
| 524 | } | 520 | } | ||
M internal/protocol/messages.go +0 -3
| @@ -29,11 +29,9 @@ func (UIFence) clientMessage() {} | |||
| 29 | 29 | func (NavigationInventoryRequest) clientMessage() {} | |
| 30 | 30 | func (NavigationInventoryPublication) clientMessage() {} | |
| 31 | 31 | func (NavigationInventoryFailure) clientMessage() {} | |
| 32 | - | func (PickerBegin) clientMessage() {} | |
| 33 | 32 | func (PickerClose) clientMessage() {} | |
| 34 | 33 | func (PickerSelection) clientMessage() {} | |
| 35 | 34 | func (PickerPreviewRequest) clientMessage() {} | |
| 36 | - | func (PickerControlRequest) clientMessage() {} | |
| 37 | 35 | ||
| 38 | 36 | func (Welcome) serverMessage() {} | |
| 39 | 37 | func (ErrorMsg) serverMessage() {} | |
| @@ -63,4 +61,3 @@ func (PickerClosed) serverMessage() {} | |||
| 63 | 61 | func (PickerResult) serverMessage() {} | |
| 64 | 62 | func (PickerFailure) serverMessage() {} | |
| 65 | 63 | 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 { | |||
| 125 | 125 | // PickerOffer opens one interaction. The daemon sends it for intents it | |
| 126 | 126 | // initiates (a session-picker command or a move request); it carries the | |
| 127 | 127 | // 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. | |
| 131 | 130 | type PickerOffer struct { | |
| 132 | 131 | InteractionID uint64 | |
| 133 | 132 | RequestID uint64 | |
| @@ -139,77 +138,6 @@ type PickerOffer struct { | |||
| 139 | 138 | 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 | 141 | // PickerProjection is one complete daemon-authorised presentation. Recent and | |
| 214 | 142 | // Grouped contain the same destination keys and actions but may differ in | |
| 215 | 143 | // order, sections, and display labels. | |
| @@ -237,8 +165,7 @@ type PickerSnapshot struct { | |||
| 237 | 165 | ||
| 238 | 166 | // PickerSelection commits the opaque row key displayed at SourceRevision. | |
| 239 | 167 | // 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. | |
| 242 | 169 | type PickerSelection struct { | |
| 243 | 170 | CauseActionID uint64 | |
| 244 | 171 | RequestID uint64 | |
| @@ -300,120 +227,6 @@ type PickerClosed struct { | |||
| 300 | 227 | 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 | 230 | func validPickerIntent(intent PickerIntent) bool { | |
| 418 | 231 | switch intent { | |
| 419 | 232 | case PickerIntentNavigation, PickerIntentMovePane, PickerIntentMoveTab: | |
| @@ -502,14 +315,6 @@ func ValidatePickerOffer(offer PickerOffer) error { | |||
| 502 | 315 | 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 | 318 | // ValidatePickerSnapshot enforces a nonzero interaction/source revision, a | |
| 514 | 319 | // bounded source identity, and unique opaque keys with display safety. Order | |
| 515 | 320 | // 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 it | 125 | // PickerOffer opens one interaction. The daemon sends it for intents it | ||
| 126 | // initiates (a session-picker command or a move request); it carries the | 126 | // initiates (a session-picker command or a move request); it carries the | ||
| 127 | // acquisition barrier the client must display before owning the terminal and | 127 | // acquisition barrier the client must display before owning the terminal and | ||
| 128 | - | // the opaque identity of the captured move source. RequestID echoes the | 128 | + | // the opaque identity of the captured move source. RequestID correlates a |
| 129 | - | // PickerBegin that asked for the interaction, and is zero for daemon-driven | 129 | + | // client-owned interaction request, and is zero for daemon-driven opens. |
| 130 | - | // opens. | |||
| 131 | type PickerOffer struct { | 130 | type PickerOffer struct { | ||
| 132 | InteractionID uint64 | 131 | InteractionID uint64 | ||
| 133 | RequestID uint64 | 132 | RequestID uint64 | ||
| @@ -139,77 +138,6 @@ type PickerOffer struct { | |||||
| 139 | Title string | 138 | 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 and | 141 | // PickerProjection is one complete daemon-authorised presentation. Recent and | ||
| 214 | // Grouped contain the same destination keys and actions but may differ in | 142 | // 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-initiated | 168 | + | // 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 uint64 | 170 | CauseActionID uint64 | ||
| 244 | RequestID uint64 | 171 | RequestID uint64 | ||
| @@ -300,120 +227,6 @@ type PickerClosed struct { | |||||
| 300 | BarrierState uint64 | 227 | 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 nil | 315 | 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, a | 318 | // ValidatePickerSnapshot enforces a nonzero interaction/source revision, a | ||
| 514 | // bounded source identity, and unique opaque keys with display safety. Order | 319 | // 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 ( | |||
| 93 | 93 | ReasonSessionKilled uint8 = 1 | |
| 94 | 94 | ReasonServerShutdown uint8 = 2 | |
| 95 | 95 | ReasonReplaced uint8 = 3 | |
| 96 | + | // ReasonDetachToPicker transfers terminal ownership to the client picker. | |
| 97 | + | ReasonDetachToPicker uint8 = 4 | |
| 96 | 98 | ) | |
| 97 | 99 | ||
| 98 | 100 | // Hello is sent by the client immediately after connecting. | |
| @@ -364,7 +366,7 @@ func validClientNoticeAction(action uint8) bool { | |||
| 364 | 366 | ||
| 365 | 367 | func validDetachedReason(reason uint8) bool { | |
| 366 | 368 | switch reason { | |
| 367 | - | case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced: | |
| 369 | + | case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced, ReasonDetachToPicker: | |
| 368 | 370 | return true | |
| 369 | 371 | default: | |
| 370 | 372 | return false | |
| @@ -93,6 +93,8 @@ const ( | |||||
| 93 | ReasonSessionKilled uint8 = 1 | 93 | ReasonSessionKilled uint8 = 1 | ||
| 94 | ReasonServerShutdown uint8 = 2 | 94 | ReasonServerShutdown uint8 = 2 | ||
| 95 | ReasonReplaced uint8 = 3 | 95 | 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 true | 370 | return true | ||
| 369 | default: | 371 | default: | ||
| 370 | return false | 372 | 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{ | |||
| 20 | 20 | "register": 101, "subscribe": 102, "resync": 103, "unsubscribe": 104, | |
| 21 | 21 | "add_host": 105, "remove_host": 106, "reconcile": 107, "open_stream": 108, | |
| 22 | 22 | "client_stream_data": 109, "close_stream": 110, "update_host_policy": 111, | |
| 23 | + | "start_preview": 112, "cancel_preview": 113, | |
| 23 | 24 | } | |
| 24 | 25 | ||
| 25 | 26 | // brokerServerTags is the frozen server tag inventory. | |
| @@ -27,6 +28,7 @@ var brokerServerTags = map[string]protoreflect.FieldNumber{ | |||
| 27 | 28 | "registered": 201, "snapshot_part": 202, "operation_result": 203, | |
| 28 | 29 | "stream_opened": 204, "server_stream_data": 205, "stream_closed": 206, | |
| 29 | 30 | "progress": 207, "broker_error_message": 208, "shutdown": 209, | |
| 31 | + | "preview_publication": 210, | |
| 30 | 32 | } | |
| 31 | 33 | ||
| 32 | 34 | func oneofTags(t *testing.T, message proto.Message) map[string]protoreflect.FieldNumber { | |
| @@ -174,6 +176,29 @@ func brokerClientSamples() map[string]*BrokerClientEnvelope { | |||
| 174 | 176 | Ref: brokerRef(), Data: bytes.Repeat([]byte{0x66}, 24), | |
| 175 | 177 | }}}, | |
| 176 | 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 | 242 | "shutdown": {Payload: &BrokerServerEnvelope_Shutdown{Shutdown: &Shutdown{ | |
| 218 | 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 | ||
| @@ -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) { | |||
| 39 | 39 | "route_navigation_failure": true, "session_creation_failure": true, | |
| 40 | 40 | "ui_fence": true, "navigation_inventory_request": true, | |
| 41 | 41 | "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, | |
| 44 | 44 | } | |
| 45 | 45 | serverSemantic := map[string]bool{ | |
| 46 | 46 | "attachment_suspended": true, "attachment_activated": true, | |
| @@ -51,9 +51,9 @@ func TestEnvelopeInventoryProvesDirectionAndSemanticPath(t *testing.T) { | |||
| 51 | 51 | "route_navigation_failure": true, "route_position": true, "route_retired": true, | |
| 52 | 52 | "same_peer_switch_failure": true, "ui_receipt": true, "ui_view_update": true, | |
| 53 | 53 | "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, | |
| 57 | 57 | "kill_result": true, | |
| 58 | 58 | } | |
| 59 | 59 | ||
| @@ -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
| @@ -461,59 +461,6 @@ func (x *PickerOffer) GetTitle() string { | |||
| 461 | 461 | return "" | |
| 462 | 462 | } | |
| 463 | 463 | ||
| 464 | - | // PickerBegin asks the daemon to open one interaction. | |
| 465 | - | type PickerBegin struct { | |
| 466 | - | state protoimpl.MessageState `protogen:"open.v1"` | |
| 467 | - | RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` | |
| 468 | - | Intent uint32 `protobuf:"varint,2,opt,name=intent,proto3" json:"intent,omitempty"` // same taxonomy as PickerOffer.intent | |
| 469 | - | unknownFields protoimpl.UnknownFields | |
| 470 | - | sizeCache protoimpl.SizeCache | |
| 471 | - | } | |
| 472 | - | ||
| 473 | - | func (x *PickerBegin) Reset() { | |
| 474 | - | *x = PickerBegin{} | |
| 475 | - | mi := &file_picker_proto_msgTypes[5] | |
| 476 | - | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 477 | - | ms.StoreMessageInfo(mi) | |
| 478 | - | } | |
| 479 | - | ||
| 480 | - | func (x *PickerBegin) String() string { | |
| 481 | - | return protoimpl.X.MessageStringOf(x) | |
| 482 | - | } | |
| 483 | - | ||
| 484 | - | func (*PickerBegin) ProtoMessage() {} | |
| 485 | - | ||
| 486 | - | func (x *PickerBegin) ProtoReflect() protoreflect.Message { | |
| 487 | - | mi := &file_picker_proto_msgTypes[5] | |
| 488 | - | if x != nil { | |
| 489 | - | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 490 | - | if ms.LoadMessageInfo() == nil { | |
| 491 | - | ms.StoreMessageInfo(mi) | |
| 492 | - | } | |
| 493 | - | return ms | |
| 494 | - | } | |
| 495 | - | return mi.MessageOf(x) | |
| 496 | - | } | |
| 497 | - | ||
| 498 | - | // Deprecated: Use PickerBegin.ProtoReflect.Descriptor instead. | |
| 499 | - | func (*PickerBegin) Descriptor() ([]byte, []int) { | |
| 500 | - | return file_picker_proto_rawDescGZIP(), []int{5} | |
| 501 | - | } | |
| 502 | - | ||
| 503 | - | func (x *PickerBegin) GetRequestId() uint64 { | |
| 504 | - | if x != nil { | |
| 505 | - | return x.RequestId | |
| 506 | - | } | |
| 507 | - | return 0 | |
| 508 | - | } | |
| 509 | - | ||
| 510 | - | func (x *PickerBegin) GetIntent() uint32 { | |
| 511 | - | if x != nil { | |
| 512 | - | return x.Intent | |
| 513 | - | } | |
| 514 | - | return 0 | |
| 515 | - | } | |
| 516 | - | ||
| 517 | 464 | // PickerSelection commits the opaque row key at a revision. | |
| 518 | 465 | type PickerSelection struct { | |
| 519 | 466 | state protoimpl.MessageState `protogen:"open.v1"` | |
| @@ -531,7 +478,7 @@ type PickerSelection struct { | |||
| 531 | 478 | ||
| 532 | 479 | func (x *PickerSelection) Reset() { | |
| 533 | 480 | *x = PickerSelection{} | |
| 534 | - | mi := &file_picker_proto_msgTypes[6] | |
| 481 | + | mi := &file_picker_proto_msgTypes[5] | |
| 535 | 482 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 536 | 483 | ms.StoreMessageInfo(mi) | |
| 537 | 484 | } | |
| @@ -543,7 +490,7 @@ func (x *PickerSelection) String() string { | |||
| 543 | 490 | func (*PickerSelection) ProtoMessage() {} | |
| 544 | 491 | ||
| 545 | 492 | func (x *PickerSelection) ProtoReflect() protoreflect.Message { | |
| 546 | - | mi := &file_picker_proto_msgTypes[6] | |
| 493 | + | mi := &file_picker_proto_msgTypes[5] | |
| 547 | 494 | if x != nil { | |
| 548 | 495 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 549 | 496 | if ms.LoadMessageInfo() == nil { | |
| @@ -556,7 +503,7 @@ func (x *PickerSelection) ProtoReflect() protoreflect.Message { | |||
| 556 | 503 | ||
| 557 | 504 | // Deprecated: Use PickerSelection.ProtoReflect.Descriptor instead. | |
| 558 | 505 | func (*PickerSelection) Descriptor() ([]byte, []int) { | |
| 559 | - | return file_picker_proto_rawDescGZIP(), []int{6} | |
| 506 | + | return file_picker_proto_rawDescGZIP(), []int{5} | |
| 560 | 507 | } | |
| 561 | 508 | ||
| 562 | 509 | func (x *PickerSelection) GetCauseActionId() uint64 { | |
| @@ -623,7 +570,7 @@ type PickerResult struct { | |||
| 623 | 570 | ||
| 624 | 571 | func (x *PickerResult) Reset() { | |
| 625 | 572 | *x = PickerResult{} | |
| 626 | - | mi := &file_picker_proto_msgTypes[7] | |
| 573 | + | mi := &file_picker_proto_msgTypes[6] | |
| 627 | 574 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 628 | 575 | ms.StoreMessageInfo(mi) | |
| 629 | 576 | } | |
| @@ -635,7 +582,7 @@ func (x *PickerResult) String() string { | |||
| 635 | 582 | func (*PickerResult) ProtoMessage() {} | |
| 636 | 583 | ||
| 637 | 584 | func (x *PickerResult) ProtoReflect() protoreflect.Message { | |
| 638 | - | mi := &file_picker_proto_msgTypes[7] | |
| 585 | + | mi := &file_picker_proto_msgTypes[6] | |
| 639 | 586 | if x != nil { | |
| 640 | 587 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 641 | 588 | if ms.LoadMessageInfo() == nil { | |
| @@ -648,7 +595,7 @@ func (x *PickerResult) ProtoReflect() protoreflect.Message { | |||
| 648 | 595 | ||
| 649 | 596 | // Deprecated: Use PickerResult.ProtoReflect.Descriptor instead. | |
| 650 | 597 | func (*PickerResult) Descriptor() ([]byte, []int) { | |
| 651 | - | return file_picker_proto_rawDescGZIP(), []int{7} | |
| 598 | + | return file_picker_proto_rawDescGZIP(), []int{6} | |
| 652 | 599 | } | |
| 653 | 600 | ||
| 654 | 601 | func (x *PickerResult) GetCauseActionId() uint64 { | |
| @@ -712,7 +659,7 @@ type PickerFailure struct { | |||
| 712 | 659 | ||
| 713 | 660 | func (x *PickerFailure) Reset() { | |
| 714 | 661 | *x = PickerFailure{} | |
| 715 | - | mi := &file_picker_proto_msgTypes[8] | |
| 662 | + | mi := &file_picker_proto_msgTypes[7] | |
| 716 | 663 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 717 | 664 | ms.StoreMessageInfo(mi) | |
| 718 | 665 | } | |
| @@ -724,7 +671,7 @@ func (x *PickerFailure) String() string { | |||
| 724 | 671 | func (*PickerFailure) ProtoMessage() {} | |
| 725 | 672 | ||
| 726 | 673 | func (x *PickerFailure) ProtoReflect() protoreflect.Message { | |
| 727 | - | mi := &file_picker_proto_msgTypes[8] | |
| 674 | + | mi := &file_picker_proto_msgTypes[7] | |
| 728 | 675 | if x != nil { | |
| 729 | 676 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 730 | 677 | if ms.LoadMessageInfo() == nil { | |
| @@ -737,7 +684,7 @@ func (x *PickerFailure) ProtoReflect() protoreflect.Message { | |||
| 737 | 684 | ||
| 738 | 685 | // Deprecated: Use PickerFailure.ProtoReflect.Descriptor instead. | |
| 739 | 686 | func (*PickerFailure) Descriptor() ([]byte, []int) { | |
| 740 | - | return file_picker_proto_rawDescGZIP(), []int{8} | |
| 687 | + | return file_picker_proto_rawDescGZIP(), []int{7} | |
| 741 | 688 | } | |
| 742 | 689 | ||
| 743 | 690 | func (x *PickerFailure) GetCauseActionId() uint64 { | |
| @@ -800,7 +747,7 @@ type PickerClose struct { | |||
| 800 | 747 | ||
| 801 | 748 | func (x *PickerClose) Reset() { | |
| 802 | 749 | *x = PickerClose{} | |
| 803 | - | mi := &file_picker_proto_msgTypes[9] | |
| 750 | + | mi := &file_picker_proto_msgTypes[8] | |
| 804 | 751 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 805 | 752 | ms.StoreMessageInfo(mi) | |
| 806 | 753 | } | |
| @@ -812,7 +759,7 @@ func (x *PickerClose) String() string { | |||
| 812 | 759 | func (*PickerClose) ProtoMessage() {} | |
| 813 | 760 | ||
| 814 | 761 | func (x *PickerClose) ProtoReflect() protoreflect.Message { | |
| 815 | - | mi := &file_picker_proto_msgTypes[9] | |
| 762 | + | mi := &file_picker_proto_msgTypes[8] | |
| 816 | 763 | if x != nil { | |
| 817 | 764 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 818 | 765 | if ms.LoadMessageInfo() == nil { | |
| @@ -825,7 +772,7 @@ func (x *PickerClose) ProtoReflect() protoreflect.Message { | |||
| 825 | 772 | ||
| 826 | 773 | // Deprecated: Use PickerClose.ProtoReflect.Descriptor instead. | |
| 827 | 774 | func (*PickerClose) Descriptor() ([]byte, []int) { | |
| 828 | - | return file_picker_proto_rawDescGZIP(), []int{9} | |
| 775 | + | return file_picker_proto_rawDescGZIP(), []int{8} | |
| 829 | 776 | } | |
| 830 | 777 | ||
| 831 | 778 | func (x *PickerClose) GetInteractionId() uint64 { | |
| @@ -854,7 +801,7 @@ type PickerClosed struct { | |||
| 854 | 801 | ||
| 855 | 802 | func (x *PickerClosed) Reset() { | |
| 856 | 803 | *x = PickerClosed{} | |
| 857 | - | mi := &file_picker_proto_msgTypes[10] | |
| 804 | + | mi := &file_picker_proto_msgTypes[9] | |
| 858 | 805 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 859 | 806 | ms.StoreMessageInfo(mi) | |
| 860 | 807 | } | |
| @@ -866,7 +813,7 @@ func (x *PickerClosed) String() string { | |||
| 866 | 813 | func (*PickerClosed) ProtoMessage() {} | |
| 867 | 814 | ||
| 868 | 815 | func (x *PickerClosed) ProtoReflect() protoreflect.Message { | |
| 869 | - | mi := &file_picker_proto_msgTypes[10] | |
| 816 | + | mi := &file_picker_proto_msgTypes[9] | |
| 870 | 817 | if x != nil { | |
| 871 | 818 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |
| 872 | 819 | if ms.LoadMessageInfo() == nil { | |
| @@ -879,7 +826,7 @@ func (x *PickerClosed) ProtoReflect() protoreflect.Message { | |||
| 879 | 826 | ||
| 880 | 827 | // Deprecated: Use PickerClosed.ProtoReflect.Descriptor instead. | |
| 881 | 828 | func (*PickerClosed) Descriptor() ([]byte, []int) { | |
| 882 | - | return file_picker_proto_rawDescGZIP(), []int{10} | |
| 829 | + | return file_picker_proto_rawDescGZIP(), []int{9} | |
| 883 | 830 | } | |
| 884 | 831 | ||
| 885 | 832 | func (x *PickerClosed) GetInteractionId() uint64 { | |
| @@ -947,11 +894,7 @@ const file_picker_proto_rawDesc = "" + | |||
| 947 | 894 | "\rbarrier_state\x18\x06 \x01(\x04R\fbarrierState\x12\x1d\n" + | |
| 948 | 895 | "\n" + | |
| 949 | 896 | "size_epoch\x18\a \x01(\x04R\tsizeEpoch\x12\x14\n" + | |
| 950 | - | "\x05title\x18\b \x01(\tR\x05title\"D\n" + | |
| 951 | - | "\vPickerBegin\x12\x1d\n" + | |
| 952 | - | "\n" + | |
| 953 | - | "request_id\x18\x01 \x01(\x04R\trequestId\x12\x16\n" + | |
| 954 | - | "\x06intent\x18\x02 \x01(\rR\x06intent\"\xef\x01\n" + | |
| 897 | + | "\x05title\x18\b \x01(\tR\x05title\"\xef\x01\n" + | |
| 955 | 898 | "\x0fPickerSelection\x12&\n" + | |
| 956 | 899 | "\x0fcause_action_id\x18\x01 \x01(\x04R\rcauseActionId\x12\x1d\n" + | |
| 957 | 900 | "\n" + | |
| @@ -999,19 +942,18 @@ func file_picker_proto_rawDescGZIP() []byte { | |||
| 999 | 942 | return file_picker_proto_rawDescData | |
| 1000 | 943 | } | |
| 1001 | 944 | ||
| 1002 | - | var file_picker_proto_msgTypes = make([]protoimpl.MessageInfo, 11) | |
| 945 | + | var file_picker_proto_msgTypes = make([]protoimpl.MessageInfo, 10) | |
| 1003 | 946 | var file_picker_proto_goTypes = []any{ | |
| 1004 | 947 | (*PickerLine)(nil), // 0: vev.wire.v1.PickerLine | |
| 1005 | 948 | (*PickerCursor)(nil), // 1: vev.wire.v1.PickerCursor | |
| 1006 | 949 | (*PickerProjection)(nil), // 2: vev.wire.v1.PickerProjection | |
| 1007 | 950 | (*PickerSnapshot)(nil), // 3: vev.wire.v1.PickerSnapshot | |
| 1008 | 951 | (*PickerOffer)(nil), // 4: vev.wire.v1.PickerOffer | |
| 1009 | - | (*PickerBegin)(nil), // 5: vev.wire.v1.PickerBegin | |
| 1010 | - | (*PickerSelection)(nil), // 6: vev.wire.v1.PickerSelection | |
| 1011 | - | (*PickerResult)(nil), // 7: vev.wire.v1.PickerResult | |
| 1012 | - | (*PickerFailure)(nil), // 8: vev.wire.v1.PickerFailure | |
| 1013 | - | (*PickerClose)(nil), // 9: vev.wire.v1.PickerClose | |
| 1014 | - | (*PickerClosed)(nil), // 10: vev.wire.v1.PickerClosed | |
| 952 | + | (*PickerSelection)(nil), // 5: vev.wire.v1.PickerSelection | |
| 953 | + | (*PickerResult)(nil), // 6: vev.wire.v1.PickerResult | |
| 954 | + | (*PickerFailure)(nil), // 7: vev.wire.v1.PickerFailure | |
| 955 | + | (*PickerClose)(nil), // 8: vev.wire.v1.PickerClose | |
| 956 | + | (*PickerClosed)(nil), // 9: vev.wire.v1.PickerClosed | |
| 1015 | 957 | } | |
| 1016 | 958 | var file_picker_proto_depIdxs = []int32{ | |
| 1017 | 959 | 0, // 0: vev.wire.v1.PickerProjection.lines:type_name -> vev.wire.v1.PickerLine | |
| @@ -1036,7 +978,7 @@ func file_picker_proto_init() { | |||
| 1036 | 978 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | |
| 1037 | 979 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_picker_proto_rawDesc), len(file_picker_proto_rawDesc)), | |
| 1038 | 980 | NumEnums: 0, | |
| 1039 | - | NumMessages: 11, | |
| 981 | + | NumMessages: 10, | |
| 1040 | 982 | NumExtensions: 0, | |
| 1041 | 983 | NumServices: 0, | |
| 1042 | 984 | }, | |
| @@ -461,59 +461,6 @@ func (x *PickerOffer) GetTitle() string { | |||||
| 461 | return "" | 461 | return "" | ||
| 462 | } | 462 | } | ||
| 463 | 463 | ||||
| 464 | - | // PickerBegin asks the daemon to open one interaction. | |||
| 465 | - | type PickerBegin struct { | |||
| 466 | - | state protoimpl.MessageState `protogen:"open.v1"` | |||
| 467 | - | RequestId uint64 `protobuf:"varint,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` | |||
| 468 | - | Intent uint32 `protobuf:"varint,2,opt,name=intent,proto3" json:"intent,omitempty"` // same taxonomy as PickerOffer.intent | |||
| 469 | - | unknownFields protoimpl.UnknownFields | |||
| 470 | - | sizeCache protoimpl.SizeCache | |||
| 471 | - | } | |||
| 472 | - | ||||
| 473 | - | func (x *PickerBegin) Reset() { | |||
| 474 | - | *x = PickerBegin{} | |||
| 475 | - | mi := &file_picker_proto_msgTypes[5] | |||
| 476 | - | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 477 | - | ms.StoreMessageInfo(mi) | |||
| 478 | - | } | |||
| 479 | - | ||||
| 480 | - | func (x *PickerBegin) String() string { | |||
| 481 | - | return protoimpl.X.MessageStringOf(x) | |||
| 482 | - | } | |||
| 483 | - | ||||
| 484 | - | func (*PickerBegin) ProtoMessage() {} | |||
| 485 | - | ||||
| 486 | - | func (x *PickerBegin) ProtoReflect() protoreflect.Message { | |||
| 487 | - | mi := &file_picker_proto_msgTypes[5] | |||
| 488 | - | if x != nil { | |||
| 489 | - | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | |||
| 490 | - | if ms.LoadMessageInfo() == nil { | |||
| 491 | - | ms.StoreMessageInfo(mi) | |||
| 492 | - | } | |||
| 493 | - | return ms | |||
| 494 | - | } | |||
| 495 | - | return mi.MessageOf(x) | |||
| 496 | - | } | |||
| 497 | - | ||||
| 498 | - | // Deprecated: Use PickerBegin.ProtoReflect.Descriptor instead. | |||
| 499 | - | func (*PickerBegin) Descriptor() ([]byte, []int) { | |||
| 500 | - | return file_picker_proto_rawDescGZIP(), []int{5} | |||
| 501 | - | } | |||
| 502 | - | ||||
| 503 | - | func (x *PickerBegin) GetRequestId() uint64 { | |||
| 504 | - | if x != nil { | |||
| 505 | - | return x.RequestId | |||
| 506 | - | } | |||
| 507 | - | return 0 | |||
| 508 | - | } | |||
| 509 | - | ||||
| 510 | - | func (x *PickerBegin) GetIntent() uint32 { | |||
| 511 | - | if x != nil { | |||
| 512 | - | return x.Intent | |||
| 513 | - | } | |||
| 514 | - | return 0 | |||
| 515 | - | } | |||
| 516 | - | ||||
| 517 | // PickerSelection commits the opaque row key at a revision. | 464 | // PickerSelection commits the opaque row key at a revision. | ||
| 518 | type PickerSelection struct { | 465 | type PickerSelection struct { | ||
| 519 | state protoimpl.MessageState `protogen:"open.v1"` | 466 | state protoimpl.MessageState `protogen:"open.v1"` | ||
| @@ -531,7 +478,7 @@ type PickerSelection struct { | |||||
| 531 | 478 | ||||
| 532 | func (x *PickerSelection) Reset() { | 479 | func (x *PickerSelection) Reset() { | ||
| 533 | *x = PickerSelection{} | 480 | *x = PickerSelection{} | ||
| 534 | - | mi := &file_picker_proto_msgTypes[6] | 481 | + | mi := &file_picker_proto_msgTypes[5] |
| 535 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 482 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 536 | ms.StoreMessageInfo(mi) | 483 | ms.StoreMessageInfo(mi) | ||
| 537 | } | 484 | } | ||
| @@ -543,7 +490,7 @@ func (x *PickerSelection) String() string { | |||||
| 543 | func (*PickerSelection) ProtoMessage() {} | 490 | func (*PickerSelection) ProtoMessage() {} | ||
| 544 | 491 | ||||
| 545 | func (x *PickerSelection) ProtoReflect() protoreflect.Message { | 492 | func (x *PickerSelection) ProtoReflect() protoreflect.Message { | ||
| 546 | - | mi := &file_picker_proto_msgTypes[6] | 493 | + | mi := &file_picker_proto_msgTypes[5] |
| 547 | if x != nil { | 494 | if x != nil { | ||
| 548 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 495 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 549 | if ms.LoadMessageInfo() == nil { | 496 | if ms.LoadMessageInfo() == nil { | ||
| @@ -556,7 +503,7 @@ func (x *PickerSelection) ProtoReflect() protoreflect.Message { | |||||
| 556 | 503 | ||||
| 557 | // Deprecated: Use PickerSelection.ProtoReflect.Descriptor instead. | 504 | // Deprecated: Use PickerSelection.ProtoReflect.Descriptor instead. | ||
| 558 | func (*PickerSelection) Descriptor() ([]byte, []int) { | 505 | func (*PickerSelection) Descriptor() ([]byte, []int) { | ||
| 559 | - | return file_picker_proto_rawDescGZIP(), []int{6} | 506 | + | return file_picker_proto_rawDescGZIP(), []int{5} |
| 560 | } | 507 | } | ||
| 561 | 508 | ||||
| 562 | func (x *PickerSelection) GetCauseActionId() uint64 { | 509 | func (x *PickerSelection) GetCauseActionId() uint64 { | ||
| @@ -623,7 +570,7 @@ type PickerResult struct { | |||||
| 623 | 570 | ||||
| 624 | func (x *PickerResult) Reset() { | 571 | func (x *PickerResult) Reset() { | ||
| 625 | *x = PickerResult{} | 572 | *x = PickerResult{} | ||
| 626 | - | mi := &file_picker_proto_msgTypes[7] | 573 | + | mi := &file_picker_proto_msgTypes[6] |
| 627 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 574 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 628 | ms.StoreMessageInfo(mi) | 575 | ms.StoreMessageInfo(mi) | ||
| 629 | } | 576 | } | ||
| @@ -635,7 +582,7 @@ func (x *PickerResult) String() string { | |||||
| 635 | func (*PickerResult) ProtoMessage() {} | 582 | func (*PickerResult) ProtoMessage() {} | ||
| 636 | 583 | ||||
| 637 | func (x *PickerResult) ProtoReflect() protoreflect.Message { | 584 | func (x *PickerResult) ProtoReflect() protoreflect.Message { | ||
| 638 | - | mi := &file_picker_proto_msgTypes[7] | 585 | + | mi := &file_picker_proto_msgTypes[6] |
| 639 | if x != nil { | 586 | if x != nil { | ||
| 640 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 587 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 641 | if ms.LoadMessageInfo() == nil { | 588 | if ms.LoadMessageInfo() == nil { | ||
| @@ -648,7 +595,7 @@ func (x *PickerResult) ProtoReflect() protoreflect.Message { | |||||
| 648 | 595 | ||||
| 649 | // Deprecated: Use PickerResult.ProtoReflect.Descriptor instead. | 596 | // Deprecated: Use PickerResult.ProtoReflect.Descriptor instead. | ||
| 650 | func (*PickerResult) Descriptor() ([]byte, []int) { | 597 | func (*PickerResult) Descriptor() ([]byte, []int) { | ||
| 651 | - | return file_picker_proto_rawDescGZIP(), []int{7} | 598 | + | return file_picker_proto_rawDescGZIP(), []int{6} |
| 652 | } | 599 | } | ||
| 653 | 600 | ||||
| 654 | func (x *PickerResult) GetCauseActionId() uint64 { | 601 | func (x *PickerResult) GetCauseActionId() uint64 { | ||
| @@ -712,7 +659,7 @@ type PickerFailure struct { | |||||
| 712 | 659 | ||||
| 713 | func (x *PickerFailure) Reset() { | 660 | func (x *PickerFailure) Reset() { | ||
| 714 | *x = PickerFailure{} | 661 | *x = PickerFailure{} | ||
| 715 | - | mi := &file_picker_proto_msgTypes[8] | 662 | + | mi := &file_picker_proto_msgTypes[7] |
| 716 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 663 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 717 | ms.StoreMessageInfo(mi) | 664 | ms.StoreMessageInfo(mi) | ||
| 718 | } | 665 | } | ||
| @@ -724,7 +671,7 @@ func (x *PickerFailure) String() string { | |||||
| 724 | func (*PickerFailure) ProtoMessage() {} | 671 | func (*PickerFailure) ProtoMessage() {} | ||
| 725 | 672 | ||||
| 726 | func (x *PickerFailure) ProtoReflect() protoreflect.Message { | 673 | func (x *PickerFailure) ProtoReflect() protoreflect.Message { | ||
| 727 | - | mi := &file_picker_proto_msgTypes[8] | 674 | + | mi := &file_picker_proto_msgTypes[7] |
| 728 | if x != nil { | 675 | if x != nil { | ||
| 729 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 676 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 730 | if ms.LoadMessageInfo() == nil { | 677 | if ms.LoadMessageInfo() == nil { | ||
| @@ -737,7 +684,7 @@ func (x *PickerFailure) ProtoReflect() protoreflect.Message { | |||||
| 737 | 684 | ||||
| 738 | // Deprecated: Use PickerFailure.ProtoReflect.Descriptor instead. | 685 | // Deprecated: Use PickerFailure.ProtoReflect.Descriptor instead. | ||
| 739 | func (*PickerFailure) Descriptor() ([]byte, []int) { | 686 | func (*PickerFailure) Descriptor() ([]byte, []int) { | ||
| 740 | - | return file_picker_proto_rawDescGZIP(), []int{8} | 687 | + | return file_picker_proto_rawDescGZIP(), []int{7} |
| 741 | } | 688 | } | ||
| 742 | 689 | ||||
| 743 | func (x *PickerFailure) GetCauseActionId() uint64 { | 690 | func (x *PickerFailure) GetCauseActionId() uint64 { | ||
| @@ -800,7 +747,7 @@ type PickerClose struct { | |||||
| 800 | 747 | ||||
| 801 | func (x *PickerClose) Reset() { | 748 | func (x *PickerClose) Reset() { | ||
| 802 | *x = PickerClose{} | 749 | *x = PickerClose{} | ||
| 803 | - | mi := &file_picker_proto_msgTypes[9] | 750 | + | mi := &file_picker_proto_msgTypes[8] |
| 804 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 751 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 805 | ms.StoreMessageInfo(mi) | 752 | ms.StoreMessageInfo(mi) | ||
| 806 | } | 753 | } | ||
| @@ -812,7 +759,7 @@ func (x *PickerClose) String() string { | |||||
| 812 | func (*PickerClose) ProtoMessage() {} | 759 | func (*PickerClose) ProtoMessage() {} | ||
| 813 | 760 | ||||
| 814 | func (x *PickerClose) ProtoReflect() protoreflect.Message { | 761 | func (x *PickerClose) ProtoReflect() protoreflect.Message { | ||
| 815 | - | mi := &file_picker_proto_msgTypes[9] | 762 | + | mi := &file_picker_proto_msgTypes[8] |
| 816 | if x != nil { | 763 | if x != nil { | ||
| 817 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 764 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 818 | if ms.LoadMessageInfo() == nil { | 765 | if ms.LoadMessageInfo() == nil { | ||
| @@ -825,7 +772,7 @@ func (x *PickerClose) ProtoReflect() protoreflect.Message { | |||||
| 825 | 772 | ||||
| 826 | // Deprecated: Use PickerClose.ProtoReflect.Descriptor instead. | 773 | // Deprecated: Use PickerClose.ProtoReflect.Descriptor instead. | ||
| 827 | func (*PickerClose) Descriptor() ([]byte, []int) { | 774 | func (*PickerClose) Descriptor() ([]byte, []int) { | ||
| 828 | - | return file_picker_proto_rawDescGZIP(), []int{9} | 775 | + | return file_picker_proto_rawDescGZIP(), []int{8} |
| 829 | } | 776 | } | ||
| 830 | 777 | ||||
| 831 | func (x *PickerClose) GetInteractionId() uint64 { | 778 | func (x *PickerClose) GetInteractionId() uint64 { | ||
| @@ -854,7 +801,7 @@ type PickerClosed struct { | |||||
| 854 | 801 | ||||
| 855 | func (x *PickerClosed) Reset() { | 802 | func (x *PickerClosed) Reset() { | ||
| 856 | *x = PickerClosed{} | 803 | *x = PickerClosed{} | ||
| 857 | - | mi := &file_picker_proto_msgTypes[10] | 804 | + | mi := &file_picker_proto_msgTypes[9] |
| 858 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 805 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 859 | ms.StoreMessageInfo(mi) | 806 | ms.StoreMessageInfo(mi) | ||
| 860 | } | 807 | } | ||
| @@ -866,7 +813,7 @@ func (x *PickerClosed) String() string { | |||||
| 866 | func (*PickerClosed) ProtoMessage() {} | 813 | func (*PickerClosed) ProtoMessage() {} | ||
| 867 | 814 | ||||
| 868 | func (x *PickerClosed) ProtoReflect() protoreflect.Message { | 815 | func (x *PickerClosed) ProtoReflect() protoreflect.Message { | ||
| 869 | - | mi := &file_picker_proto_msgTypes[10] | 816 | + | mi := &file_picker_proto_msgTypes[9] |
| 870 | if x != nil { | 817 | if x != nil { | ||
| 871 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | 818 | ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) | ||
| 872 | if ms.LoadMessageInfo() == nil { | 819 | if ms.LoadMessageInfo() == nil { | ||
| @@ -879,7 +826,7 @@ func (x *PickerClosed) ProtoReflect() protoreflect.Message { | |||||
| 879 | 826 | ||||
| 880 | // Deprecated: Use PickerClosed.ProtoReflect.Descriptor instead. | 827 | // Deprecated: Use PickerClosed.ProtoReflect.Descriptor instead. | ||
| 881 | func (*PickerClosed) Descriptor() ([]byte, []int) { | 828 | func (*PickerClosed) Descriptor() ([]byte, []int) { | ||
| 882 | - | return file_picker_proto_rawDescGZIP(), []int{10} | 829 | + | return file_picker_proto_rawDescGZIP(), []int{9} |
| 883 | } | 830 | } | ||
| 884 | 831 | ||||
| 885 | func (x *PickerClosed) GetInteractionId() uint64 { | 832 | func (x *PickerClosed) GetInteractionId() uint64 { | ||
| @@ -947,11 +894,7 @@ const file_picker_proto_rawDesc = "" + | |||||
| 947 | "\rbarrier_state\x18\x06 \x01(\x04R\fbarrierState\x12\x1d\n" + | 894 | "\rbarrier_state\x18\x06 \x01(\x04R\fbarrierState\x12\x1d\n" + | ||
| 948 | "\n" + | 895 | "\n" + | ||
| 949 | "size_epoch\x18\a \x01(\x04R\tsizeEpoch\x12\x14\n" + | 896 | "size_epoch\x18\a \x01(\x04R\tsizeEpoch\x12\x14\n" + | ||
| 950 | - | "\x05title\x18\b \x01(\tR\x05title\"D\n" + | 897 | + | "\x05title\x18\b \x01(\tR\x05title\"\xef\x01\n" + |
| 951 | - | "\vPickerBegin\x12\x1d\n" + | |||
| 952 | - | "\n" + | |||
| 953 | - | "request_id\x18\x01 \x01(\x04R\trequestId\x12\x16\n" + | |||
| 954 | - | "\x06intent\x18\x02 \x01(\rR\x06intent\"\xef\x01\n" + | |||
| 955 | "\x0fPickerSelection\x12&\n" + | 898 | "\x0fPickerSelection\x12&\n" + | ||
| 956 | "\x0fcause_action_id\x18\x01 \x01(\x04R\rcauseActionId\x12\x1d\n" + | 899 | "\x0fcause_action_id\x18\x01 \x01(\x04R\rcauseActionId\x12\x1d\n" + | ||
| 957 | "\n" + | 900 | "\n" + | ||
| @@ -999,19 +942,18 @@ func file_picker_proto_rawDescGZIP() []byte { | |||||
| 999 | return file_picker_proto_rawDescData | 942 | return file_picker_proto_rawDescData | ||
| 1000 | } | 943 | } | ||
| 1001 | 944 | ||||
| 1002 | - | var file_picker_proto_msgTypes = make([]protoimpl.MessageInfo, 11) | 945 | + | var file_picker_proto_msgTypes = make([]protoimpl.MessageInfo, 10) |
| 1003 | var file_picker_proto_goTypes = []any{ | 946 | var file_picker_proto_goTypes = []any{ | ||
| 1004 | (*PickerLine)(nil), // 0: vev.wire.v1.PickerLine | 947 | (*PickerLine)(nil), // 0: vev.wire.v1.PickerLine | ||
| 1005 | (*PickerCursor)(nil), // 1: vev.wire.v1.PickerCursor | 948 | (*PickerCursor)(nil), // 1: vev.wire.v1.PickerCursor | ||
| 1006 | (*PickerProjection)(nil), // 2: vev.wire.v1.PickerProjection | 949 | (*PickerProjection)(nil), // 2: vev.wire.v1.PickerProjection | ||
| 1007 | (*PickerSnapshot)(nil), // 3: vev.wire.v1.PickerSnapshot | 950 | (*PickerSnapshot)(nil), // 3: vev.wire.v1.PickerSnapshot | ||
| 1008 | (*PickerOffer)(nil), // 4: vev.wire.v1.PickerOffer | 951 | (*PickerOffer)(nil), // 4: vev.wire.v1.PickerOffer | ||
| 1009 | - | (*PickerBegin)(nil), // 5: vev.wire.v1.PickerBegin | 952 | + | (*PickerSelection)(nil), // 5: vev.wire.v1.PickerSelection |
| 1010 | - | (*PickerSelection)(nil), // 6: vev.wire.v1.PickerSelection | 953 | + | (*PickerResult)(nil), // 6: vev.wire.v1.PickerResult |
| 1011 | - | (*PickerResult)(nil), // 7: vev.wire.v1.PickerResult | 954 | + | (*PickerFailure)(nil), // 7: vev.wire.v1.PickerFailure |
| 1012 | - | (*PickerFailure)(nil), // 8: vev.wire.v1.PickerFailure | 955 | + | (*PickerClose)(nil), // 8: vev.wire.v1.PickerClose |
| 1013 | - | (*PickerClose)(nil), // 9: vev.wire.v1.PickerClose | 956 | + | (*PickerClosed)(nil), // 9: vev.wire.v1.PickerClosed |
| 1014 | - | (*PickerClosed)(nil), // 10: vev.wire.v1.PickerClosed | |||
| 1015 | } | 957 | } | ||
| 1016 | var file_picker_proto_depIdxs = []int32{ | 958 | var file_picker_proto_depIdxs = []int32{ | ||
| 1017 | 0, // 0: vev.wire.v1.PickerProjection.lines:type_name -> vev.wire.v1.PickerLine | 959 | 0, // 0: vev.wire.v1.PickerProjection.lines:type_name -> vev.wire.v1.PickerLine | ||
| @@ -1036,7 +978,7 @@ func file_picker_proto_init() { | |||||
| 1036 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | 978 | GoPackagePath: reflect.TypeOf(x{}).PkgPath(), | ||
| 1037 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_picker_proto_rawDesc), len(file_picker_proto_rawDesc)), | 979 | RawDescriptor: unsafe.Slice(unsafe.StringData(file_picker_proto_rawDesc), len(file_picker_proto_rawDesc)), | ||
| 1038 | NumEnums: 0, | 980 | NumEnums: 0, | ||
| 1039 | - | NumMessages: 11, | 981 | + | NumMessages: 10, |
| 1040 | NumExtensions: 0, | 982 | NumExtensions: 0, | ||
| 1041 | NumServices: 0, | 983 | NumServices: 0, | ||
| 1042 | }, | 984 | }, | ||
M internal/protocol/wire/schema/broker.proto +36 -1
| @@ -1,7 +1,7 @@ | |||
| 1 | 1 | // Broker wire schema: the client/server broker conversations. | |
| 2 | 2 | // | |
| 3 | 3 | // 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 | |
| 5 | 5 | // every session tag, and each direction is its own closed oneof union. | |
| 6 | 6 | // Scope (broker epoch plus client connection) is carried explicitly on | |
| 7 | 7 | // every payload so stream IDs and events never cross a scope. Semantic | |
| @@ -12,6 +12,7 @@ syntax = "proto3"; | |||
| 12 | 12 | package vev.wire.v1; | |
| 13 | 13 | ||
| 14 | 14 | import "common.proto"; | |
| 15 | + | import "terminal.proto"; | |
| 15 | 16 | ||
| 16 | 17 | option go_package = "github.com/bnema/vev/internal/protocol/wire"; | |
| 17 | 18 | ||
| @@ -86,6 +87,8 @@ message BrokerClientEnvelope { | |||
| 86 | 87 | ClientStreamData client_stream_data = 109; | |
| 87 | 88 | CloseStream close_stream = 110; | |
| 88 | 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 | 105 | Progress progress = 207; | |
| 103 | 106 | BrokerErrorMessage broker_error_message = 208; | |
| 104 | 107 | Shutdown shutdown = 209; | |
| 108 | + | PreviewPublication preview_publication = 210; | |
| 105 | 109 | } | |
| 106 | 110 | } | |
| 107 | 111 | ||
| @@ -356,6 +360,37 @@ message Shutdown { | |||
| 356 | 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 | 394 | // BrokerCatalogSession mirrors catalogue.RemoteCatalogSession exactly: the | |
| 360 | 395 | // lifecycle identity is a fixed 16-byte value, the state is a closed | |
| 361 | 396 | // 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 session | 3 | // 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 |
| 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 on | 6 | // Scope (broker epoch plus client connection) is carried explicitly on | ||
| 7 | // every payload so stream IDs and events never cross a scope. Semantic | 7 | // 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: the | 394 | // BrokerCatalogSession mirrors catalogue.RemoteCatalogSession exactly: the | ||
| 360 | // lifecycle identity is a fixed 16-byte value, the state is a closed | 395 | // lifecycle identity is a fixed 16-byte value, the state is a closed | ||
| 361 | // taxonomy, and the tab list keeps its presence separately from its | 396 | // 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 { | |||
| 91 | 91 | NavigationInventoryRequest navigation_inventory_request = 21; | |
| 92 | 92 | NavigationInventoryPublication navigation_inventory_publication = 22; | |
| 93 | 93 | NavigationInventoryFailure navigation_inventory_failure = 23; | |
| 94 | - | PickerBegin picker_begin = 24; | |
| 95 | 94 | PickerClose picker_close = 25; | |
| 96 | 95 | PickerSelection picker_selection = 26; | |
| 97 | 96 | PickerPreviewRequest picker_preview_request = 27; | |
| 98 | - | PickerControlRequest picker_control_request = 28; | |
| 99 | 97 | SuspendAttachment suspend_attachment = 29; | |
| 100 | 98 | ActivateAttachment activate_attachment = 30; | |
| 101 | 99 | } | |
| @@ -135,7 +133,6 @@ message ServerEnvelope { | |||
| 135 | 133 | PickerResult picker_result = 25; | |
| 136 | 134 | PickerFailure picker_failure = 26; | |
| 137 | 135 | PickerPreview picker_preview = 27; | |
| 138 | - | PickerControlResponse picker_control_response = 28; | |
| 139 | 136 | AttachmentSuspended attachment_suspended = 31; | |
| 140 | 137 | AttachmentActivated attachment_activated = 32; | |
| 141 | 138 | 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"; | |||
| 4 | 4 | package vev.wire.v1; | |
| 5 | 5 | ||
| 6 | 6 | import "common.proto"; | |
| 7 | - | import "picker.proto"; | |
| 8 | 7 | import "session.proto"; | |
| 9 | 8 | ||
| 10 | 9 | option go_package = "github.com/bnema/vev/internal/protocol/wire"; | |
| @@ -83,37 +82,3 @@ message NavigationInventoryFailure { | |||
| 83 | 82 | // 6 = restore failed. | |
| 84 | 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 | - | } | |
| @@ -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 { | |||
| 61 | 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 | 64 | // PickerSelection commits the opaque row key at a revision. | |
| 71 | 65 | message PickerSelection { | |
| 72 | 66 | 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; | ||