detached be1cf92

refactor(protocol): extract presentation contracts (#227)

* refactor(protocol): extract presentation contracts

* docs(architecture): describe presentation contract ownership

* test(protocol): check decoded same-peer switch types

* test(protocol): isolate route validation cases
commit
be1cf92256b87ad8a550fa1859f00e291b031861
author
bnema · · 2026-08-28 11:44 UTC
committer
GitHub · · 2026-08-28 11:44 UTC
parent
040f576
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
117 files · +2183 -2059
M AGENTS.md +1 -1
@@ -61,7 +61,7 @@ Before touching daemon teardown paths, read the lock-ordering notes at the top o
61 61
62 ## Wire protocol62 ## Wire protocol
63 63
64-Wire payload types/codecs live in `internal/ports/frame.go` and `internal/ports/wire.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.64+Semantic presentation and route values live in `internal/protocol`; session-control values live in `internal/ports/session_control.go`. Frame IDs and codecs live in `internal/ports/frame.go`, `internal/ports/wire.go`, `internal/ports/routes_wire.go`, and `internal/ports/wire_command.go`. Connection framing lives in `internal/adapters/ipc/transport.go`.
65 65
66 - IPC frames on a connection are 4-byte big-endian length, 1 type byte, then payload.66 - IPC frames on a connection are 4-byte big-endian length, 1 type byte, then payload.
67 - Client message types occupy `1–13`, `15`, `32–33`, and `35` (`MsgParkedRouteRequest`); server types occupy `16–23`, `25–31`, `34`, and `36` (`MsgParkedRouteResponse`). Types `14` and `24` remain reserved.67 - Client message types occupy `1–13`, `15`, `32–33`, and `35` (`MsgParkedRouteRequest`); server types occupy `16–23`, `25–31`, `34`, and `36` (`MsgParkedRouteResponse`). Types `14` and `24` remain reserved.
M internal/adapters/dgram/network_sim_test.go +4 -3
@@ -11,6 +11,7 @@ import (
11 11
12 "github.com/bnema/vev/internal/domain"12 "github.com/bnema/vev/internal/domain"
13 "github.com/bnema/vev/internal/ports"13 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
14 pdgram "github.com/bnema/vev/pkg/dgram"15 pdgram "github.com/bnema/vev/pkg/dgram"
15 )16 )
16 17
@@ -452,7 +453,7 @@ func TestTransportFloodClassification(t *testing.T) {
452 }453 }
453 }454 }
454 b.mu.Unlock()455 b.mu.Unlock()
455- primer := mustMarshalOutput(ports.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("primer")})456+ primer := mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("primer")})
456 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: primer}); err != nil {457 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: primer}); err != nil {
457 t.Fatal(err)458 t.Fatal(err)
458 }459 }
@@ -529,7 +530,7 @@ func TestTransportFloodClassification(t *testing.T) {
529 }530 }
530 })531 })
531 for state := 1; state < outputCount; state++ {532 for state := 1; state < outputCount; state++ {
532- payload := mustMarshalOutput(ports.Output{533+ payload := mustMarshalOutput(protocol.Output{
533 Epoch: 1,534 Epoch: 1,
534 Base: uint64(state),535 Base: uint64(state),
535 New: uint64(state + 1),536 New: uint64(state + 1),
@@ -854,7 +855,7 @@ func floodState(sender, receiver *Transport, link *simulatedLink) floodTransport
854 }855 }
855 856
856 func floodOutputPayload(baseState, mtu int) []byte {857 func floodOutputPayload(baseState, mtu int) []byte {
857- return mustMarshalOutput(ports.Output{858+ return mustMarshalOutput(protocol.Output{
858 Epoch: 1,859 Epoch: 1,
859 Base: uint64(baseState),860 Base: uint64(baseState),
860 New: uint64(baseState + 1),861 New: uint64(baseState + 1),
M internal/adapters/dgram/proxy_test.go +2 -1
@@ -12,6 +12,7 @@ import (
12 12
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
15 )16 )
16 17
17 type fakeTransport struct {18 type fakeTransport struct {
@@ -105,7 +106,7 @@ func TestProxyRuntimeClampsDatagramHelloOutputWindowAndPreservesControl(t *testi
105 }106 }
106 107
107 input := ports.Frame{Type: ports.MsgInput, Payload: []byte("still flowing")}108 input := ports.Frame{Type: ports.MsgInput, Payload: []byte("still flowing")}
108- ack := ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(ports.Ack{Epoch: 1, State: 7})}109+ ack := ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(protocol.Ack{Epoch: 1, State: 7})}
109 client.recv <- recvResult{frame: input}110 client.recv <- recvResult{frame: input}
110 client.recv <- recvResult{frame: ack}111 client.recv <- recvResult{frame: ack}
111 for _, want := range []ports.Frame{input, ack} {112 for _, want := range []ports.Frame{input, ack} {
M internal/adapters/dgram/transport_send_test.go +4 -3
@@ -9,6 +9,7 @@ import (
9 9
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 pdgram "github.com/bnema/vev/pkg/dgram"13 pdgram "github.com/bnema/vev/pkg/dgram"
13 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
14 )15 )
@@ -114,7 +115,7 @@ func TestOutputPacingSendsAtMostOneOversizedFramePerTick(t *testing.T) {
114 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("prime")}); err != nil {115 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: []byte("prime")}); err != nil {
115 t.Fatal(err)116 t.Fatal(err)
116 }117 }
117- large := mustMarshalOutput(ports.Output{Epoch: 1, Base: 1, New: 2, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 24*a.mtu)})118+ large := mustMarshalOutput(protocol.Output{Epoch: 1, Base: 1, New: 2, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 24*a.mtu)})
118 for range 2 {119 for range 2 {
119 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: large}); err != nil {120 if err := a.SendAsync(ports.Frame{Type: ports.MsgOutput, Payload: large}); err != nil {
120 t.Fatal(err)121 t.Fatal(err)
@@ -222,7 +223,7 @@ func TestOwnedSynchronousMaxSideEffectCompletesThroughConcurrentPacedWork(t *tes
222 }223 }
223 a.mu.Unlock()224 a.mu.Unlock()
224 go a.resendPending()225 go a.resendPending()
225- sideEffect := ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 100*1024)})}226+ sideEffect := ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: make([]byte, 100*1024)})}
226 done := make(chan error, 1)227 done := make(chan error, 1)
227 go func() { done <- a.SendSynchronous(sideEffect) }()228 go func() { done <- a.SendSynchronous(sideEffect) }()
228 229
@@ -330,7 +331,7 @@ func TestCloseUnblocksQueuedSynchronousFrames(t *testing.T) {
330 frame ports.Frame331 frame ports.Frame
331 }{332 }{
332 {name: "control", frame: ports.Frame{Type: ports.MsgPing}},333 {name: "control", frame: ports.Frame{Type: ports.MsgPing}},
333- {name: "terminal side effect", frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("osc")})}},334+ {name: "terminal side effect", frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("osc")})}},
334 }335 }
335 for _, tt := range frames {336 for _, tt := range frames {
336 t.Run(tt.name, func(t *testing.T) {337 t.Run(tt.name, func(t *testing.T) {
M internal/adapters/dgram/wire_test_helpers_test.go +6 -3
@@ -1,8 +1,11 @@
1 package dgram1 package dgram
2 2
3-import "github.com/bnema/vev/internal/ports"3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
4 7
5-func mustMarshalOutput(m ports.Output) []byte {8+func mustMarshalOutput(m protocol.Output) []byte {
6 payload, err := ports.MarshalOutput(m)9 payload, err := ports.MarshalOutput(m)
7 if err != nil {10 if err != nil {
8 panic(err)11 panic(err)
@@ -10,7 +13,7 @@ func mustMarshalOutput(m ports.Output) []byte {
10 return payload13 return payload
11 }14 }
12 15
13-func mustMarshalAck(m ports.Ack) []byte {16+func mustMarshalAck(m protocol.Ack) []byte {
14 payload, err := ports.MarshalAck(m)17 payload, err := ports.MarshalAck(m)
15 if err != nil {18 if err != nil {
16 panic(err)19 panic(err)
M internal/adapters/ipc/transport_test.go +2 -1
@@ -13,6 +13,7 @@ import (
13 13
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
16 )17 )
17 18
18 func TestTransportSendRecvBothDirections(t *testing.T) {19 func TestTransportSendRecvBothDirections(t *testing.T) {
@@ -140,7 +141,7 @@ func TestTransportManySmallFramesBackToBack(t *testing.T) {
140 }141 }
141 // Vary a couple to prove ordering and content are both preserved.142 // Vary a couple to prove ordering and content are both preserved.
142 frames[7] = ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("hop")})}143 frames[7] = ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("hop")})}
143- frames[150] = ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(ports.Resize{Size: domain.Size{Cols: 5, Rows: 6}})}144+ frames[150] = ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(protocol.Resize{Size: domain.Size{Cols: 5, Rows: 6}})}
144 145
145 var wg sync.WaitGroup146 var wg sync.WaitGroup
146 wg.Go(func() {147 wg.Go(func() {
M internal/adapters/ipc/wire_test_helpers_test.go +5 -2
@@ -1,8 +1,11 @@
1 package ipc1 package ipc
2 2
3-import "github.com/bnema/vev/internal/ports"3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
4 7
5-func mustMarshalResize(m ports.Resize) []byte {8+func mustMarshalResize(m protocol.Resize) []byte {
6 payload, err := ports.MarshalResize(m)9 payload, err := ports.MarshalResize(m)
7 if err != nil {10 if err != nil {
8 panic(err)11 panic(err)
M internal/adapters/remote/preview.go +15 -14
@@ -11,6 +11,7 @@ import (
11 "github.com/bnema/vev/internal/adapters/sshstdio"11 "github.com/bnema/vev/internal/adapters/sshstdio"
12 "github.com/bnema/vev/internal/domain"12 "github.com/bnema/vev/internal/domain"
13 "github.com/bnema/vev/internal/ports"13 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
14 )15 )
15 16
16 const (17 const (
@@ -39,11 +40,11 @@ func NewPreviewClient() ports.RemotePreviewClient {
39 return &PreviewClient{command: exec.CommandContext}40 return &PreviewClient{command: exec.CommandContext}
40 }41 }
41 42
42-func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {43+func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
43- request := ports.RemotePreviewRequest{Version: ports.RemotePreviewSchemaVersion, Target: target, Width: width, Height: height}44+ request := protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: width, Height: height}
44 payload := ports.MarshalRemotePreviewRequest(request)45 payload := ports.MarshalRemotePreviewRequest(request)
45 if payload == nil {46 if payload == nil {
46- return ports.RemotePreview{}, ports.ErrInvalidRemotePreviewRequest47+ return protocol.RemotePreview{}, protocol.ErrInvalidRemotePreviewRequest
47 }48 }
48 runCtx, cancel := context.WithTimeout(ctx, c.previewTimeout())49 runCtx, cancel := context.WithTimeout(ctx, c.previewTimeout())
49 defer cancel()50 defer cancel()
@@ -54,7 +55,7 @@ func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSession
54 encoded := base64.RawURLEncoding.EncodeToString(payload)55 encoded := base64.RawURLEncoding.EncodeToString(payload)
55 spec := sshstdio.BuildCommandForRemoteCommand(target.Endpoint, "vev", "_remote-preview", encoded)56 spec := sshstdio.BuildCommandForRemoteCommand(target.Endpoint, "vev", "_remote-preview", encoded)
56 cmd := command(runCtx, spec.Path, spec.Args...)57 cmd := command(runCtx, spec.Path, spec.Args...)
57- stdout := boundedBuffer{limit: ports.RemotePreviewMaxBytes}58+ stdout := boundedBuffer{limit: protocol.RemotePreviewMaxBytes}
58 stderr := boundedBuffer{limit: remotePreviewMaxDiagnostic}59 stderr := boundedBuffer{limit: remotePreviewMaxDiagnostic}
59 cmd.Stdout = &stdout60 cmd.Stdout = &stdout
60 cmd.Stderr = &stderr61 cmd.Stderr = &stderr
@@ -62,30 +63,30 @@ func (c *PreviewClient) Preview(ctx context.Context, target domain.RemoteSession
62 if err := cmd.Run(); err != nil {63 if err := cmd.Run(); err != nil {
63 if ctxErr := runCtx.Err(); ctxErr != nil {64 if ctxErr := runCtx.Err(); ctxErr != nil {
64 if parentErr := ctx.Err(); parentErr != nil {65 if parentErr := ctx.Err(); parentErr != nil {
65- return ports.RemotePreview{}, parentErr66+ return protocol.RemotePreview{}, parentErr
66 }67 }
67- return ports.RemotePreview{}, ports.ErrRemotePreviewTimeout68+ return protocol.RemotePreview{}, protocol.ErrRemotePreviewTimeout
68 }69 }
69 if stdout.overflow || stderr.overflow {70 if stdout.overflow || stderr.overflow {
70- return ports.RemotePreview{}, errRemotePreviewTooLarge71+ return protocol.RemotePreview{}, errRemotePreviewTooLarge
71 }72 }
72 if diagnostic := sanitizeCatalogDiagnostic(string(stderr.Bytes())); diagnostic != "" {73 if diagnostic := sanitizeCatalogDiagnostic(string(stderr.Bytes())); diagnostic != "" {
73- return ports.RemotePreview{}, fmt.Errorf("%w: %v: %s", errRemotePreviewSSH, err, diagnostic)74+ return protocol.RemotePreview{}, fmt.Errorf("%w: %v: %s", errRemotePreviewSSH, err, diagnostic)
74 }75 }
75- return ports.RemotePreview{}, fmt.Errorf("%w: %v", errRemotePreviewSSH, err)76+ return protocol.RemotePreview{}, fmt.Errorf("%w: %v", errRemotePreviewSSH, err)
76 }77 }
77 if stdout.overflow || stderr.overflow {78 if stdout.overflow || stderr.overflow {
78- return ports.RemotePreview{}, errRemotePreviewTooLarge79+ return protocol.RemotePreview{}, errRemotePreviewTooLarge
79 }80 }
80 preview, err := ports.UnmarshalRemotePreview(stdout.Bytes())81 preview, err := ports.UnmarshalRemotePreview(stdout.Bytes())
81 if err != nil {82 if err != nil {
82- return ports.RemotePreview{}, err83+ return protocol.RemotePreview{}, err
83 }84 }
84- if preview.Status != ports.RemotePreviewOK {85+ if preview.Status != protocol.RemotePreviewOK {
85- return ports.RemotePreview{}, fmt.Errorf("remote preview unavailable: status %d", preview.Status)86+ return protocol.RemotePreview{}, fmt.Errorf("remote preview unavailable: status %d", preview.Status)
86 }87 }
87 if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {88 if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {
88- return ports.RemotePreview{}, errRemotePreviewIdentity89+ return protocol.RemotePreview{}, errRemotePreviewIdentity
89 }90 }
90 return preview, nil91 return preview, nil
91 }92 }
M internal/adapters/remote/preview_test.go +10 -9
@@ -16,6 +16,7 @@ import (
16 "github.com/bnema/vev/internal/adapters/sshstdio"16 "github.com/bnema/vev/internal/adapters/sshstdio"
17 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
18 "github.com/bnema/vev/internal/ports"18 "github.com/bnema/vev/internal/ports"
19+ "github.com/bnema/vev/internal/protocol"
19 )20 )
20 21
21 func previewClientTargetForTest() domain.RemoteSessionTarget {22 func previewClientTargetForTest() domain.RemoteSessionTarget {
@@ -31,8 +32,8 @@ func TestRemotePreviewClientBuildsExactSSHCommandAndDecodesResponse(t *testing.T
31 Endpoint: "build@mule", DisplayOrigin: "mule", LifecycleID: lifecycle,32 Endpoint: "build@mule", DisplayOrigin: "mule", LifecycleID: lifecycle,
32 SessionName: "work", LiveTabID: "tab-1",33 SessionName: "work", LiveTabID: "tab-1",
33 }34 }
34- want := ports.RemotePreview{35+ want := protocol.RemotePreview{
35- Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,36+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
36 LifecycleID: lifecycle, TabID: target.LiveTabID, Revision: 7, Width: 1, Height: 1,37 LifecycleID: lifecycle, TabID: target.LiveTabID, Revision: 7, Width: 1, Height: 1,
37 Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},38 Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},
38 }39 }
@@ -79,17 +80,17 @@ func TestRemotePreviewClientRejectsMismatchedResponseIdentity(t *testing.T) {
79 target := previewClientTargetForTest()80 target := previewClientTargetForTest()
80 var otherLifecycle domain.SessionLifecycleID81 var otherLifecycle domain.SessionLifecycleID
81 otherLifecycle[0] = 282 otherLifecycle[0] = 2
82- valid := ports.RemotePreview{83+ valid := protocol.RemotePreview{
83- Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,84+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
84 LifecycleID: target.LifecycleID, TabID: target.LiveTabID, Revision: 1, Width: 1, Height: 1,85 LifecycleID: target.LifecycleID, TabID: target.LiveTabID, Revision: 1, Width: 1, Height: 1,
85 Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},86 Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},
86 }87 }
87 tests := []struct {88 tests := []struct {
88 name string89 name string
89- mutate func(*ports.RemotePreview)90+ mutate func(*protocol.RemotePreview)
90 }{91 }{
91- {name: "lifecycle ID", mutate: func(preview *ports.RemotePreview) { preview.LifecycleID = otherLifecycle }},92+ {name: "lifecycle ID", mutate: func(preview *protocol.RemotePreview) { preview.LifecycleID = otherLifecycle }},
92- {name: "tab ID", mutate: func(preview *ports.RemotePreview) { preview.TabID = "tab-2" }},93+ {name: "tab ID", mutate: func(preview *protocol.RemotePreview) { preview.TabID = "tab-2" }},
93 }94 }
94 for _, tt := range tests {95 for _, tt := range tests {
95 t.Run(tt.name, func(t *testing.T) {96 t.Run(tt.name, func(t *testing.T) {
@@ -120,7 +121,7 @@ func TestRemotePreviewClientRejectsMalformedResponse(t *testing.T) {
120 121
121 func TestRemotePreviewClientRejectsOversizedOutput(t *testing.T) {122 func TestRemotePreviewClientRejectsOversizedOutput(t *testing.T) {
122 client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {123 client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
123- return exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("head -c %d /dev/zero", ports.RemotePreviewMaxBytes+1))124+ return exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("head -c %d /dev/zero", protocol.RemotePreviewMaxBytes+1))
124 }}125 }}
125 target := previewClientTargetForTest()126 target := previewClientTargetForTest()
126 _, err := client.Preview(context.Background(), target, 1, 1)127 _, err := client.Preview(context.Background(), target, 1, 1)
@@ -145,7 +146,7 @@ func TestRemotePreviewClientReturnsInternalTimeoutSentinel(t *testing.T) {
145 },146 },
146 }147 }
147 _, err := client.Preview(context.Background(), previewClientTargetForTest(), 1, 1)148 _, err := client.Preview(context.Background(), previewClientTargetForTest(), 1, 1)
148- require.ErrorIs(t, err, ports.ErrRemotePreviewTimeout)149+ require.ErrorIs(t, err, protocol.ErrRemotePreviewTimeout)
149 }150 }
150 151
151 func TestRemotePreviewClientReturnsCallerCancellation(t *testing.T) {152 func TestRemotePreviewClientReturnsCallerCancellation(t *testing.T) {
M internal/app/final_acceptance_test.go +6 -5
@@ -8,6 +8,7 @@ import (
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 "github.com/bnema/vev/internal/usecase/daemon"12 "github.com/bnema/vev/internal/usecase/daemon"
12 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
13 )14 )
@@ -46,7 +47,7 @@ func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.Comma
46 return result47 return result
47 }48 }
48 49
49-func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(ports.Output) bool) ports.Output {50+func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(protocol.Output) bool) protocol.Output {
50 t.Helper()51 t.Helper()
51 frame := awaitAcceptanceFrame(t, p, ports.MsgOutput, func(frame ports.Frame) bool {52 frame := awaitAcceptanceFrame(t, p, ports.MsgOutput, func(frame ports.Frame) bool {
52 output, err := ports.UnmarshalOutput(frame.Payload)53 output, err := ports.UnmarshalOutput(frame.Payload)
@@ -113,15 +114,15 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {
113 // The resize keeps its attachment-local output stream while recalculating114 // The resize keeps its attachment-local output stream while recalculating
114 // shared PTY/content geometry from the latest valid attachment claim; the115 // shared PTY/content geometry from the latest valid attachment claim; the
115 // peer stream remains independent.116 // peer stream remains independent.
116- resizePayload, err := ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 100, Rows: 30}})117+ resizePayload, err := ports.MarshalResize(protocol.Resize{Size: domain.Size{Cols: 100, Rows: 30}})
117 require.NoError(t, err)118 require.NoError(t, err)
118 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: resizePayload}))119 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgResize, Payload: resizePayload}))
119- resized := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) })120+ resized := awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Size == (domain.Size{Cols: 100, Rows: 30}) })
120 require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size)121 require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size)
121 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")122 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
122 123
123 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))124 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
124- reset := awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Base == 0 && output.Full })125+ reset := awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Base == 0 && output.Full })
125 require.True(t, reset.Full)126 require.True(t, reset.Full)
126 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")127 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
127 128
@@ -173,6 +174,6 @@ func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {
173 174
174 drained:175 drained:
175 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))176 require.NoError(t, first.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}))
176- awaitAcceptanceOutput(t, firstPump, func(output ports.Output) bool { return output.Full && output.Base == 0 })177+ awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Full && output.Base == 0 })
177 assertNoTextAfterInput(t, secondPump, size, "Commands")178 assertNoTextAfterInput(t, secondPump, size, "Commands")
178 }179 }
M internal/app/remote_final_acceptance_test.go +3 -2
@@ -15,6 +15,7 @@ import (
15 "github.com/bnema/vev/internal/domain"15 "github.com/bnema/vev/internal/domain"
16 "github.com/bnema/vev/internal/ports"16 "github.com/bnema/vev/internal/ports"
17 portsmocks "github.com/bnema/vev/internal/ports/mocks"17 portsmocks "github.com/bnema/vev/internal/ports/mocks"
18+ "github.com/bnema/vev/internal/protocol"
18 "github.com/bnema/vev/internal/usecase/client"19 "github.com/bnema/vev/internal/usecase/client"
19 "github.com/stretchr/testify/require"20 "github.com/stretchr/testify/require"
20 )21 )
@@ -78,7 +79,7 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta
78 if outputs != nil {79 if outputs != nil {
79 outputs <- string(data)80 outputs <- string(data)
80 }81 }
81- payload, err := ports.MarshalOutput(ports.Output{82+ payload, err := ports.MarshalOutput(protocol.Output{
82 Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}, Data: data,83 Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}, Data: data,
83 })84 })
84 if err != nil {85 if err != nil {
@@ -155,7 +156,7 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
155 welcome, err := wireTransport.Recv()156 welcome, err := wireTransport.Recv()
156 require.NoError(t, err)157 require.NoError(t, err)
157 require.Equal(t, ports.MsgWelcome, welcome.Type)158 require.Equal(t, ports.MsgWelcome, welcome.Type)
158- require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(ports.Theme{})}))159+ require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(protocol.Theme{})}))
159 attachTarget, err := wireTransport.Recv()160 attachTarget, err := wireTransport.Recv()
160 require.NoError(t, err)161 require.NoError(t, err)
161 target, err := ports.UnmarshalAttachTarget(attachTarget.Payload)162 target, err := ports.UnmarshalAttachTarget(attachTarget.Payload)
M internal/app/remote_preview.go +4 -3
@@ -9,6 +9,7 @@ import (
9 9
10 "github.com/bnema/vev/internal/adapters/ipc"10 "github.com/bnema/vev/internal/adapters/ipc"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 )13 )
13 14
14 // runRemotePreview is the hidden SSH-side carriage. The request is encoded in15 // runRemotePreview is the hidden SSH-side carriage. The request is encoded in
@@ -27,7 +28,7 @@ func runRemotePreview(ctx context.Context, encoded string) error {
27 if ctxErr := ctx.Err(); ctxErr != nil {28 if ctxErr := ctx.Err(); ctxErr != nil {
28 return ctxErr29 return ctxErr
29 }30 }
30- return writeRemotePreviewStatus(ports.RemotePreviewUnavailable)31+ return writeRemotePreviewStatus(protocol.RemotePreviewUnavailable)
31 }32 }
32 defer func() { _ = transport.Close() }()33 defer func() { _ = transport.Close() }()
33 if err := transport.Send(ports.Frame{Type: ports.MsgRemotePreviewRequest, Payload: request}); err != nil {34 if err := transport.Send(ports.Frame{Type: ports.MsgRemotePreviewRequest, Payload: request}); err != nil {
@@ -63,8 +64,8 @@ func runRemotePreview(ctx context.Context, encoded string) error {
63 }64 }
64 }65 }
65 66
66-func writeRemotePreviewStatus(status ports.RemotePreviewStatus) error {67+func writeRemotePreviewStatus(status protocol.RemotePreviewStatus) error {
67- payload := ports.MarshalRemotePreview(ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: status})68+ payload := ports.MarshalRemotePreview(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: status})
68 if payload == nil {69 if payload == nil {
69 return errors.New("vev: failed to encode remote preview status")70 return errors.New("vev: failed to encode remote preview status")
70 }71 }
M internal/app/run.go +8 -7
@@ -46,6 +46,7 @@ import (
46 "github.com/bnema/vev/internal/persist"46 "github.com/bnema/vev/internal/persist"
47 "github.com/bnema/vev/internal/platform"47 "github.com/bnema/vev/internal/platform"
48 "github.com/bnema/vev/internal/ports"48 "github.com/bnema/vev/internal/ports"
49+ "github.com/bnema/vev/internal/protocol"
49 "github.com/bnema/vev/internal/usecase/client"50 "github.com/bnema/vev/internal/usecase/client"
50 "github.com/bnema/vev/internal/usecase/daemon"51 "github.com/bnema/vev/internal/usecase/daemon"
51 "github.com/bnema/vev/internal/usecase/recovery"52 "github.com/bnema/vev/internal/usecase/recovery"
@@ -892,10 +893,10 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
892 var remoteSelection *domain.RemoteSessionTarget893 var remoteSelection *domain.RemoteSessionTarget
893 var remoteEnvironmentPolicy ports.EnvironmentPolicy894 var remoteEnvironmentPolicy ports.EnvironmentPolicy
894 remoteDisplayOrigin := domain.RemoteDisplayOrigin(remoteTarget)895 remoteDisplayOrigin := domain.RemoteDisplayOrigin(remoteTarget)
895- routeOrigin := ports.RouteOriginLocal896+ routeOrigin := protocol.RouteOriginLocal
896 routeOriginKey := "local"897 routeOriginKey := "local"
897 if remoteTarget != "" {898 if remoteTarget != "" {
898- routeOrigin = ports.RouteOriginRemote899+ routeOrigin = protocol.RouteOriginRemote
899 routeOriginKey = remoteTarget900 routeOriginKey = remoteTarget
900 if modeErr != nil {901 if modeErr != nil {
901 return modeErr902 return modeErr
@@ -939,7 +940,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
939 Intent: target.Intent,940 Intent: target.Intent,
940 SessionName: target.Session,941 SessionName: target.Session,
941 Remote: true,942 Remote: true,
942- Origin: ports.RouteOriginDiscovery,943+ Origin: protocol.RouteOriginDiscovery,
943 OriginKey: target.Endpoint,944 OriginKey: target.Endpoint,
944 RemoteTarget: selection,945 RemoteTarget: selection,
945 HostLabel: displayOrigin,946 HostLabel: displayOrigin,
@@ -968,7 +969,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
968 RemoteHostLearner: attachRememberLearner(deps, remoteTarget, log),969 RemoteHostLearner: attachRememberLearner(deps, remoteTarget, log),
969 AttachHandoff: handoff,970 AttachHandoff: handoff,
970 Remote: true,971 Remote: true,
971- Origin: ports.RouteOriginRemote,972+ Origin: protocol.RouteOriginRemote,
972 OriginKey: remoteTarget,973 OriginKey: remoteTarget,
973 }, client.AttachRequest{974 }, client.AttachRequest{
974 Intent: intent,975 Intent: intent,
@@ -992,9 +993,9 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
992 RuntimeObserver: deps.runtimeObserver,993 RuntimeObserver: deps.runtimeObserver,
993 AttachHandoff: handoff,994 AttachHandoff: handoff,
994 Remote: false,995 Remote: false,
995- Origin: ports.RouteOriginLocal,996+ Origin: protocol.RouteOriginLocal,
996 OriginKey: "local",997 OriginKey: "local",
997- }, client.AttachRequest{Intent: intent, SessionName: name, Origin: ports.RouteOriginLocal, OriginKey: "local"})998+ }, client.AttachRequest{Intent: intent, SessionName: name, Origin: protocol.RouteOriginLocal, OriginKey: "local"})
998 }999 }
999 1000
1000 var handoffErr *client.AttachTargetError1001 var handoffErr *client.AttachTargetError
@@ -1013,7 +1014,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
1013 handoffAttempts++1014 handoffAttempts++
1014 remoteTarget = handoffErr.Target.Endpoint1015 remoteTarget = handoffErr.Target.Endpoint
1015 remoteDisplayOrigin = domain.RemoteDisplayOrigin(remoteTarget)1016 remoteDisplayOrigin = domain.RemoteDisplayOrigin(remoteTarget)
1016- routeOrigin = ports.RouteOriginDiscovery1017+ routeOrigin = protocol.RouteOriginDiscovery
1017 routeOriginKey = remoteTarget1018 routeOriginKey = remoteTarget
1018 name = handoffErr.Target.Session1019 name = handoffErr.Target.Session
1019 intent = handoffErr.Target.Intent1020 intent = handoffErr.Target.Intent
M internal/app/run_test.go +3 -2
@@ -22,6 +22,7 @@ import (
22 "github.com/bnema/vev/internal/persist"22 "github.com/bnema/vev/internal/persist"
23 "github.com/bnema/vev/internal/ports"23 "github.com/bnema/vev/internal/ports"
24 portsmocks "github.com/bnema/vev/internal/ports/mocks"24 portsmocks "github.com/bnema/vev/internal/ports/mocks"
25+ "github.com/bnema/vev/internal/protocol"
25 "github.com/bnema/vev/internal/usecase/client"26 "github.com/bnema/vev/internal/usecase/client"
26 "github.com/bnema/vev/internal/usecase/daemon"27 "github.com/bnema/vev/internal/usecase/daemon"
27 "github.com/bnema/vev/pkg/kv"28 "github.com/bnema/vev/pkg/kv"
@@ -962,11 +963,11 @@ func TestRunAttachWithDepsLocalPickerHandoffAttachesSelectedRemote(t *testing.T)
962 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {963 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
963 if deps.Remote {964 if deps.Remote {
964 remoteCalls++965 remoteCalls++
965- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "picked", Remote: true, Origin: ports.RouteOriginDiscovery, OriginKey: "selected.example", HostLabel: "selected.example", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}, request)966+ require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "picked", Remote: true, Origin: protocol.RouteOriginDiscovery, OriginKey: "selected.example", HostLabel: "selected.example", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}, request)
966 return nil967 return nil
967 }968 }
968 localCalls++969 localCalls++
969- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Origin: ports.RouteOriginLocal, OriginKey: "local"}, request)970+ require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local"}, request)
970 return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach}}971 return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach}}
971 },972 },
972 })973 })
M internal/ports/frame.go +11 -147
@@ -1,27 +1,9 @@
1 package ports1 package ports
2 2
3-import (
4- "time"
5-
6- renderer "github.com/bnema/vev-vt"
7- "github.com/bnema/vev/internal/domain"
8-)
9-
10-// HandshakeTimeout bounds every transport handshake from connect through the
11-// first committed publication.
12-const HandshakeTimeout = 15 * time.Second
13-
14-// ProtocolVersion is the current vev IPC wire protocol version.
15-const ProtocolVersion uint16 = 37
16-
17 // MaxFrameLen is the largest permitted frame length, including the type byte3 // MaxFrameLen is the largest permitted frame length, including the type byte
18 // and excluding the four-byte length prefix.4 // and excluding the four-byte length prefix.
19 const MaxFrameLen = 16 << 205 const MaxFrameLen = 16 << 20
20 6
21-// MaxOutputDataLen is the largest decoded terminal byte stream that fits in
22-// one Output frame after its type byte and wire metadata.
23-const MaxOutputDataLen = MaxFrameLen - 1 - outputPayloadOverhead
24-
25 // MsgType identifies the kind of payload carried by a Frame.7 // MsgType identifies the kind of payload carried by a Frame.
26 type MsgType uint88 type MsgType uint8
27 9
@@ -48,18 +30,16 @@ const (
48 MsgSamePeerSwitchRequest MsgType = 3330 MsgSamePeerSwitchRequest MsgType = 33
49 MsgParkedRouteRequest MsgType = 3531 MsgParkedRouteRequest MsgType = 35
50 32
51- MsgWelcome MsgType = 1633+ MsgWelcome MsgType = 16
52- MsgError MsgType = 1734+ MsgError MsgType = 17
53- MsgOutput MsgType = 1835+ MsgOutput MsgType = 18
54- MsgDetached MsgType = 1936+ MsgDetached MsgType = 19
55- MsgPong MsgType = 2037+ MsgPong MsgType = 20
56- MsgSessions MsgType = 2138+ MsgSessions MsgType = 21
57- MsgCommandResult MsgType = 2239+ MsgCommandResult MsgType = 22
58- MsgNavigationAction MsgType = 2340+ MsgNavigationAction MsgType = 23
59- MsgAttachTarget MsgType = 2541+ MsgAttachTarget MsgType = 25
60- MsgRemotePreviewResponse MsgType = 2642+ MsgRemotePreviewResponse MsgType = 26
61- // Route navigation metadata/control frames occupy the post-26 server range;
62- // 14 and 24 remain reserved for compatibility with older peers.
63 MsgCommittedRouteIdentity MsgType = 2743 MsgCommittedRouteIdentity MsgType = 27
64 MsgRecentRouteSnapshot MsgType = 2844 MsgRecentRouteSnapshot MsgType = 28
65 MsgNavigateRecentRoute MsgType = 2945 MsgNavigateRecentRoute MsgType = 29
@@ -70,124 +50,8 @@ const (
70 )50 )
71 51
72 // Frame is the unit of exchange over a Transport: a typed, length-delimited52 // Frame is the unit of exchange over a Transport: a typed, length-delimited
73-// message. It lives in ports (rather than an ipc adapter) so usecases and53+// message.
74-// adapters can share it without either depending on a concrete transport
75-// implementation.
76 type Frame struct {54 type Frame struct {
77 Type MsgType55 Type MsgType
78 Payload []byte56 Payload []byte
79 }57 }
80-
81-// NavigationCapabilities advertises the bounded client routes available to a
82-// directly attached daemon.
83-type NavigationCapabilities uint8
84-
85-const (
86- NavigationCapabilityHomePicker NavigationCapabilities = 1 << iota
87- NavigationCapabilityBack
88-)
89-
90-// StartupOverlay selects the one startup overlay opened by a navigation route.
91-type StartupOverlay uint8
92-
93-const (
94- StartupOverlayNone StartupOverlay = iota
95- StartupOverlaySessionPicker
96-)
97-
98-// NavigationAction is a server request for one bounded client route transition.
99-type NavigationAction uint8
100-
101-const (
102- NavigationOpenHomePicker NavigationAction = 1
103- NavigationBack NavigationAction = 2
104-)
105-
106-// ParkedRouteLeaseID is an opaque capability minted by the attached daemon for
107-// one home-picker excursion. It is bound server-side to the exact connection
108-// generation and transport that received the navigation directive.
109-type ParkedRouteLeaseID [16]byte
110-
111-func (id ParkedRouteLeaseID) IsZero() bool { return id == ParkedRouteLeaseID{} }
112-
113-// NavigationDirective requests one client-owned route transition. Opening the
114-// home picker always carries a fresh lease; Back is sent by the transient home
115-// route and therefore carries no lease.
116-type NavigationDirective struct {
117- Action NavigationAction
118- LeaseID ParkedRouteLeaseID
119-}
120-
121-// ParkedRouteAction advances one retained route through its one-shot lease.
122-type ParkedRouteAction uint8
123-
124-const (
125- ParkedRoutePrepare ParkedRouteAction = iota + 1
126- ParkedRouteResume
127- ParkedRouteSwitch
128-)
129-
130-// ParkedRouteRequest prepares, resumes, or switches the retained authenticated
131-// route. Target is present only for Switch and retains exact lifecycle/tab
132-// authority from the home picker's remote catalogue row.
133-type ParkedRouteRequest struct {
134- RequestID uint64
135- LeaseID ParkedRouteLeaseID
136- Action ParkedRouteAction
137- Target *domain.RemoteSessionTarget
138-}
139-
140-// ParkedRouteStatus is the explicit outcome for a retained-route request.
141-type ParkedRouteStatus uint8
142-
143-const (
144- ParkedRouteReady ParkedRouteStatus = iota + 1
145- ParkedRouteResumed
146- ParkedRouteSwitched
147- ParkedRouteRejected
148- ParkedRouteExpired
149- ParkedRouteStaleTarget
150-)
151-
152-// ParkedRouteResponse correlates one retained-route request with its outcome.
153-type ParkedRouteResponse struct {
154- RequestID uint64
155- Status ParkedRouteStatus
156-}
157-
158-// RemotePreviewSchemaVersion is independent from the attachment IPC version.
159-const RemotePreviewSchemaVersion uint16 = 1
160-
161-// RemotePreviewStatus is a closed response taxonomy. Terminal contents are
162-// never carried in an error response.
163-type RemotePreviewStatus uint8
164-
165-const (
166- RemotePreviewOK RemotePreviewStatus = iota
167- RemotePreviewUnavailable
168- RemotePreviewNoSuchTarget
169- RemotePreviewStale
170- RemotePreviewMalformed
171- RemotePreviewTooLarge
172-)
173-
174-// RemotePreviewRequest asks the owning daemon for an in-memory live viewport.
175-type RemotePreviewRequest struct {
176- Version uint16
177- Target domain.RemoteSessionTarget
178- Width uint16
179- Height uint16
180-}
181-
182-// RemotePreview is a bounded row-major styled-cell viewport. It is a
183-// process-memory DTO and is never persisted, logged, or traced.
184-type RemotePreview struct {
185- Version uint16
186- Status RemotePreviewStatus
187- LifecycleID domain.SessionLifecycleID
188- TabID domain.TabStableID
189- Revision uint64
190- Width uint16
191- Height uint16
192- Cells []renderer.Cell
193-}
M internal/ports/frame_test.go +3 -1
@@ -1,6 +1,8 @@
1 package ports1 package ports
2 2
3-import "testing"3+import (
4+ "testing"
5+)
4 6
5 func TestProtocolVersion(t *testing.T) {7 func TestProtocolVersion(t *testing.T) {
6 if ProtocolVersion != 37 {8 if ProtocolVersion != 37 {
M internal/ports/mocks/remote_mock.go +8 -7
@@ -10,6 +10,7 @@ import (
10 10
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12 "github.com/bnema/vev/internal/ports"12 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
13 mock "github.com/stretchr/testify/mock"14 mock "github.com/stretchr/testify/mock"
14 )15 )
15 16
@@ -666,22 +667,22 @@ func (_m *MockRemotePreviewClient) EXPECT() *MockRemotePreviewClient_Expecter {
666 }667 }
667 668
668 // Preview provides a mock function for the type MockRemotePreviewClient669 // Preview provides a mock function for the type MockRemotePreviewClient
669-func (_mock *MockRemotePreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (ports.RemotePreview, error) {670+func (_mock *MockRemotePreviewClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (protocol.RemotePreview, error) {
670 ret := _mock.Called(ctx, target, width, height)671 ret := _mock.Called(ctx, target, width, height)
671 672
672 if len(ret) == 0 {673 if len(ret) == 0 {
673 panic("no return value specified for Preview")674 panic("no return value specified for Preview")
674 }675 }
675 676
676- var r0 ports.RemotePreview677+ var r0 protocol.RemotePreview
677 var r1 error678 var r1 error
678- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) (ports.RemotePreview, error)); ok {679+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) (protocol.RemotePreview, error)); ok {
679 return returnFunc(ctx, target, width, height)680 return returnFunc(ctx, target, width, height)
680 }681 }
681- if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) ports.RemotePreview); ok {682+ if returnFunc, ok := ret.Get(0).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) protocol.RemotePreview); ok {
682 r0 = returnFunc(ctx, target, width, height)683 r0 = returnFunc(ctx, target, width, height)
683 } else {684 } else {
684- r0 = ret.Get(0).(ports.RemotePreview)685+ r0 = ret.Get(0).(protocol.RemotePreview)
685 }686 }
686 if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) error); ok {687 if returnFunc, ok := ret.Get(1).(func(context.Context, domain.RemoteSessionTarget, uint16, uint16) error); ok {
687 r1 = returnFunc(ctx, target, width, height)688 r1 = returnFunc(ctx, target, width, height)
@@ -733,12 +734,12 @@ func (_c *MockRemotePreviewClient_Preview_Call) Run(run func(ctx context.Context
733 return _c734 return _c
734 }735 }
735 736
736-func (_c *MockRemotePreviewClient_Preview_Call) Return(remotePreview ports.RemotePreview, err error) *MockRemotePreviewClient_Preview_Call {737+func (_c *MockRemotePreviewClient_Preview_Call) Return(remotePreview protocol.RemotePreview, err error) *MockRemotePreviewClient_Preview_Call {
737 _c.Call.Return(remotePreview, err)738 _c.Call.Return(remotePreview, err)
738 return _c739 return _c
739 }740 }
740 741
741-func (_c *MockRemotePreviewClient_Preview_Call) RunAndReturn(run func(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (ports.RemotePreview, error)) *MockRemotePreviewClient_Preview_Call {742+func (_c *MockRemotePreviewClient_Preview_Call) RunAndReturn(run func(ctx context.Context, target domain.RemoteSessionTarget, width uint16, height uint16) (protocol.RemotePreview, error)) *MockRemotePreviewClient_Preview_Call {
742 _c.Call.Return(run)743 _c.Call.Return(run)
743 return _c744 return _c
744 }745 }
M internal/ports/navigation_test.go +45 -44
@@ -5,22 +5,23 @@ import (
5 "testing"5 "testing"
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/protocol"
8 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
9 )10 )
10 11
11 func TestNavigationDirectiveWireTable(t *testing.T) {12 func TestNavigationDirectiveWireTable(t *testing.T) {
12- lease := ParkedRouteLeaseID{1, 2, 3}13+ lease := protocol.ParkedRouteLeaseID{1, 2, 3}
13 tests := []struct {14 tests := []struct {
14 name string15 name string
15- directive NavigationDirective16+ directive protocol.NavigationDirective
16 valid bool17 valid bool
17 }{18 }{
18- {name: "open home", directive: NavigationDirective{Action: NavigationOpenHomePicker, LeaseID: lease}, valid: true},19+ {name: "open home", directive: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}, valid: true},
19- {name: "back", directive: NavigationDirective{Action: NavigationBack}, valid: true},20+ {name: "back", directive: protocol.NavigationDirective{Action: protocol.NavigationBack}, valid: true},
20- {name: "open home without lease", directive: NavigationDirective{Action: NavigationOpenHomePicker}},21+ {name: "open home without lease", directive: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker}},
21- {name: "back with lease", directive: NavigationDirective{Action: NavigationBack, LeaseID: lease}},22+ {name: "back with lease", directive: protocol.NavigationDirective{Action: protocol.NavigationBack, LeaseID: lease}},
22- {name: "zero", directive: NavigationDirective{}},23+ {name: "zero", directive: protocol.NavigationDirective{}},
23- {name: "unknown", directive: NavigationDirective{Action: 3}},24+ {name: "unknown", directive: protocol.NavigationDirective{Action: 3}},
24 }25 }
25 26
26 for _, tt := range tests {27 for _, tt := range tests {
@@ -35,25 +36,25 @@ func TestNavigationDirectiveWireTable(t *testing.T) {
35 require.Equal(t, tt.directive, decoded)36 require.Equal(t, tt.directive, decoded)
36 assertAllPrefixesFail(t, got, UnmarshalNavigationDirective)37 assertAllPrefixesFail(t, got, UnmarshalNavigationDirective)
37 _, err = UnmarshalNavigationDirective(append(append([]byte(nil), got...), 0))38 _, err = UnmarshalNavigationDirective(append(append([]byte(nil), got...), 0))
38- require.ErrorIs(t, err, ErrInvalidNavigation)39+ require.ErrorIs(t, err, protocol.ErrInvalidNavigation)
39 })40 })
40 }41 }
41 }42 }
42 43
43 func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {44 func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
44- lease := ParkedRouteLeaseID{1, 2, 3}45+ lease := protocol.ParkedRouteLeaseID{1, 2, 3}
45 target := &domain.RemoteSessionTarget{46 target := &domain.RemoteSessionTarget{
46 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{4},47 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{4},
47 SessionName: "work", LiveTabID: "tab-1",48 SessionName: "work", LiveTabID: "tab-1",
48 }49 }
49 requests := []struct {50 requests := []struct {
50 name string51 name string
51- value ParkedRouteRequest52+ value protocol.ParkedRouteRequest
52 golden string53 golden string
53 }{54 }{
54- {name: "prepare", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRoutePrepare}, golden: "0000000000000001" + "01020300000000000000000000000000" + "010001"},55+ {name: "prepare", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRoutePrepare}, golden: "0000000000000001" + "01020300000000000000000000000000" + "010001"},
55- {name: "resume", value: ParkedRouteRequest{RequestID: 2, LeaseID: lease, Action: ParkedRouteResume}},56+ {name: "resume", value: protocol.ParkedRouteRequest{RequestID: 2, LeaseID: lease, Action: protocol.ParkedRouteResume}},
56- {name: "switch", value: ParkedRouteRequest{RequestID: 3, LeaseID: lease, Action: ParkedRouteSwitch, Target: target}},57+ {name: "switch", value: protocol.ParkedRouteRequest{RequestID: 3, LeaseID: lease, Action: protocol.ParkedRouteSwitch, Target: target}},
57 }58 }
58 for _, tt := range requests {59 for _, tt := range requests {
59 t.Run("request "+tt.name, func(t *testing.T) {60 t.Run("request "+tt.name, func(t *testing.T) {
@@ -74,15 +75,15 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
74 75
75 responses := []struct {76 responses := []struct {
76 name string77 name string
77- value ParkedRouteResponse78+ value protocol.ParkedRouteResponse
78 golden string79 golden string
79 }{80 }{
80- {name: "ready", value: ParkedRouteResponse{RequestID: 1, Status: ParkedRouteReady}, golden: "000000000000000101"},81+ {name: "ready", value: protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}, golden: "000000000000000101"},
81- {name: "resumed", value: ParkedRouteResponse{RequestID: 2, Status: ParkedRouteResumed}},82+ {name: "resumed", value: protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed}},
82- {name: "switched", value: ParkedRouteResponse{RequestID: 3, Status: ParkedRouteSwitched}},83+ {name: "switched", value: protocol.ParkedRouteResponse{RequestID: 3, Status: protocol.ParkedRouteSwitched}},
83- {name: "rejected", value: ParkedRouteResponse{RequestID: 4, Status: ParkedRouteRejected}},84+ {name: "rejected", value: protocol.ParkedRouteResponse{RequestID: 4, Status: protocol.ParkedRouteRejected}},
84- {name: "expired", value: ParkedRouteResponse{RequestID: 5, Status: ParkedRouteExpired}},85+ {name: "expired", value: protocol.ParkedRouteResponse{RequestID: 5, Status: protocol.ParkedRouteExpired}},
85- {name: "stale target", value: ParkedRouteResponse{RequestID: 6, Status: ParkedRouteStaleTarget}},86+ {name: "stale target", value: protocol.ParkedRouteResponse{RequestID: 6, Status: protocol.ParkedRouteStaleTarget}},
86 }87 }
87 for _, tt := range responses {88 for _, tt := range responses {
88 t.Run("response "+tt.name, func(t *testing.T) {89 t.Run("response "+tt.name, func(t *testing.T) {
@@ -102,15 +103,15 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
102 103
103 invalidRequests := []struct {104 invalidRequests := []struct {
104 name string105 name string
105- value ParkedRouteRequest106+ value protocol.ParkedRouteRequest
106 }{107 }{
107- {name: "zero request ID", value: ParkedRouteRequest{LeaseID: lease, Action: ParkedRoutePrepare}},108+ {name: "zero request ID", value: protocol.ParkedRouteRequest{LeaseID: lease, Action: protocol.ParkedRoutePrepare}},
108- {name: "zero lease", value: ParkedRouteRequest{RequestID: 1, Action: ParkedRoutePrepare}},109+ {name: "zero lease", value: protocol.ParkedRouteRequest{RequestID: 1, Action: protocol.ParkedRoutePrepare}},
109- {name: "unknown action", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: 99}},110+ {name: "unknown action", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: 99}},
110- {name: "prepare target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRoutePrepare, Target: target}},111+ {name: "prepare target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRoutePrepare, Target: target}},
111- {name: "resume target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRouteResume, Target: target}},112+ {name: "resume target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRouteResume, Target: target}},
112- {name: "missing switch target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRouteSwitch}},113+ {name: "missing switch target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRouteSwitch}},
113- {name: "invalid switch target", value: ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}}},114+ {name: "invalid switch target", value: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}}},
114 }115 }
115 for _, tt := range invalidRequests {116 for _, tt := range invalidRequests {
116 t.Run("invalid request "+tt.name, func(t *testing.T) {117 t.Run("invalid request "+tt.name, func(t *testing.T) {
@@ -119,11 +120,11 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
119 }120 }
120 for _, tt := range []struct {121 for _, tt := range []struct {
121 name string122 name string
122- value ParkedRouteResponse123+ value protocol.ParkedRouteResponse
123 }{124 }{
124- {name: "zero request ID", value: ParkedRouteResponse{Status: ParkedRouteReady}},125+ {name: "zero request ID", value: protocol.ParkedRouteResponse{Status: protocol.ParkedRouteReady}},
125- {name: "zero status", value: ParkedRouteResponse{RequestID: 1}},126+ {name: "zero status", value: protocol.ParkedRouteResponse{RequestID: 1}},
126- {name: "unknown status", value: ParkedRouteResponse{RequestID: 1, Status: 99}},127+ {name: "unknown status", value: protocol.ParkedRouteResponse{RequestID: 1, Status: 99}},
127 } {128 } {
128 t.Run("invalid response "+tt.name, func(t *testing.T) {129 t.Run("invalid response "+tt.name, func(t *testing.T) {
129 require.Nil(t, MarshalParkedRouteResponse(tt.value))130 require.Nil(t, MarshalParkedRouteResponse(tt.value))
@@ -141,22 +142,22 @@ func TestHelloNavigationValidationTable(t *testing.T) {
141 name string142 name string
142 hello Hello143 hello Hello
143 valid bool144 valid bool
144- capability NavigationCapabilities145+ capability protocol.NavigationCapabilities
145- overlay StartupOverlay146+ overlay protocol.StartupOverlay
146 wantPayload string147 wantPayload string
147 }{148 }{
148 {name: "ordinary", hello: base, valid: true},149 {name: "ordinary", hello: base, valid: true},
149 {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true},150 {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true},
150- {name: "daemon-owned home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: NavigationCapabilityHomePicker},151+ {name: "daemon-owned home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
151- {name: "remote-target home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: NavigationCapabilityHomePicker},152+ {name: "remote-target home picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
152- {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityHomePicker}, valid: false},153+ {name: "client-owned home picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
153- {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},154+ {name: "daemon-owned back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
154- {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},155+ {name: "remote-target back picker rejected", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}, valid: false},
155- {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: true, capability: NavigationCapabilityBack, overlay: StartupOverlaySessionPicker, wantPayload: "002502" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "02010000"},156+ {name: "back with startup picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: true, capability: protocol.NavigationCapabilityBack, overlay: protocol.StartupOverlaySessionPicker, wantPayload: "002502" + "00000000000000000000000000000000" + "0000000000000000" + "0000" + "00500018" + "00000000" + "0000" + "0000" + "00" + "00" + "00000000" + "00" + "00" + "00" + "0000" + "02010000"},
156- {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack, StartupOverlay: StartupOverlaySessionPicker}, valid: false},157+ {name: "new rejects navigation", hello: Hello{Version: ProtocolVersion, Intent: IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
157 {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false},158 {name: "unknown capability", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, valid: false},
158- {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: NavigationCapabilityBack}, valid: false},159+ {name: "back without picker", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
159- {name: "picker without back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, StartupOverlay: StartupOverlaySessionPicker}, valid: false},160+ {name: "picker without back", hello: Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
160 }161 }
161 162
162 for _, tt := range tests {163 for _, tt := range tests {
M internal/ports/output_compression_test.go +6 -5
@@ -6,10 +6,11 @@ import (
6 "testing"6 "testing"
7 7
8 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
9 )10 )
10 11
11 func TestOutputCompression(t *testing.T) {12 func TestOutputCompression(t *testing.T) {
12- full := Output{13+ full := protocol.Output{
13 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,14 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,
14 Data: bytes.Repeat([]byte("\x1b[38;2;120;180;240mstyled viewport row\x1b[0m\r\n"), 128),15 Data: bytes.Repeat([]byte("\x1b[38;2;120;180;240mstyled viewport row\x1b[0m\r\n"), 128),
15 }16 }
@@ -20,12 +21,12 @@ func TestOutputCompression(t *testing.T) {
20 21
21 for _, tt := range []struct {22 for _, tt := range []struct {
22 name string23 name string
23- output Output24+ output protocol.Output
24 wantKind byte25 wantKind byte
25 }{26 }{
26 {name: "large full snapshot", output: full, wantKind: outputCompressionZlib},27 {name: "large full snapshot", output: full, wantKind: outputCompressionZlib},
27 {name: "incremental output", output: incremental, wantKind: outputCompressionNone},28 {name: "incremental output", output: incremental, wantKind: outputCompressionNone},
28- {name: "small full snapshot", output: Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("small")}, wantKind: outputCompressionNone},29+ {name: "small full snapshot", output: protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("small")}, wantKind: outputCompressionNone},
29 } {30 } {
30 t.Run(tt.name, func(t *testing.T) {31 t.Run(tt.name, func(t *testing.T) {
31 payload, err := MarshalOutput(tt.output)32 payload, err := MarshalOutput(tt.output)
@@ -47,7 +48,7 @@ func TestOutputCompression(t *testing.T) {
47 }48 }
48 49
49 func TestCompressedOutputRejectsMalformedPayloads(t *testing.T) {50 func TestCompressedOutputRejectsMalformedPayloads(t *testing.T) {
50- output := Output{51+ output := protocol.Output{
51 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,52 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,
52 Data: bytes.Repeat([]byte("\x1b[31mcompressed\x1b[0m\r\n"), 256),53 Data: bytes.Repeat([]byte("\x1b[31mcompressed\x1b[0m\r\n"), 256),
53 }54 }
@@ -92,7 +93,7 @@ func TestCompressedOutputRejectsMalformedPayloads(t *testing.T) {
92 }93 }
93 94
94 func BenchmarkMarshalOutput(b *testing.B) {95 func BenchmarkMarshalOutput(b *testing.B) {
95- fixtures := map[string]Output{96+ fixtures := map[string]protocol.Output{
96 "empty-120x40": {97 "empty-120x40": {
97 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,98 Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 120, Rows: 40}, Full: true,
98 Data: bytes.Repeat([]byte(" "), 120*40),99 Data: bytes.Repeat([]byte(" "), 120*40),
M internal/ports/remote.go +2 -1
@@ -12,6 +12,7 @@ import (
12 "unicode/utf8"12 "unicode/utf8"
13 13
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15+ "github.com/bnema/vev/internal/protocol"
15 )16 )
16 17
17 // RemoteTransportMode selects the transport used for remote attach after CLI parsing.18 // RemoteTransportMode selects the transport used for remote attach after CLI parsing.
@@ -57,7 +58,7 @@ type RemoteCatalogClient interface {
57 // RemotePreviewClient fetches one bounded, exact-target viewport without58 // RemotePreviewClient fetches one bounded, exact-target viewport without
58 // attaching to or mutating the remote session.59 // attaching to or mutating the remote session.
59 type RemotePreviewClient interface {60 type RemotePreviewClient interface {
60- Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (RemotePreview, error)61+ Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error)
61 }62 }
62 63
63 const (64 const (
M internal/ports/remote_preview_hardening_test.go +15 -14
@@ -8,6 +8,7 @@ import (
8 8
9 renderer "github.com/bnema/vev-vt"9 renderer "github.com/bnema/vev-vt"
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/protocol"
11 )12 )
12 13
13 const (14 const (
@@ -42,8 +43,8 @@ func TestRemotePreviewCodecGoldenRoundTrip(t *testing.T) {
42 if err != nil {43 if err != nil {
43 t.Fatalf("UnmarshalRemotePreview() error = %v", err)44 t.Fatalf("UnmarshalRemotePreview() error = %v", err)
44 }45 }
45- if got.Version != RemotePreviewSchemaVersion || got.Status != RemotePreviewOK {46+ if got.Version != protocol.RemotePreviewSchemaVersion || got.Status != protocol.RemotePreviewOK {
46- t.Fatalf("header = (%d, %d), want (%d, %d)", got.Version, got.Status, RemotePreviewSchemaVersion, RemotePreviewOK)47+ t.Fatalf("header = (%d, %d), want (%d, %d)", got.Version, got.Status, protocol.RemotePreviewSchemaVersion, protocol.RemotePreviewOK)
47 }48 }
48 var wantLifecycle domain.SessionLifecycleID49 var wantLifecycle domain.SessionLifecycleID
49 for i := range wantLifecycle {50 for i := range wantLifecycle {
@@ -86,19 +87,19 @@ func TestRemotePreviewCodecRejectsInvalidStatusFlagsAndRanges(t *testing.T) {
86 func TestRemotePreviewCodecRejectsWideCellBoundaryAndInvalidIDs(t *testing.T) {87 func TestRemotePreviewCodecRejectsWideCellBoundaryAndInvalidIDs(t *testing.T) {
87 var lifecycle domain.SessionLifecycleID88 var lifecycle domain.SessionLifecycleID
88 lifecycle[0] = 189 lifecycle[0] = 1
89- valid := RemotePreview{90+ valid := protocol.RemotePreview{
90- Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,91+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
91 LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1,92 LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1,
92 Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true}},93 Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true}},
93 }94 }
94 if MarshalRemotePreview(valid) == nil {95 if MarshalRemotePreview(valid) == nil {
95 t.Fatal("valid wide-cell preview did not marshal")96 t.Fatal("valid wide-cell preview did not marshal")
96 }97 }
97- for _, malformed := range []RemotePreview{98+ for _, malformed := range []protocol.RemotePreview{
98- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: '界'}}},99+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: '界'}}},
99- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Continuation: true}, {Rune: 'x'}}},100+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Continuation: true}, {Rune: 'x'}}},
100- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true, Rune: 'x'}}},101+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 1, Cells: []renderer.Cell{{Rune: '界'}, {Continuation: true, Rune: 'x'}}},
101- {Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 2, Cells: []renderer.Cell{{Rune: 'x'}, {Rune: '界'}, {Continuation: true}, {Rune: 'y'}}},102+ {Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: lifecycle, TabID: "tab-1", Revision: 1, Width: 2, Height: 2, Cells: []renderer.Cell{{Rune: 'x'}, {Rune: '界'}, {Continuation: true}, {Rune: 'y'}}},
102 } {103 } {
103 if MarshalRemotePreview(malformed) != nil {104 if MarshalRemotePreview(malformed) != nil {
104 t.Fatalf("malformed wide-cell preview marshaled: %#v", malformed.Cells)105 t.Fatalf("malformed wide-cell preview marshaled: %#v", malformed.Cells)
@@ -117,16 +118,16 @@ func TestRemotePreviewCodecRejectsWideCellBoundaryAndInvalidIDs(t *testing.T) {
117 }118 }
118 119
119 func TestRemotePreviewRejectsControlRunes(t *testing.T) {120 func TestRemotePreviewRejectsControlRunes(t *testing.T) {
120- valid := RemotePreview{121+ valid := protocol.RemotePreview{
121- Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,122+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
122 LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1,123 LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1,
123 Cells: []renderer.Cell{{Rune: 'x'}},124 Cells: []renderer.Cell{{Rune: 'x'}},
124 }125 }
125 for _, control := range []rune{'\x01', '\x1b', '\x7f', '\u0085'} {126 for _, control := range []rune{'\x01', '\x1b', '\x7f', '\u0085'} {
126 preview := valid127 preview := valid
127 preview.Cells = []renderer.Cell{{Rune: control}}128 preview.Cells = []renderer.Cell{{Rune: control}}
128- if err := ValidateRemotePreview(preview); err != ErrInvalidRemotePreview {129+ if err := protocol.ValidateRemotePreview(preview); err != protocol.ErrInvalidRemotePreview {
129- t.Fatalf("control rune %U: ValidateRemotePreview() error = %v, want %v", control, err, ErrInvalidRemotePreview)130+ t.Fatalf("control rune %U: ValidateRemotePreview() error = %v, want %v", control, err, protocol.ErrInvalidRemotePreview)
130 }131 }
131 if MarshalRemotePreview(preview) != nil {132 if MarshalRemotePreview(preview) != nil {
132 t.Fatalf("control rune %U was marshaled", control)133 t.Fatalf("control rune %U was marshaled", control)
@@ -137,7 +138,7 @@ func TestRemotePreviewRejectsControlRunes(t *testing.T) {
137 func TestRemotePreviewRequestRejectsOversizedNestedRouteData(t *testing.T) {138 func TestRemotePreviewRequestRejectsOversizedNestedRouteData(t *testing.T) {
138 target := previewTargetForTest()139 target := previewTargetForTest()
139 target.Endpoint = strings.Repeat("h", math.MaxUint16+1)140 target.Endpoint = strings.Repeat("h", math.MaxUint16+1)
140- request := RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}141+ request := protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}
141 if MarshalRemotePreviewRequest(request) != nil {142 if MarshalRemotePreviewRequest(request) != nil {
142 t.Fatal("oversized endpoint was truncated and marshaled")143 t.Fatal("oversized endpoint was truncated and marshaled")
143 }144 }
M internal/ports/remote_preview_test.go +7 -6
@@ -5,6 +5,7 @@ import (
5 5
6 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/protocol"
8 )9 )
9 10
10 func previewTargetForTest() domain.RemoteSessionTarget {11 func previewTargetForTest() domain.RemoteSessionTarget {
@@ -19,8 +20,8 @@ func TestRemotePreviewCodecPreservesWideStyledCells(t *testing.T) {
19 style.Attrs = renderer.AttrUnderline20 style.Attrs = renderer.AttrUnderline
20 style.HasForegroundRGB = true21 style.HasForegroundRGB = true
21 style.ForegroundRGB = renderer.RGB{R: 1, G: 2, B: 3}22 style.ForegroundRGB = renderer.RGB{R: 1, G: 2, B: 3}
22- preview := RemotePreview{23+ preview := protocol.RemotePreview{
23- Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,24+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
24 LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 4, Width: 3, Height: 1,25 LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 4, Width: 3, Height: 1,
25 Cells: []renderer.Cell{{Rune: '界', Style: style}, {Continuation: true, Style: style}, {Rune: 'x', Style: style}},26 Cells: []renderer.Cell{{Rune: '界', Style: style}, {Continuation: true, Style: style}, {Rune: 'x', Style: style}},
26 }27 }
@@ -38,7 +39,7 @@ func TestRemotePreviewCodecPreservesWideStyledCells(t *testing.T) {
38 }39 }
39 40
40 func TestRemotePreviewCodecRejectsMalformedBoundsAndGarbage(t *testing.T) {41 func TestRemotePreviewCodecRejectsMalformedBoundsAndGarbage(t *testing.T) {
41- preview := RemotePreview{Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK, LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: 'x'}}}42+ preview := protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK, LifecycleID: previewTargetForTest().LifecycleID, TabID: "tab-1", Revision: 1, Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: 'x'}}}
42 payload := MarshalRemotePreview(preview)43 payload := MarshalRemotePreview(preview)
43 for i := 0; i < len(payload); i++ {44 for i := 0; i < len(payload); i++ {
44 if _, err := UnmarshalRemotePreview(payload[:i]); err == nil {45 if _, err := UnmarshalRemotePreview(payload[:i]); err == nil {
@@ -49,7 +50,7 @@ func TestRemotePreviewCodecRejectsMalformedBoundsAndGarbage(t *testing.T) {
49 t.Fatal("trailing garbage unexpectedly decoded")50 t.Fatal("trailing garbage unexpectedly decoded")
50 }51 }
51 bad := preview52 bad := preview
52- bad.Width = RemotePreviewMaxWidth + 153+ bad.Width = protocol.RemotePreviewMaxWidth + 1
53 if MarshalRemotePreview(bad) != nil {54 if MarshalRemotePreview(bad) != nil {
54 t.Fatal("oversized preview marshaled")55 t.Fatal("oversized preview marshaled")
55 }56 }
@@ -57,14 +58,14 @@ func TestRemotePreviewCodecRejectsMalformedBoundsAndGarbage(t *testing.T) {
57 58
58 func TestRemotePreviewRequestRejectsStoppedAndInvalidTargets(t *testing.T) {59 func TestRemotePreviewRequestRejectsStoppedAndInvalidTargets(t *testing.T) {
59 target := previewTargetForTest()60 target := previewTargetForTest()
60- request := RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}61+ request := protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}
61 if MarshalRemotePreviewRequest(request) == nil {62 if MarshalRemotePreviewRequest(request) == nil {
62 t.Fatal("valid preview request did not marshal")63 t.Fatal("valid preview request did not marshal")
63 }64 }
64 target.Stopped = true65 target.Stopped = true
65 target.LiveTabID = ""66 target.LiveTabID = ""
66 target.StoppedTab = domain.NewOrdinalTabSelector(0, "", 1)67 target.StoppedTab = domain.NewOrdinalTabSelector(0, "", 1)
67- if MarshalRemotePreviewRequest(RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}) != nil {68+ if MarshalRemotePreviewRequest(protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 20, Height: 5}) != nil {
68 t.Fatal("stopped preview request marshaled")69 t.Fatal("stopped preview request marshaled")
69 }70 }
70 }71 }
M internal/ports/routes_test.go +52 -46
@@ -6,11 +6,12 @@ import (
6 "testing"6 "testing"
7 7
8 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/protocol"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
10 )11 )
11 12
12-func testExactTarget() ExactSessionTarget {13+func testExactTarget() protocol.ExactSessionTarget {
13- return ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1, 2, 3}, SessionName: "work"}14+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1, 2, 3}, SessionName: "work"}
14 }15 }
15 16
16 func TestExactSessionTargetCodecIsStrict(t *testing.T) {17 func TestExactSessionTargetCodecIsStrict(t *testing.T) {
@@ -26,8 +27,8 @@ func TestExactSessionTargetCodecIsStrict(t *testing.T) {
26 require.NoError(t, err)27 require.NoError(t, err)
27 require.Equal(t, want, got)28 require.Equal(t, want, got)
28 29
29- _, err = MarshalExactSessionTarget(ExactSessionTarget{SessionName: "work"})30+ _, err = MarshalExactSessionTarget(protocol.ExactSessionTarget{SessionName: "work"})
30- require.ErrorIs(t, err, ErrInvalidRouteWire)31+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
31 }32 }
32 33
33 func TestHelloExactTargetRoundTrip(t *testing.T) {34 func TestHelloExactTargetRoundTrip(t *testing.T) {
@@ -57,7 +58,7 @@ func TestHelloExactTargetRoundTrip(t *testing.T) {
57 }58 }
58 59
59 func TestRoutePositionCodecIsStrict(t *testing.T) {60 func TestRoutePositionCodecIsStrict(t *testing.T) {
60- want := RoutePosition{Target: testExactTarget(), ActiveTabID: "tab-2"}61+ want := protocol.RoutePosition{Target: testExactTarget(), ActiveTabID: "tab-2"}
61 encoded, err := MarshalRoutePosition(want)62 encoded, err := MarshalRoutePosition(want)
62 require.NoError(t, err)63 require.NoError(t, err)
63 require.Equal(t, "010203000000000000000000000000000004776f726b00057461622d32", hex.EncodeToString(encoded))64 require.Equal(t, "010203000000000000000000000000000004776f726b00057461622d32", hex.EncodeToString(encoded))
@@ -69,14 +70,14 @@ func TestRoutePositionCodecIsStrict(t *testing.T) {
69 require.NoError(t, err)70 require.NoError(t, err)
70 require.Equal(t, want, got)71 require.Equal(t, want, got)
71 72
72- _, err = MarshalRoutePosition(RoutePosition{Target: testExactTarget()})73+ _, err = MarshalRoutePosition(protocol.RoutePosition{Target: testExactTarget()})
73- require.ErrorIs(t, err, ErrInvalidRouteWire)74+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
74- _, err = MarshalRoutePosition(RoutePosition{Target: testExactTarget(), ActiveTabID: "bad tab"})75+ _, err = MarshalRoutePosition(protocol.RoutePosition{Target: testExactTarget(), ActiveTabID: "bad tab"})
75- require.ErrorIs(t, err, ErrInvalidRouteWire)76+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
76 }77 }
77 78
78 func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {79 func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
79- identity := &CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}80+ identity := &protocol.CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
80 want := Welcome{SessionID: "daemon-session", SessionName: "work", Ephemeral: true, CommittedIdentity: identity}81 want := Welcome{SessionID: "daemon-session", SessionName: "work", Ephemeral: true, CommittedIdentity: identity}
81 82
82 got, err := UnmarshalWelcome(MarshalWelcome(want))83 got, err := UnmarshalWelcome(MarshalWelcome(want))
@@ -85,14 +86,14 @@ func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
85 }86 }
86 87
87 func TestWelcomeRejectsMismatchedCommittedIdentity(t *testing.T) {88 func TestWelcomeRejectsMismatchedCommittedIdentity(t *testing.T) {
88- identity := &CommittedRouteIdentity{Target: testExactTarget()}89+ identity := &protocol.CommittedRouteIdentity{Target: testExactTarget()}
89 payload := MarshalWelcome(Welcome{SessionID: "daemon-session", SessionName: "other", CommittedIdentity: identity})90 payload := MarshalWelcome(Welcome{SessionID: "daemon-session", SessionName: "other", CommittedIdentity: identity})
90 91
91 require.Nil(t, payload)92 require.Nil(t, payload)
92 }93 }
93 94
94 func TestCommittedRouteIdentityCodec(t *testing.T) {95 func TestCommittedRouteIdentityCodec(t *testing.T) {
95- want := CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}96+ want := protocol.CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
96 encoded, err := MarshalCommittedRouteIdentity(want)97 encoded, err := MarshalCommittedRouteIdentity(want)
97 require.NoError(t, err)98 require.NoError(t, err)
98 99
@@ -111,17 +112,17 @@ func TestCommittedRouteIdentityCodec(t *testing.T) {
111 require.Error(t, err)112 require.Error(t, err)
112 }113 }
113 114
114-func testRouteSnapshot() RecentRouteSnapshot {115+func testRouteSnapshot() protocol.RecentRouteSnapshot {
115 activeTarget := testExactTarget()116 activeTarget := testExactTarget()
116- logsTarget := ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{4, 5, 6}, SessionName: "logs"}117+ logsTarget := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{4, 5, 6}, SessionName: "logs"}
117- return RecentRouteSnapshot{118+ return protocol.RecentRouteSnapshot{
118 Generation: 9,119 Generation: 9,
119- Active: RouteRef{Key: 11, Generation: 3},120+ Active: protocol.RouteRef{Key: 11, Generation: 3},
120- ActiveEntry: RecentRouteEntry{Key: 11, Generation: 3, Target: activeTarget, Name: "work", Kind: RouteKindLocal, Reachability: RouteReachabilityUnknown},121+ ActiveEntry: protocol.RecentRouteEntry{Key: 11, Generation: 3, Target: activeTarget, Name: "work", Kind: protocol.RouteKindLocal, Reachability: protocol.RouteReachabilityUnknown},
121- Previous: RouteRef{Key: 12, Generation: 4},122+ Previous: protocol.RouteRef{Key: 12, Generation: 4},
122- Home: RouteRef{Key: 11, Generation: 3},123+ Home: protocol.RouteRef{Key: 11, Generation: 3},
123- Entries: []RecentRouteEntry{124+ Entries: []protocol.RecentRouteEntry{
124- {Key: 12, Generation: 4, Target: logsTarget, Name: "logs", HostLabel: "edge", Kind: RouteKindRemote, Attention: true, Reachability: RouteReachabilityUnknown},125+ {Key: 12, Generation: 4, Target: logsTarget, Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote, Attention: true, Reachability: protocol.RouteReachabilityUnknown},
125 },126 },
126 }127 }
127 }128 }
@@ -142,36 +143,41 @@ func TestRecentRouteSnapshotCodecRoundTripAndBounds(t *testing.T) {
142 143
143 invalidCases := []struct {144 invalidCases := []struct {
144 name string145 name string
145- mutate func(*RecentRouteSnapshot)146+ mutate func(*protocol.RecentRouteSnapshot)
146 }{147 }{
147- {name: "missing previous reference", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.Previous = RouteRef{Key: 99, Generation: 99} }},148+ {name: "missing previous reference", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
148- {name: "missing active presentation", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.ActiveEntry = RecentRouteEntry{} }},149+ snapshot.Previous = protocol.RouteRef{Key: 99, Generation: 99}
149- {name: "route name differs from lifecycle target", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.Entries[0].Name = "other" }},150+ }},
150- {name: "entry is active", mutate: func(snapshot *RecentRouteSnapshot) {151+ {name: "missing active presentation", mutate: func(snapshot *protocol.RecentRouteSnapshot) { snapshot.ActiveEntry = protocol.RecentRouteEntry{} }},
152+ {name: "route name differs from lifecycle target", mutate: func(snapshot *protocol.RecentRouteSnapshot) { snapshot.Entries[0].Name = "other" }},
153+ {name: "entry is active", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
151 snapshot.Entries = append(snapshot.Entries, snapshot.ActiveEntry)154 snapshot.Entries = append(snapshot.Entries, snapshot.ActiveEntry)
152 }},155 }},
153- {name: "duplicate entry", mutate: func(snapshot *RecentRouteSnapshot) { snapshot.Entries = append(snapshot.Entries, snapshot.Entries[0]) }},156+ {name: "duplicate entry", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
154- {name: "too many entries", mutate: func(snapshot *RecentRouteSnapshot) {157+ snapshot.Entries = append(snapshot.Entries, snapshot.Entries[0])
155- snapshot.Entries = make([]RecentRouteEntry, RouteSnapshotMaxEntries+1)158+ }},
159+ {name: "too many entries", mutate: func(snapshot *protocol.RecentRouteSnapshot) {
160+ snapshot.Entries = make([]protocol.RecentRouteEntry, protocol.RouteSnapshotMaxEntries+1)
156 for i := range snapshot.Entries {161 for i := range snapshot.Entries {
157- target := ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{byte(i + 1)}, SessionName: "x"}162+ target := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{byte(i + 1)}, SessionName: "x"}
158- snapshot.Entries[i] = RecentRouteEntry{Key: uint64(i + 1), Generation: uint64(i + 1), Target: target, Name: "x", Kind: RouteKindLocal}163+ snapshot.Entries[i] = protocol.RecentRouteEntry{Key: uint64(i + 1), Generation: uint64(i + 1), Target: target, Name: "x", Kind: protocol.RouteKindLocal}
159 }164 }
160 }},165 }},
161 }166 }
162 for _, tc := range invalidCases {167 for _, tc := range invalidCases {
163 t.Run(tc.name, func(t *testing.T) {168 t.Run(tc.name, func(t *testing.T) {
164 invalid := want169 invalid := want
170+ invalid.Entries = append([]protocol.RecentRouteEntry(nil), want.Entries...)
165 tc.mutate(&invalid)171 tc.mutate(&invalid)
166 _, err := MarshalRecentRouteSnapshot(invalid)172 _, err := MarshalRecentRouteSnapshot(invalid)
167- require.ErrorIs(t, err, ErrInvalidRouteWire)173+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
168 })174 })
169 }175 }
170 }176 }
171 177
172 func TestRouteAttentionSubscriptionCodecIsStrict(t *testing.T) {178 func TestRouteAttentionSubscriptionCodecIsStrict(t *testing.T) {
173- want := RouteAttentionSubscription{Targets: []RouteAttentionTarget{{179+ want := protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{{
174- Ref: RouteRef{Key: 7, Generation: 3},180+ Ref: protocol.RouteRef{Key: 7, Generation: 3},
175 Target: testExactTarget(),181 Target: testExactTarget(),
176 }}}182 }}}
177 encoded, err := MarshalRouteAttentionSubscription(want)183 encoded, err := MarshalRouteAttentionSubscription(want)
@@ -185,15 +191,15 @@ func TestRouteAttentionSubscriptionCodecIsStrict(t *testing.T) {
185 require.NoError(t, err)191 require.NoError(t, err)
186 require.Equal(t, want, got)192 require.Equal(t, want, got)
187 193
188- _, err = MarshalRouteAttentionSubscription(RouteAttentionSubscription{Targets: []RouteAttentionTarget{194+ _, err = MarshalRouteAttentionSubscription(protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{
189 want.Targets[0], want.Targets[0],195 want.Targets[0], want.Targets[0],
190 }})196 }})
191- require.ErrorIs(t, err, ErrInvalidRouteWire)197+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
192 }198 }
193 199
194 func TestRecentRouteSnapshotRejectsOversizedFrameBeforeParsing(t *testing.T) {200 func TestRecentRouteSnapshotRejectsOversizedFrameBeforeParsing(t *testing.T) {
195 _, err := UnmarshalRecentRouteSnapshot(make([]byte, MaxFrameLen))201 _, err := UnmarshalRecentRouteSnapshot(make([]byte, MaxFrameLen))
196- require.ErrorIs(t, err, ErrInvalidRouteWire)202+ require.ErrorIs(t, err, protocol.ErrInvalidRouteWire)
197 }203 }
198 204
199 func TestRouteLabelBoundsMatchRemoteDisplayOrigin(t *testing.T) {205 func TestRouteLabelBoundsMatchRemoteDisplayOrigin(t *testing.T) {
@@ -207,7 +213,7 @@ func TestRouteLabelBoundsMatchRemoteDisplayOrigin(t *testing.T) {
207 }213 }
208 for _, tt := range tests {214 for _, tt := range tests {
209 t.Run(tt.name, func(t *testing.T) {215 t.Run(tt.name, func(t *testing.T) {
210- err := ValidateRouteLabel(strings.Repeat("a", tt.length), false)216+ err := protocol.ValidateRouteLabel(strings.Repeat("a", tt.length), false)
211 if tt.wantErr {217 if tt.wantErr {
212 require.Error(t, err)218 require.Error(t, err)
213 return219 return
@@ -221,16 +227,16 @@ func TestRouteLabelsRejectTerminalUnsafeText(t *testing.T) {
221 values := []string{"line\nfeed", "\u2028line-separator", "\u2029paragraph-separator", "\x1b[31mred", "\u202eoverride", string([]byte{0xff, 0xfe})}227 values := []string{"line\nfeed", "\u2028line-separator", "\u2029paragraph-separator", "\x1b[31mred", "\u202eoverride", string([]byte{0xff, 0xfe})}
222 for _, value := range values {228 for _, value := range values {
223 t.Run("name/"+value, func(t *testing.T) {229 t.Run("name/"+value, func(t *testing.T) {
224- _, err := MarshalRecentRouteSnapshot(RecentRouteSnapshot{230+ _, err := MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{
225 Generation: 1,231 Generation: 1,
226- Entries: []RecentRouteEntry{{Key: 1, Generation: 1, Target: ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: value}, Name: value, Kind: RouteKindLocal}},232+ Entries: []protocol.RecentRouteEntry{{Key: 1, Generation: 1, Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: value}, Name: value, Kind: protocol.RouteKindLocal}},
227 })233 })
228 require.Error(t, err)234 require.Error(t, err)
229 })235 })
230 t.Run("host/"+value, func(t *testing.T) {236 t.Run("host/"+value, func(t *testing.T) {
231- _, err := MarshalRecentRouteSnapshot(RecentRouteSnapshot{237+ _, err := MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{
232 Generation: 1,238 Generation: 1,
233- Entries: []RecentRouteEntry{{Key: 1, Generation: 1, Target: testExactTarget(), Name: "work", HostLabel: value, Kind: RouteKindRemote}},239+ Entries: []protocol.RecentRouteEntry{{Key: 1, Generation: 1, Target: testExactTarget(), Name: "work", HostLabel: value, Kind: protocol.RouteKindRemote}},
234 })240 })
235 require.Error(t, err)241 require.Error(t, err)
236 })242 })
@@ -238,7 +244,7 @@ func TestRouteLabelsRejectTerminalUnsafeText(t *testing.T) {
238 }244 }
239 245
240 func TestRouteActionAndFailureCodecsAreBounded(t *testing.T) {246 func TestRouteActionAndFailureCodecsAreBounded(t *testing.T) {
241- action := RouteNavigationAction{SnapshotGeneration: 9, Key: 7, Generation: 8}247+ action := protocol.RouteNavigationAction{SnapshotGeneration: 9, Key: 7, Generation: 8}
242 encoded, err := MarshalRouteNavigationAction(action)248 encoded, err := MarshalRouteNavigationAction(action)
243 require.NoError(t, err)249 require.NoError(t, err)
244 require.Equal(t, "000000000000000900000000000000070000000000000008", hex.EncodeToString(encoded))250 require.Equal(t, "000000000000000900000000000000070000000000000008", hex.EncodeToString(encoded))
@@ -249,7 +255,7 @@ func TestRouteActionAndFailureCodecsAreBounded(t *testing.T) {
249 require.NoError(t, err)255 require.NoError(t, err)
250 require.Equal(t, action, got)256 require.Equal(t, action, got)
251 257
252- failure := RouteNavigationFailure{Key: 7, Generation: 8, Code: RouteFailureStaleSelection}258+ failure := protocol.RouteNavigationFailure{Key: 7, Generation: 8, Code: protocol.RouteFailureStaleSelection}
253 encoded, err = MarshalRouteNavigationFailure(failure)259 encoded, err = MarshalRouteNavigationFailure(failure)
254 require.NoError(t, err)260 require.NoError(t, err)
255 require.Equal(t, "0000000000000007000000000000000801", hex.EncodeToString(encoded))261 require.Equal(t, "0000000000000007000000000000000801", hex.EncodeToString(encoded))
@@ -260,11 +266,11 @@ func TestRouteActionAndFailureCodecsAreBounded(t *testing.T) {
260 require.NoError(t, err)266 require.NoError(t, err)
261 require.Equal(t, failure, gotFailure)267 require.Equal(t, failure, gotFailure)
262 268
263- for _, invalid := range []RouteNavigationAction{{}, {SnapshotGeneration: 1, Key: 1}, {SnapshotGeneration: 1, Generation: 1}, {Key: 1, Generation: 1}} {269+ for _, invalid := range []protocol.RouteNavigationAction{{}, {SnapshotGeneration: 1, Key: 1}, {SnapshotGeneration: 1, Generation: 1}, {Key: 1, Generation: 1}} {
264 _, err := MarshalRouteNavigationAction(invalid)270 _, err := MarshalRouteNavigationAction(invalid)
265 require.Error(t, err)271 require.Error(t, err)
266 }272 }
267- for _, invalid := range []RouteNavigationFailure{{}, {Key: 1, Generation: 1}, {Key: 1, Generation: 1, Code: 255}} {273+ for _, invalid := range []protocol.RouteNavigationFailure{{}, {Key: 1, Generation: 1}, {Key: 1, Generation: 1, Code: 255}} {
268 _, err := MarshalRouteNavigationFailure(invalid)274 _, err := MarshalRouteNavigationFailure(invalid)
269 require.Error(t, err)275 require.Error(t, err)
270 }276 }
M internal/ports/routes_wire.go +165 -167
@@ -1,34 +1,32 @@
1 package ports1 package ports
2 2
3 import (3 import (
4- "errors"
5 "fmt"4 "fmt"
6 5
7 "github.com/bnema/vev/internal/domain"6 "github.com/bnema/vev/internal/domain"
7+ "github.com/bnema/vev/internal/protocol"
8 )8 )
9 9
10-var ErrInvalidRouteWire = errors.New("invalid route wire message")
11-
12 func validateRouteString(value, field string, allowEmpty bool) error {10 func validateRouteString(value, field string, allowEmpty bool) error {
13- if err := ValidateRouteLabel(value, allowEmpty); err != nil {11+ if err := protocol.ValidateRouteLabel(value, allowEmpty); err != nil {
14 return fmt.Errorf("%s: %w", field, err)12 return fmt.Errorf("%s: %w", field, err)
15 }13 }
16 return nil14 return nil
17 }15 }
18 16
19-func marshalExactSessionTarget(w *payloadWriter, target ExactSessionTarget) {17+func marshalExactSessionTarget(w *payloadWriter, target protocol.ExactSessionTarget) {
20 w.putBytes(target.LifecycleID[:])18 w.putBytes(target.LifecycleID[:])
21 w.putString(target.SessionName)19 w.putString(target.SessionName)
22 }20 }
23 21
24-func marshalExactTargetSection(w *payloadWriter, target *ExactSessionTarget) {22+func marshalExactTargetSection(w *payloadWriter, target *protocol.ExactSessionTarget) {
25 w.putBool(target != nil)23 w.putBool(target != nil)
26 if target != nil {24 if target != nil {
27 marshalExactSessionTarget(w, *target)25 marshalExactSessionTarget(w, *target)
28 }26 }
29 }27 }
30 28
31-func unmarshalExactTargetSection(r *payloadReader) (*ExactSessionTarget, error) {29+func unmarshalExactTargetSection(r *payloadReader) (*protocol.ExactSessionTarget, error) {
32 present, err := r.getBool()30 present, err := r.getBool()
33 if err != nil {31 if err != nil {
34 return nil, err32 return nil, err
@@ -57,27 +55,27 @@ func skipExactTargetSection(r *payloadReader) error {
57 return nil55 return nil
58 }56 }
59 57
60-func unmarshalExactSessionTarget(r *payloadReader) (ExactSessionTarget, error) {58+func unmarshalExactSessionTarget(r *payloadReader) (protocol.ExactSessionTarget, error) {
61- var target ExactSessionTarget59+ var target protocol.ExactSessionTarget
62 lifecycle, err := r.getBytes(len(target.LifecycleID))60 lifecycle, err := r.getBytes(len(target.LifecycleID))
63 if err != nil {61 if err != nil {
64- return ExactSessionTarget{}, err62+ return protocol.ExactSessionTarget{}, err
65 }63 }
66 copy(target.LifecycleID[:], lifecycle)64 copy(target.LifecycleID[:], lifecycle)
67 if target.SessionName, err = r.getString(); err != nil {65 if target.SessionName, err = r.getString(); err != nil {
68- return ExactSessionTarget{}, err66+ return protocol.ExactSessionTarget{}, err
69 }67 }
70 if err := target.Validate(); err != nil {68 if err := target.Validate(); err != nil {
71- return ExactSessionTarget{}, fmt.Errorf("%w: exact target: %v", ErrInvalidRouteWire, err)69+ return protocol.ExactSessionTarget{}, fmt.Errorf("%w: exact target: %v", protocol.ErrInvalidRouteWire, err)
72 }70 }
73 return target, nil71 return target, nil
74 }72 }
75 73
76 // MarshalExactSessionTarget encodes the exact lifecycle/name pair used by74 // MarshalExactSessionTarget encodes the exact lifecycle/name pair used by
77 // Hello and route-transition tests.75 // Hello and route-transition tests.
78-func MarshalExactSessionTarget(target ExactSessionTarget) ([]byte, error) {76+func MarshalExactSessionTarget(target protocol.ExactSessionTarget) ([]byte, error) {
79 if err := target.Validate(); err != nil {77 if err := target.Validate(); err != nil {
80- return nil, fmt.Errorf("%w: exact target: %v", ErrInvalidRouteWire, err)78+ return nil, fmt.Errorf("%w: exact target: %v", protocol.ErrInvalidRouteWire, err)
81 }79 }
82 w := payloadWriter{}80 w := payloadWriter{}
83 marshalExactSessionTarget(&w, target)81 marshalExactSessionTarget(&w, target)
@@ -86,21 +84,21 @@ func MarshalExactSessionTarget(target ExactSessionTarget) ([]byte, error) {
86 84
87 // UnmarshalExactSessionTarget decodes one exact lifecycle/name pair and rejects85 // UnmarshalExactSessionTarget decodes one exact lifecycle/name pair and rejects
88 // truncation, invalid identity, and trailing bytes.86 // truncation, invalid identity, and trailing bytes.
89-func UnmarshalExactSessionTarget(b []byte) (ExactSessionTarget, error) {87+func UnmarshalExactSessionTarget(b []byte) (protocol.ExactSessionTarget, error) {
90 r := payloadReader{b: b}88 r := payloadReader{b: b}
91 target, err := unmarshalExactSessionTarget(&r)89 target, err := unmarshalExactSessionTarget(&r)
92 if err != nil {90 if err != nil {
93- return ExactSessionTarget{}, err91+ return protocol.ExactSessionTarget{}, err
94 }92 }
95 if err := r.done(); err != nil {93 if err := r.done(); err != nil {
96- return ExactSessionTarget{}, err94+ return protocol.ExactSessionTarget{}, err
97 }95 }
98 return target, nil96 return target, nil
99 }97 }
100 98
101 // MarshalSamePeerSwitchRequest encodes the strict in-band session switch99 // MarshalSamePeerSwitchRequest encodes the strict in-band session switch
102 // confirmation. The target is non-optional and carries no display origin.100 // confirmation. The target is non-optional and carries no display origin.
103-func MarshalSamePeerSwitchRequest(request SamePeerSwitchRequest) ([]byte, error) {101+func MarshalSamePeerSwitchRequest(request protocol.SamePeerSwitchRequest) ([]byte, error) {
104 if err := request.Validate(); err != nil {102 if err := request.Validate(); err != nil {
105 return nil, err103 return nil, err
106 }104 }
@@ -112,32 +110,32 @@ func MarshalSamePeerSwitchRequest(request SamePeerSwitchRequest) ([]byte, error)
112 }110 }
113 111
114 // UnmarshalSamePeerSwitchRequest decodes one complete in-band switch request.112 // UnmarshalSamePeerSwitchRequest decodes one complete in-band switch request.
115-func UnmarshalSamePeerSwitchRequest(b []byte) (SamePeerSwitchRequest, error) {113+func UnmarshalSamePeerSwitchRequest(b []byte) (protocol.SamePeerSwitchRequest, error) {
116 r := payloadReader{b: b}114 r := payloadReader{b: b}
117- var request SamePeerSwitchRequest115+ var request protocol.SamePeerSwitchRequest
118 var err error116 var err error
119 if request.RequestID, err = r.getUint64(); err != nil {117 if request.RequestID, err = r.getUint64(); err != nil {
120- return SamePeerSwitchRequest{}, err118+ return protocol.SamePeerSwitchRequest{}, err
121 }119 }
122 if request.Target, err = unmarshalExactSessionTarget(&r); err != nil {120 if request.Target, err = unmarshalExactSessionTarget(&r); err != nil {
123- return SamePeerSwitchRequest{}, err121+ return protocol.SamePeerSwitchRequest{}, err
124 }122 }
125 preferred, err := r.getString()123 preferred, err := r.getString()
126 if err != nil {124 if err != nil {
127- return SamePeerSwitchRequest{}, err125+ return protocol.SamePeerSwitchRequest{}, err
128 }126 }
129 request.PreferredTabID = domain.TabStableID(preferred)127 request.PreferredTabID = domain.TabStableID(preferred)
130 if err := r.done(); err != nil {128 if err := r.done(); err != nil {
131- return SamePeerSwitchRequest{}, err129+ return protocol.SamePeerSwitchRequest{}, err
132 }130 }
133 if err := request.Validate(); err != nil {131 if err := request.Validate(); err != nil {
134- return SamePeerSwitchRequest{}, err132+ return protocol.SamePeerSwitchRequest{}, err
135 }133 }
136 return request, nil134 return request, nil
137 }135 }
138 136
139 // MarshalSamePeerSwitchFailure encodes a bounded pre-commit rejection.137 // MarshalSamePeerSwitchFailure encodes a bounded pre-commit rejection.
140-func MarshalSamePeerSwitchFailure(failure SamePeerSwitchFailure) ([]byte, error) {138+func MarshalSamePeerSwitchFailure(failure protocol.SamePeerSwitchFailure) ([]byte, error) {
141 if err := failure.Validate(); err != nil {139 if err := failure.Validate(); err != nil {
142 return nil, err140 return nil, err
143 }141 }
@@ -148,37 +146,37 @@ func MarshalSamePeerSwitchFailure(failure SamePeerSwitchFailure) ([]byte, error)
148 }146 }
149 147
150 // UnmarshalSamePeerSwitchFailure decodes a complete bounded rejection.148 // UnmarshalSamePeerSwitchFailure decodes a complete bounded rejection.
151-func UnmarshalSamePeerSwitchFailure(b []byte) (SamePeerSwitchFailure, error) {149+func UnmarshalSamePeerSwitchFailure(b []byte) (protocol.SamePeerSwitchFailure, error) {
152 r := payloadReader{b: b}150 r := payloadReader{b: b}
153- var failure SamePeerSwitchFailure151+ var failure protocol.SamePeerSwitchFailure
154 var err error152 var err error
155 if failure.RequestID, err = r.getUint64(); err != nil {153 if failure.RequestID, err = r.getUint64(); err != nil {
156- return SamePeerSwitchFailure{}, err154+ return protocol.SamePeerSwitchFailure{}, err
157 }155 }
158 code, err := r.getUint8()156 code, err := r.getUint8()
159 if err != nil {157 if err != nil {
160- return SamePeerSwitchFailure{}, err158+ return protocol.SamePeerSwitchFailure{}, err
161 }159 }
162- failure.Code = SamePeerSwitchFailureCode(code)160+ failure.Code = protocol.SamePeerSwitchFailureCode(code)
163 if err := r.done(); err != nil {161 if err := r.done(); err != nil {
164- return SamePeerSwitchFailure{}, err162+ return protocol.SamePeerSwitchFailure{}, err
165 }163 }
166 if err := failure.Validate(); err != nil {164 if err := failure.Validate(); err != nil {
167- return SamePeerSwitchFailure{}, err165+ return protocol.SamePeerSwitchFailure{}, err
168 }166 }
169 return failure, nil167 return failure, nil
170 }168 }
171 169
172-func validateCommittedRouteIdentity(identity CommittedRouteIdentity) error {170+func validateCommittedRouteIdentity(identity protocol.CommittedRouteIdentity) error {
173 if err := identity.Validate(); err != nil {171 if err := identity.Validate(); err != nil {
174- return fmt.Errorf("%w: committed identity: %v", ErrInvalidRouteWire, err)172+ return fmt.Errorf("%w: committed identity: %v", protocol.ErrInvalidRouteWire, err)
175 }173 }
176 return nil174 return nil
177 }175 }
178 176
179 // MarshalCommittedRouteIdentity encodes the daemon's committed exact session177 // MarshalCommittedRouteIdentity encodes the daemon's committed exact session
180 // identity without exposing transport or client-ledger details.178 // identity without exposing transport or client-ledger details.
181-func MarshalCommittedRouteIdentity(identity CommittedRouteIdentity) ([]byte, error) {179+func MarshalCommittedRouteIdentity(identity protocol.CommittedRouteIdentity) ([]byte, error) {
182 if err := validateCommittedRouteIdentity(identity); err != nil {180 if err := validateCommittedRouteIdentity(identity); err != nil {
183 return nil, err181 return nil, err
184 }182 }
@@ -187,14 +185,14 @@ func MarshalCommittedRouteIdentity(identity CommittedRouteIdentity) ([]byte, err
187 return w.b, nil185 return w.b, nil
188 }186 }
189 187
190-func marshalCommittedRouteIdentityFields(w *payloadWriter, identity CommittedRouteIdentity) {188+func marshalCommittedRouteIdentityFields(w *payloadWriter, identity protocol.CommittedRouteIdentity) {
191 marshalExactSessionTarget(w, identity.Target)189 marshalExactSessionTarget(w, identity.Target)
192 w.putBool(identity.Ephemeral)190 w.putBool(identity.Ephemeral)
193 }191 }
194 192
195 // marshalCommittedIdentitySection shares the standalone identity body and193 // marshalCommittedIdentitySection shares the standalone identity body and
196 // validates embedded identities before Welcome serialization.194 // validates embedded identities before Welcome serialization.
197-func marshalCommittedIdentitySection(w *payloadWriter, identity *CommittedRouteIdentity) bool {195+func marshalCommittedIdentitySection(w *payloadWriter, identity *protocol.CommittedRouteIdentity) bool {
198 w.putBool(identity != nil)196 w.putBool(identity != nil)
199 if identity == nil {197 if identity == nil {
200 return true198 return true
@@ -206,7 +204,7 @@ func marshalCommittedIdentitySection(w *payloadWriter, identity *CommittedRouteI
206 return true204 return true
207 }205 }
208 206
209-func unmarshalCommittedIdentitySection(r *payloadReader) (*CommittedRouteIdentity, error) {207+func unmarshalCommittedIdentitySection(r *payloadReader) (*protocol.CommittedRouteIdentity, error) {
210 present, err := r.getBool()208 present, err := r.getBool()
211 if err != nil {209 if err != nil {
212 return nil, err210 return nil, err
@@ -222,38 +220,38 @@ func unmarshalCommittedIdentitySection(r *payloadReader) (*CommittedRouteIdentit
222 if err != nil {220 if err != nil {
223 return nil, err221 return nil, err
224 }222 }
225- identity := &CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}223+ identity := &protocol.CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
226 if err := validateCommittedRouteIdentity(*identity); err != nil {224 if err := validateCommittedRouteIdentity(*identity); err != nil {
227 return nil, err225 return nil, err
228 }226 }
229 return identity, nil227 return identity, nil
230 }228 }
231 229
232-func UnmarshalCommittedRouteIdentity(b []byte) (CommittedRouteIdentity, error) {230+func UnmarshalCommittedRouteIdentity(b []byte) (protocol.CommittedRouteIdentity, error) {
233 r := payloadReader{b: b}231 r := payloadReader{b: b}
234 target, err := unmarshalExactSessionTarget(&r)232 target, err := unmarshalExactSessionTarget(&r)
235 if err != nil {233 if err != nil {
236- return CommittedRouteIdentity{}, err234+ return protocol.CommittedRouteIdentity{}, err
237 }235 }
238 ephemeral, err := r.getBool()236 ephemeral, err := r.getBool()
239 if err != nil {237 if err != nil {
240- return CommittedRouteIdentity{}, err238+ return protocol.CommittedRouteIdentity{}, err
241 }239 }
242 if err := r.done(); err != nil {240 if err := r.done(); err != nil {
243- return CommittedRouteIdentity{}, err241+ return protocol.CommittedRouteIdentity{}, err
244 }242 }
245- identity := CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}243+ identity := protocol.CommittedRouteIdentity{Target: target, Ephemeral: ephemeral}
246 if err := validateCommittedRouteIdentity(identity); err != nil {244 if err := validateCommittedRouteIdentity(identity); err != nil {
247- return CommittedRouteIdentity{}, err245+ return protocol.CommittedRouteIdentity{}, err
248 }246 }
249 return identity, nil247 return identity, nil
250 }248 }
251 249
252 // MarshalRoutePosition encodes one exact session and its attachment-local tab250 // MarshalRoutePosition encodes one exact session and its attachment-local tab
253 // cursor. Route positions are mutable client memory, not attach authority.251 // cursor. Route positions are mutable client memory, not attach authority.
254-func MarshalRoutePosition(position RoutePosition) ([]byte, error) {252+func MarshalRoutePosition(position protocol.RoutePosition) ([]byte, error) {
255 if err := position.Validate(); err != nil {253 if err := position.Validate(); err != nil {
256- return nil, fmt.Errorf("%w: route position: %v", ErrInvalidRouteWire, err)254+ return nil, fmt.Errorf("%w: route position: %v", protocol.ErrInvalidRouteWire, err)
257 }255 }
258 w := payloadWriter{}256 w := payloadWriter{}
259 marshalExactSessionTarget(&w, position.Target)257 marshalExactSessionTarget(&w, position.Target)
@@ -262,85 +260,85 @@ func MarshalRoutePosition(position RoutePosition) ([]byte, error) {
262 }260 }
263 261
264 // UnmarshalRoutePosition decodes one strict route-position payload.262 // UnmarshalRoutePosition decodes one strict route-position payload.
265-func UnmarshalRoutePosition(b []byte) (RoutePosition, error) {263+func UnmarshalRoutePosition(b []byte) (protocol.RoutePosition, error) {
266 r := payloadReader{b: b}264 r := payloadReader{b: b}
267 target, err := unmarshalExactSessionTarget(&r)265 target, err := unmarshalExactSessionTarget(&r)
268 if err != nil {266 if err != nil {
269- return RoutePosition{}, err267+ return protocol.RoutePosition{}, err
270 }268 }
271 activeTabID, err := r.getString()269 activeTabID, err := r.getString()
272 if err != nil {270 if err != nil {
273- return RoutePosition{}, err271+ return protocol.RoutePosition{}, err
274 }272 }
275 if err := r.done(); err != nil {273 if err := r.done(); err != nil {
276- return RoutePosition{}, err274+ return protocol.RoutePosition{}, err
277 }275 }
278- position := RoutePosition{Target: target, ActiveTabID: domain.TabStableID(activeTabID)}276+ position := protocol.RoutePosition{Target: target, ActiveTabID: domain.TabStableID(activeTabID)}
279 if err := position.Validate(); err != nil {277 if err := position.Validate(); err != nil {
280- return RoutePosition{}, fmt.Errorf("%w: route position: %v", ErrInvalidRouteWire, err)278+ return protocol.RoutePosition{}, fmt.Errorf("%w: route position: %v", protocol.ErrInvalidRouteWire, err)
281 }279 }
282 return position, nil280 return position, nil
283 }281 }
284 282
285-func validateRouteRef(ref RouteRef) error {283+func validateRouteRef(ref protocol.RouteRef) error {
286 if err := ref.Validate(); err != nil {284 if err := ref.Validate(); err != nil {
287- return fmt.Errorf("%w: route reference: %v", ErrInvalidRouteWire, err)285+ return fmt.Errorf("%w: route reference: %v", protocol.ErrInvalidRouteWire, err)
288 }286 }
289 return nil287 return nil
290 }288 }
291 289
292-func marshalRouteRef(w *payloadWriter, ref RouteRef) {290+func marshalRouteRef(w *payloadWriter, ref protocol.RouteRef) {
293 w.putUint64(ref.Key)291 w.putUint64(ref.Key)
294 w.putUint64(ref.Generation)292 w.putUint64(ref.Generation)
295 }293 }
296 294
297-func unmarshalRouteRef(r *payloadReader) (RouteRef, error) {295+func unmarshalRouteRef(r *payloadReader) (protocol.RouteRef, error) {
298 key, err := r.getUint64()296 key, err := r.getUint64()
299 if err != nil {297 if err != nil {
300- return RouteRef{}, err298+ return protocol.RouteRef{}, err
301 }299 }
302 generation, err := r.getUint64()300 generation, err := r.getUint64()
303 if err != nil {301 if err != nil {
304- return RouteRef{}, err302+ return protocol.RouteRef{}, err
305 }303 }
306- ref := RouteRef{Key: key, Generation: generation}304+ ref := protocol.RouteRef{Key: key, Generation: generation}
307 if err := validateRouteRef(ref); err != nil {305 if err := validateRouteRef(ref); err != nil {
308- return RouteRef{}, err306+ return protocol.RouteRef{}, err
309 }307 }
310 return ref, nil308 return ref, nil
311 }309 }
312 310
313-func validateRecentRouteEntry(entry RecentRouteEntry) error {311+func validateRecentRouteEntry(entry protocol.RecentRouteEntry) error {
314 if entry.Key == 0 || entry.Generation == 0 {312 if entry.Key == 0 || entry.Generation == 0 {
315- return fmt.Errorf("%w: route entry identity is zero", ErrInvalidRouteWire)313+ return fmt.Errorf("%w: route entry identity is zero", protocol.ErrInvalidRouteWire)
316 }314 }
317 if err := entry.Target.Validate(); err != nil {315 if err := entry.Target.Validate(); err != nil {
318- return fmt.Errorf("%w: invalid route lifecycle target: %v", ErrInvalidRouteWire, err)316+ return fmt.Errorf("%w: invalid route lifecycle target: %v", protocol.ErrInvalidRouteWire, err)
319 }317 }
320 if entry.Name != entry.Target.SessionName {318 if entry.Name != entry.Target.SessionName {
321- return fmt.Errorf("%w: route name does not match lifecycle target", ErrInvalidRouteWire)319+ return fmt.Errorf("%w: route name does not match lifecycle target", protocol.ErrInvalidRouteWire)
322 }320 }
323 if err := validateRouteString(entry.Name, "route name", false); err != nil {321 if err := validateRouteString(entry.Name, "route name", false); err != nil {
324- return fmt.Errorf("%w: %v", ErrInvalidRouteWire, err)322+ return fmt.Errorf("%w: %v", protocol.ErrInvalidRouteWire, err)
325 }323 }
326 if err := validateRouteString(entry.HostLabel, "route host label", true); err != nil {324 if err := validateRouteString(entry.HostLabel, "route host label", true); err != nil {
327- return fmt.Errorf("%w: %v", ErrInvalidRouteWire, err)325+ return fmt.Errorf("%w: %v", protocol.ErrInvalidRouteWire, err)
328 }326 }
329- if !entry.Kind.valid() {327+ if entry.Kind.Validate() != nil {
330- return fmt.Errorf("%w: invalid route kind", ErrInvalidRouteWire)328+ return fmt.Errorf("%w: invalid route kind", protocol.ErrInvalidRouteWire)
331 }329 }
332- if !entry.Reachability.valid() {330+ if entry.Reachability.Validate() != nil {
333- return fmt.Errorf("%w: invalid route reachability", ErrInvalidRouteWire)331+ return fmt.Errorf("%w: invalid route reachability", protocol.ErrInvalidRouteWire)
334 }332 }
335 return nil333 return nil
336 }334 }
337 335
338-func validateRecentRouteSnapshot(snapshot RecentRouteSnapshot) error {336+func validateRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) error {
339- if len(snapshot.Entries) > RouteSnapshotMaxEntries {337+ if len(snapshot.Entries) > protocol.RouteSnapshotMaxEntries {
340- return fmt.Errorf("%w: too many route entries", ErrInvalidRouteWire)338+ return fmt.Errorf("%w: too many route entries", protocol.ErrInvalidRouteWire)
341 }339 }
342- if snapshot.Generation == 0 && (len(snapshot.Entries) != 0 || snapshot.ActiveEntry != (RecentRouteEntry{})) {340+ if snapshot.Generation == 0 && (len(snapshot.Entries) != 0 || snapshot.ActiveEntry != (protocol.RecentRouteEntry{})) {
343- return fmt.Errorf("%w: non-empty snapshot has zero generation", ErrInvalidRouteWire)341+ return fmt.Errorf("%w: non-empty snapshot has zero generation", protocol.ErrInvalidRouteWire)
344 }342 }
345 if err := validateRouteRef(snapshot.Active); err != nil {343 if err := validateRouteRef(snapshot.Active); err != nil {
346 return err344 return err
@@ -351,55 +349,55 @@ func validateRecentRouteSnapshot(snapshot RecentRouteSnapshot) error {
351 if err := validateRouteRef(snapshot.Home); err != nil {349 if err := validateRouteRef(snapshot.Home); err != nil {
352 return err350 return err
353 }351 }
354- if snapshot.Active.empty() {352+ if snapshot.Active.IsZero() {
355- if snapshot.ActiveEntry != (RecentRouteEntry{}) {353+ if snapshot.ActiveEntry != (protocol.RecentRouteEntry{}) {
356- return fmt.Errorf("%w: active presentation has no active route", ErrInvalidRouteWire)354+ return fmt.Errorf("%w: active presentation has no active route", protocol.ErrInvalidRouteWire)
357 }355 }
358 } else {356 } else {
359 if err := validateRecentRouteEntry(snapshot.ActiveEntry); err != nil {357 if err := validateRecentRouteEntry(snapshot.ActiveEntry); err != nil {
360 return err358 return err
361 }359 }
362- if snapshot.Active != (RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation}) {360+ if snapshot.Active != (protocol.RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation}) {
363- return fmt.Errorf("%w: active presentation does not match active route", ErrInvalidRouteWire)361+ return fmt.Errorf("%w: active presentation does not match active route", protocol.ErrInvalidRouteWire)
364 }362 }
365 }363 }
366- if !snapshot.Active.empty() && snapshot.Active == snapshot.Previous {364+ if !snapshot.Active.IsZero() && snapshot.Active == snapshot.Previous {
367- return fmt.Errorf("%w: active and previous routes are identical", ErrInvalidRouteWire)365+ return fmt.Errorf("%w: active and previous routes are identical", protocol.ErrInvalidRouteWire)
368 }366 }
369- refs := make(map[RouteRef]struct{}, len(snapshot.Entries))367+ refs := make(map[protocol.RouteRef]struct{}, len(snapshot.Entries))
370 for _, entry := range snapshot.Entries {368 for _, entry := range snapshot.Entries {
371- if snapshot.Active == (RouteRef{Key: entry.Key, Generation: entry.Generation}) {369+ if snapshot.Active == (protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}) {
372- return fmt.Errorf("%w: active route must be metadata-only", ErrInvalidRouteWire)370+ return fmt.Errorf("%w: active route must be metadata-only", protocol.ErrInvalidRouteWire)
373 }371 }
374 if err := validateRecentRouteEntry(entry); err != nil {372 if err := validateRecentRouteEntry(entry); err != nil {
375 return err373 return err
376 }374 }
377- ref := RouteRef{Key: entry.Key, Generation: entry.Generation}375+ ref := protocol.RouteRef{Key: entry.Key, Generation: entry.Generation}
378 if _, exists := refs[ref]; exists {376 if _, exists := refs[ref]; exists {
379- return fmt.Errorf("%w: duplicate route entry", ErrInvalidRouteWire)377+ return fmt.Errorf("%w: duplicate route entry", protocol.ErrInvalidRouteWire)
380 }378 }
381 refs[ref] = struct{}{}379 refs[ref] = struct{}{}
382 }380 }
383 for _, item := range []struct {381 for _, item := range []struct {
384 name string382 name string
385- ref RouteRef383+ ref protocol.RouteRef
386 }{384 }{
387 {name: "previous", ref: snapshot.Previous},385 {name: "previous", ref: snapshot.Previous},
388 {name: "home", ref: snapshot.Home},386 {name: "home", ref: snapshot.Home},
389 } {387 } {
390- if item.ref.empty() || item.ref == snapshot.Active {388+ if item.ref.IsZero() || item.ref == snapshot.Active {
391 // Active is metadata-only and intentionally excluded from the389 // Active is metadata-only and intentionally excluded from the
392 // recent display entries. Home may point at active as well.390 // recent display entries. Home may point at active as well.
393 continue391 continue
394 }392 }
395 if _, exists := refs[item.ref]; !exists {393 if _, exists := refs[item.ref]; !exists {
396- return fmt.Errorf("%w: %s route reference is absent from snapshot", ErrInvalidRouteWire, item.name)394+ return fmt.Errorf("%w: %s route reference is absent from snapshot", protocol.ErrInvalidRouteWire, item.name)
397 }395 }
398 }396 }
399 return nil397 return nil
400 }398 }
401 399
402-func marshalRecentRouteEntry(w *payloadWriter, entry RecentRouteEntry) {400+func marshalRecentRouteEntry(w *payloadWriter, entry protocol.RecentRouteEntry) {
403 w.putUint64(entry.Key)401 w.putUint64(entry.Key)
404 w.putUint64(entry.Generation)402 w.putUint64(entry.Generation)
405 marshalExactSessionTarget(w, entry.Target)403 marshalExactSessionTarget(w, entry.Target)
@@ -411,52 +409,52 @@ func marshalRecentRouteEntry(w *payloadWriter, entry RecentRouteEntry) {
411 w.putUint8(uint8(entry.Reachability))409 w.putUint8(uint8(entry.Reachability))
412 }410 }
413 411
414-func unmarshalRecentRouteEntry(r *payloadReader) (RecentRouteEntry, error) {412+func unmarshalRecentRouteEntry(r *payloadReader) (protocol.RecentRouteEntry, error) {
415- var entry RecentRouteEntry413+ var entry protocol.RecentRouteEntry
416 var err error414 var err error
417 if entry.Key, err = r.getUint64(); err != nil {415 if entry.Key, err = r.getUint64(); err != nil {
418- return RecentRouteEntry{}, err416+ return protocol.RecentRouteEntry{}, err
419 }417 }
420 if entry.Generation, err = r.getUint64(); err != nil {418 if entry.Generation, err = r.getUint64(); err != nil {
421- return RecentRouteEntry{}, err419+ return protocol.RecentRouteEntry{}, err
422 }420 }
423 if entry.Target, err = unmarshalExactSessionTarget(r); err != nil {421 if entry.Target, err = unmarshalExactSessionTarget(r); err != nil {
424- return RecentRouteEntry{}, err422+ return protocol.RecentRouteEntry{}, err
425 }423 }
426 if entry.Name, err = r.getString(); err != nil {424 if entry.Name, err = r.getString(); err != nil {
427- return RecentRouteEntry{}, err425+ return protocol.RecentRouteEntry{}, err
428 }426 }
429 if entry.HostLabel, err = r.getString(); err != nil {427 if entry.HostLabel, err = r.getString(); err != nil {
430- return RecentRouteEntry{}, err428+ return protocol.RecentRouteEntry{}, err
431 }429 }
432 kind, err := r.getUint8()430 kind, err := r.getUint8()
433 if err != nil {431 if err != nil {
434- return RecentRouteEntry{}, err432+ return protocol.RecentRouteEntry{}, err
435 }433 }
436- entry.Kind = RouteKind(kind)434+ entry.Kind = protocol.RouteKind(kind)
437 if entry.Ephemeral, err = r.getBool(); err != nil {435 if entry.Ephemeral, err = r.getBool(); err != nil {
438- return RecentRouteEntry{}, err436+ return protocol.RecentRouteEntry{}, err
439 }437 }
440 if entry.Attention, err = r.getBool(); err != nil {438 if entry.Attention, err = r.getBool(); err != nil {
441- return RecentRouteEntry{}, err439+ return protocol.RecentRouteEntry{}, err
442 }440 }
443 reachability, err := r.getUint8()441 reachability, err := r.getUint8()
444 if err != nil {442 if err != nil {
445- return RecentRouteEntry{}, err443+ return protocol.RecentRouteEntry{}, err
446 }444 }
447- entry.Reachability = RouteReachability(reachability)445+ entry.Reachability = protocol.RouteReachability(reachability)
448 return entry, nil446 return entry, nil
449 }447 }
450 448
451 // MarshalRecentRouteSnapshot encodes a complete bounded client route view.449 // MarshalRecentRouteSnapshot encodes a complete bounded client route view.
452-func MarshalRecentRouteSnapshot(snapshot RecentRouteSnapshot) ([]byte, error) {450+func MarshalRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) ([]byte, error) {
453 if err := validateRecentRouteSnapshot(snapshot); err != nil {451 if err := validateRecentRouteSnapshot(snapshot); err != nil {
454 return nil, err452 return nil, err
455 }453 }
456 w := payloadWriter{}454 w := payloadWriter{}
457 w.putUint64(snapshot.Generation)455 w.putUint64(snapshot.Generation)
458 marshalRouteRef(&w, snapshot.Active)456 marshalRouteRef(&w, snapshot.Active)
459- if !snapshot.Active.empty() {457+ if !snapshot.Active.IsZero() {
460 marshalRecentRouteEntry(&w, snapshot.ActiveEntry)458 marshalRecentRouteEntry(&w, snapshot.ActiveEntry)
461 }459 }
462 marshalRouteRef(&w, snapshot.Previous)460 marshalRouteRef(&w, snapshot.Previous)
@@ -469,25 +467,25 @@ func MarshalRecentRouteSnapshot(snapshot RecentRouteSnapshot) ([]byte, error) {
469 // make this unreachable; it protects the wire contract if those bounds467 // make this unreachable; it protects the wire contract if those bounds
470 // change independently in a future protocol revision.468 // change independently in a future protocol revision.
471 if len(w.b) > MaxFrameLen-1 {469 if len(w.b) > MaxFrameLen-1 {
472- return nil, fmt.Errorf("%w: route snapshot is too large", ErrInvalidRouteWire)470+ return nil, fmt.Errorf("%w: route snapshot is too large", protocol.ErrInvalidRouteWire)
473 }471 }
474 return w.b, nil472 return w.b, nil
475 }473 }
476 474
477-func validateRouteAttentionSubscription(subscription RouteAttentionSubscription) error {475+func validateRouteAttentionSubscription(subscription protocol.RouteAttentionSubscription) error {
478- if len(subscription.Targets) > RouteSnapshotMaxEntries {476+ if len(subscription.Targets) > protocol.RouteSnapshotMaxEntries {
479- return fmt.Errorf("%w: too many attention targets", ErrInvalidRouteWire)477+ return fmt.Errorf("%w: too many attention targets", protocol.ErrInvalidRouteWire)
480 }478 }
481- refs := make(map[RouteRef]struct{}, len(subscription.Targets))479+ refs := make(map[protocol.RouteRef]struct{}, len(subscription.Targets))
482 for _, target := range subscription.Targets {480 for _, target := range subscription.Targets {
483- if err := validateRouteRef(target.Ref); err != nil || target.Ref.empty() {481+ if err := validateRouteRef(target.Ref); err != nil || target.Ref.IsZero() {
484- return fmt.Errorf("%w: invalid attention route reference", ErrInvalidRouteWire)482+ return fmt.Errorf("%w: invalid attention route reference", protocol.ErrInvalidRouteWire)
485 }483 }
486 if err := target.Target.Validate(); err != nil {484 if err := target.Target.Validate(); err != nil {
487- return fmt.Errorf("%w: invalid attention target: %v", ErrInvalidRouteWire, err)485+ return fmt.Errorf("%w: invalid attention target: %v", protocol.ErrInvalidRouteWire, err)
488 }486 }
489 if _, exists := refs[target.Ref]; exists {487 if _, exists := refs[target.Ref]; exists {
490- return fmt.Errorf("%w: duplicate attention route reference", ErrInvalidRouteWire)488+ return fmt.Errorf("%w: duplicate attention route reference", protocol.ErrInvalidRouteWire)
491 }489 }
492 refs[target.Ref] = struct{}{}490 refs[target.Ref] = struct{}{}
493 }491 }
@@ -496,7 +494,7 @@ func validateRouteAttentionSubscription(subscription RouteAttentionSubscription)
496 494
497 // MarshalRouteAttentionSubscription encodes the bounded client mapping used495 // MarshalRouteAttentionSubscription encodes the bounded client mapping used
498 // by the connected daemon to resolve live route-attention indicators.496 // by the connected daemon to resolve live route-attention indicators.
499-func MarshalRouteAttentionSubscription(subscription RouteAttentionSubscription) ([]byte, error) {497+func MarshalRouteAttentionSubscription(subscription protocol.RouteAttentionSubscription) ([]byte, error) {
500 if err := validateRouteAttentionSubscription(subscription); err != nil {498 if err := validateRouteAttentionSubscription(subscription); err != nil {
501 return nil, err499 return nil, err
502 }500 }
@@ -511,96 +509,96 @@ func MarshalRouteAttentionSubscription(subscription RouteAttentionSubscription)
511 509
512 // UnmarshalRouteAttentionSubscription decodes one strict bounded attention510 // UnmarshalRouteAttentionSubscription decodes one strict bounded attention
513 // mapping and rejects truncated, malformed, and trailing payloads.511 // mapping and rejects truncated, malformed, and trailing payloads.
514-func UnmarshalRouteAttentionSubscription(b []byte) (RouteAttentionSubscription, error) {512+func UnmarshalRouteAttentionSubscription(b []byte) (protocol.RouteAttentionSubscription, error) {
515 r := payloadReader{b: b}513 r := payloadReader{b: b}
516 count, err := r.getUint8()514 count, err := r.getUint8()
517 if err != nil {515 if err != nil {
518- return RouteAttentionSubscription{}, err516+ return protocol.RouteAttentionSubscription{}, err
519 }517 }
520- if int(count) > RouteSnapshotMaxEntries {518+ if int(count) > protocol.RouteSnapshotMaxEntries {
521- return RouteAttentionSubscription{}, fmt.Errorf("%w: too many attention targets", ErrInvalidRouteWire)519+ return protocol.RouteAttentionSubscription{}, fmt.Errorf("%w: too many attention targets", protocol.ErrInvalidRouteWire)
522 }520 }
523- subscription := RouteAttentionSubscription{Targets: make([]RouteAttentionTarget, 0, int(count))}521+ subscription := protocol.RouteAttentionSubscription{Targets: make([]protocol.RouteAttentionTarget, 0, int(count))}
524 for range int(count) {522 for range int(count) {
525 ref, err := unmarshalRouteRef(&r)523 ref, err := unmarshalRouteRef(&r)
526 if err != nil {524 if err != nil {
527- return RouteAttentionSubscription{}, err525+ return protocol.RouteAttentionSubscription{}, err
528 }526 }
529 target, err := unmarshalExactSessionTarget(&r)527 target, err := unmarshalExactSessionTarget(&r)
530 if err != nil {528 if err != nil {
531- return RouteAttentionSubscription{}, err529+ return protocol.RouteAttentionSubscription{}, err
532 }530 }
533- subscription.Targets = append(subscription.Targets, RouteAttentionTarget{Ref: ref, Target: target})531+ subscription.Targets = append(subscription.Targets, protocol.RouteAttentionTarget{Ref: ref, Target: target})
534 }532 }
535 if err := r.done(); err != nil {533 if err := r.done(); err != nil {
536- return RouteAttentionSubscription{}, err534+ return protocol.RouteAttentionSubscription{}, err
537 }535 }
538 if err := validateRouteAttentionSubscription(subscription); err != nil {536 if err := validateRouteAttentionSubscription(subscription); err != nil {
539- return RouteAttentionSubscription{}, err537+ return protocol.RouteAttentionSubscription{}, err
540 }538 }
541 return subscription, nil539 return subscription, nil
542 }540 }
543 541
544 // UnmarshalRecentRouteSnapshot decodes and strictly validates one complete542 // UnmarshalRecentRouteSnapshot decodes and strictly validates one complete
545 // immutable snapshot, including every referenced active/previous/home entry.543 // immutable snapshot, including every referenced active/previous/home entry.
546-func UnmarshalRecentRouteSnapshot(b []byte) (RecentRouteSnapshot, error) {544+func UnmarshalRecentRouteSnapshot(b []byte) (protocol.RecentRouteSnapshot, error) {
547 if len(b) > MaxFrameLen-1 {545 if len(b) > MaxFrameLen-1 {
548- return RecentRouteSnapshot{}, ErrInvalidRouteWire546+ return protocol.RecentRouteSnapshot{}, protocol.ErrInvalidRouteWire
549 }547 }
550 r := payloadReader{b: b}548 r := payloadReader{b: b}
551- var snapshot RecentRouteSnapshot549+ var snapshot protocol.RecentRouteSnapshot
552 var err error550 var err error
553 if snapshot.Generation, err = r.getUint64(); err != nil {551 if snapshot.Generation, err = r.getUint64(); err != nil {
554- return RecentRouteSnapshot{}, err552+ return protocol.RecentRouteSnapshot{}, err
555 }553 }
556 if snapshot.Active, err = unmarshalRouteRef(&r); err != nil {554 if snapshot.Active, err = unmarshalRouteRef(&r); err != nil {
557- return RecentRouteSnapshot{}, err555+ return protocol.RecentRouteSnapshot{}, err
558 }556 }
559- if !snapshot.Active.empty() {557+ if !snapshot.Active.IsZero() {
560 if snapshot.ActiveEntry, err = unmarshalRecentRouteEntry(&r); err != nil {558 if snapshot.ActiveEntry, err = unmarshalRecentRouteEntry(&r); err != nil {
561- return RecentRouteSnapshot{}, err559+ return protocol.RecentRouteSnapshot{}, err
562 }560 }
563 }561 }
564 if snapshot.Previous, err = unmarshalRouteRef(&r); err != nil {562 if snapshot.Previous, err = unmarshalRouteRef(&r); err != nil {
565- return RecentRouteSnapshot{}, err563+ return protocol.RecentRouteSnapshot{}, err
566 }564 }
567 if snapshot.Home, err = unmarshalRouteRef(&r); err != nil {565 if snapshot.Home, err = unmarshalRouteRef(&r); err != nil {
568- return RecentRouteSnapshot{}, err566+ return protocol.RecentRouteSnapshot{}, err
569 }567 }
570 count, err := r.getUint8()568 count, err := r.getUint8()
571 if err != nil {569 if err != nil {
572- return RecentRouteSnapshot{}, err570+ return protocol.RecentRouteSnapshot{}, err
573 }571 }
574- if int(count) > RouteSnapshotMaxEntries {572+ if int(count) > protocol.RouteSnapshotMaxEntries {
575- return RecentRouteSnapshot{}, fmt.Errorf("%w: too many route entries", ErrInvalidRouteWire)573+ return protocol.RecentRouteSnapshot{}, fmt.Errorf("%w: too many route entries", protocol.ErrInvalidRouteWire)
576 }574 }
577 if count != 0 {575 if count != 0 {
578- snapshot.Entries = make([]RecentRouteEntry, 0, int(count))576+ snapshot.Entries = make([]protocol.RecentRouteEntry, 0, int(count))
579 }577 }
580 for range int(count) {578 for range int(count) {
581 entry, err := unmarshalRecentRouteEntry(&r)579 entry, err := unmarshalRecentRouteEntry(&r)
582 if err != nil {580 if err != nil {
583- return RecentRouteSnapshot{}, err581+ return protocol.RecentRouteSnapshot{}, err
584 }582 }
585 snapshot.Entries = append(snapshot.Entries, entry)583 snapshot.Entries = append(snapshot.Entries, entry)
586 }584 }
587 if err := r.done(); err != nil {585 if err := r.done(); err != nil {
588- return RecentRouteSnapshot{}, err586+ return protocol.RecentRouteSnapshot{}, err
589 }587 }
590 if err := validateRecentRouteSnapshot(snapshot); err != nil {588 if err := validateRecentRouteSnapshot(snapshot); err != nil {
591- return RecentRouteSnapshot{}, err589+ return protocol.RecentRouteSnapshot{}, err
592 }590 }
593 return snapshot, nil591 return snapshot, nil
594 }592 }
595 593
596-func validateRouteNavigationAction(action RouteNavigationAction) error {594+func validateRouteNavigationAction(action protocol.RouteNavigationAction) error {
597 if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {595 if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {
598- return fmt.Errorf("%w: navigation action identity is zero", ErrInvalidRouteWire)596+ return fmt.Errorf("%w: navigation action identity is zero", protocol.ErrInvalidRouteWire)
599 }597 }
600 return nil598 return nil
601 }599 }
602 600
603-func MarshalRouteNavigationAction(action RouteNavigationAction) ([]byte, error) {601+func MarshalRouteNavigationAction(action protocol.RouteNavigationAction) ([]byte, error) {
604 if err := validateRouteNavigationAction(action); err != nil {602 if err := validateRouteNavigationAction(action); err != nil {
605 return nil, err603 return nil, err
606 }604 }
@@ -611,39 +609,39 @@ func MarshalRouteNavigationAction(action RouteNavigationAction) ([]byte, error)
611 return w.b, nil609 return w.b, nil
612 }610 }
613 611
614-func UnmarshalRouteNavigationAction(b []byte) (RouteNavigationAction, error) {612+func UnmarshalRouteNavigationAction(b []byte) (protocol.RouteNavigationAction, error) {
615 r := payloadReader{b: b}613 r := payloadReader{b: b}
616- var action RouteNavigationAction614+ var action protocol.RouteNavigationAction
617 var err error615 var err error
618 if action.SnapshotGeneration, err = r.getUint64(); err != nil {616 if action.SnapshotGeneration, err = r.getUint64(); err != nil {
619- return RouteNavigationAction{}, err617+ return protocol.RouteNavigationAction{}, err
620 }618 }
621 if action.Key, err = r.getUint64(); err != nil {619 if action.Key, err = r.getUint64(); err != nil {
622- return RouteNavigationAction{}, err620+ return protocol.RouteNavigationAction{}, err
623 }621 }
624 if action.Generation, err = r.getUint64(); err != nil {622 if action.Generation, err = r.getUint64(); err != nil {
625- return RouteNavigationAction{}, err623+ return protocol.RouteNavigationAction{}, err
626 }624 }
627 if err := r.done(); err != nil {625 if err := r.done(); err != nil {
628- return RouteNavigationAction{}, err626+ return protocol.RouteNavigationAction{}, err
629 }627 }
630 if err := validateRouteNavigationAction(action); err != nil {628 if err := validateRouteNavigationAction(action); err != nil {
631- return RouteNavigationAction{}, err629+ return protocol.RouteNavigationAction{}, err
632 }630 }
633 return action, nil631 return action, nil
634 }632 }
635 633
636-func validateRouteNavigationFailure(failure RouteNavigationFailure) error {634+func validateRouteNavigationFailure(failure protocol.RouteNavigationFailure) error {
637 if failure.Key == 0 || failure.Generation == 0 {635 if failure.Key == 0 || failure.Generation == 0 {
638- return fmt.Errorf("%w: failure identity is zero", ErrInvalidRouteWire)636+ return fmt.Errorf("%w: failure identity is zero", protocol.ErrInvalidRouteWire)
639 }637 }
640- if !failure.Code.valid() {638+ if failure.Code.Validate() != nil {
641- return fmt.Errorf("%w: invalid failure code", ErrInvalidRouteWire)639+ return fmt.Errorf("%w: invalid failure code", protocol.ErrInvalidRouteWire)
642 }640 }
643 return nil641 return nil
644 }642 }
645 643
646-func MarshalRouteNavigationFailure(failure RouteNavigationFailure) ([]byte, error) {644+func MarshalRouteNavigationFailure(failure protocol.RouteNavigationFailure) ([]byte, error) {
647 if err := validateRouteNavigationFailure(failure); err != nil {645 if err := validateRouteNavigationFailure(failure); err != nil {
648 return nil, err646 return nil, err
649 }647 }
@@ -654,26 +652,26 @@ func MarshalRouteNavigationFailure(failure RouteNavigationFailure) ([]byte, erro
654 return w.b, nil652 return w.b, nil
655 }653 }
656 654
657-func UnmarshalRouteNavigationFailure(b []byte) (RouteNavigationFailure, error) {655+func UnmarshalRouteNavigationFailure(b []byte) (protocol.RouteNavigationFailure, error) {
658 r := payloadReader{b: b}656 r := payloadReader{b: b}
659- var failure RouteNavigationFailure657+ var failure protocol.RouteNavigationFailure
660 var err error658 var err error
661 if failure.Key, err = r.getUint64(); err != nil {659 if failure.Key, err = r.getUint64(); err != nil {
662- return RouteNavigationFailure{}, err660+ return protocol.RouteNavigationFailure{}, err
663 }661 }
664 if failure.Generation, err = r.getUint64(); err != nil {662 if failure.Generation, err = r.getUint64(); err != nil {
665- return RouteNavigationFailure{}, err663+ return protocol.RouteNavigationFailure{}, err
666 }664 }
667 code, err := r.getUint8()665 code, err := r.getUint8()
668 if err != nil {666 if err != nil {
669- return RouteNavigationFailure{}, err667+ return protocol.RouteNavigationFailure{}, err
670 }668 }
671- failure.Code = RouteFailureCode(code)669+ failure.Code = protocol.RouteFailureCode(code)
672 if err := r.done(); err != nil {670 if err := r.done(); err != nil {
673- return RouteNavigationFailure{}, err671+ return protocol.RouteNavigationFailure{}, err
674 }672 }
675 if err := validateRouteNavigationFailure(failure); err != nil {673 if err := validateRouteNavigationFailure(failure); err != nil {
676- return RouteNavigationFailure{}, err674+ return protocol.RouteNavigationFailure{}, err
677 }675 }
678 return failure, nil676 return failure, nil
679 }677 }
M internal/ports/same_peer_switch_test.go +17 -8
@@ -5,19 +5,20 @@ import (
5 "testing"5 "testing"
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8+ "github.com/bnema/vev/internal/protocol"
8 )9 )
9 10
10-func samePeerSwitchTarget() ExactSessionTarget {11+func samePeerSwitchTarget() protocol.ExactSessionTarget {
11 var lifecycle domain.SessionLifecycleID12 var lifecycle domain.SessionLifecycleID
12 lifecycle[0] = 913 lifecycle[0] = 9
13- return ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}14+ return protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
14 }15 }
15 16
16 func TestSamePeerSwitchWireStrict(t *testing.T) {17 func TestSamePeerSwitchWireStrict(t *testing.T) {
17- request := SamePeerSwitchRequest{RequestID: 7, Target: samePeerSwitchTarget(), PreferredTabID: "tab-2"}18+ request := protocol.SamePeerSwitchRequest{RequestID: 7, Target: samePeerSwitchTarget(), PreferredTabID: "tab-2"}
18 requestWire := append([]byte{0, 0, 0, 0, 0, 0, 0, 7, 9}, make([]byte, 15)...)19 requestWire := append([]byte{0, 0, 0, 0, 0, 0, 0, 7, 9}, make([]byte, 15)...)
19 requestWire = append(requestWire, 0, 4, 'w', 'o', 'r', 'k', 0, 5, 't', 'a', 'b', '-', '2')20 requestWire = append(requestWire, 0, 4, 'w', 'o', 'r', 'k', 0, 5, 't', 'a', 'b', '-', '2')
20- failure := SamePeerSwitchFailure{RequestID: 7, Code: SamePeerSwitchStaleTarget}21+ failure := protocol.SamePeerSwitchFailure{RequestID: 7, Code: protocol.SamePeerSwitchStaleTarget}
21 22
22 for _, tt := range []struct {23 for _, tt := range []struct {
23 name string24 name string
@@ -33,7 +34,11 @@ func TestSamePeerSwitchWireStrict(t *testing.T) {
33 decode: func(payload []byte) (any, error) { return UnmarshalSamePeerSwitchRequest(payload) },34 decode: func(payload []byte) (any, error) { return UnmarshalSamePeerSwitchRequest(payload) },
34 assert: func(t *testing.T, decoded any) {35 assert: func(t *testing.T, decoded any) {
35 t.Helper()36 t.Helper()
36- if got := decoded.(SamePeerSwitchRequest); got != request {37+ got, ok := decoded.(protocol.SamePeerSwitchRequest)
38+ if !ok {
39+ t.Fatalf("decoded type = %T, want protocol.SamePeerSwitchRequest", decoded)
40+ }
41+ if got != request {
37 t.Fatalf("request = %+v, want %+v", got, request)42 t.Fatalf("request = %+v, want %+v", got, request)
38 }43 }
39 },44 },
@@ -45,7 +50,11 @@ func TestSamePeerSwitchWireStrict(t *testing.T) {
45 decode: func(payload []byte) (any, error) { return UnmarshalSamePeerSwitchFailure(payload) },50 decode: func(payload []byte) (any, error) { return UnmarshalSamePeerSwitchFailure(payload) },
46 assert: func(t *testing.T, decoded any) {51 assert: func(t *testing.T, decoded any) {
47 t.Helper()52 t.Helper()
48- if got := decoded.(SamePeerSwitchFailure); got != failure {53+ got, ok := decoded.(protocol.SamePeerSwitchFailure)
54+ if !ok {
55+ t.Fatalf("decoded type = %T, want protocol.SamePeerSwitchFailure", decoded)
56+ }
57+ if got != failure {
49 t.Fatalf("failure = %+v, want %+v", got, failure)58 t.Fatalf("failure = %+v, want %+v", got, failure)
50 }59 }
51 },60 },
@@ -71,8 +80,8 @@ func TestSamePeerSwitchWireStrict(t *testing.T) {
71 }80 }
72 81
73 func TestSamePeerSwitchWireRejectsInvalidValues(t *testing.T) {82 func TestSamePeerSwitchWireRejectsInvalidValues(t *testing.T) {
74- valid := SamePeerSwitchRequest{RequestID: 1, Target: samePeerSwitchTarget()}83+ valid := protocol.SamePeerSwitchRequest{RequestID: 1, Target: samePeerSwitchTarget()}
75- for _, request := range []SamePeerSwitchRequest{84+ for _, request := range []protocol.SamePeerSwitchRequest{
76 {},85 {},
77 {RequestID: 1},86 {RequestID: 1},
78 {RequestID: 1, Target: samePeerSwitchTarget(), PreferredTabID: "bad tab"},87 {RequestID: 1, Target: samePeerSwitchTarget(), PreferredTabID: "bad tab"},
A internal/ports/session_control.go +323 -0
@@ -0,0 +1,323 @@
1+package ports
2+
3+import (
4+ "errors"
5+ "fmt"
6+ "math"
7+ "time"
8+
9+ "github.com/bnema/vev/internal/domain"
10+ "github.com/bnema/vev/internal/protocol"
11+)
12+
13+// HandshakeTimeout bounds every transport handshake from connect through the
14+// first committed publication.
15+const HandshakeTimeout = 15 * time.Second
16+
17+// ProtocolVersion is the current vev IPC wire protocol version.
18+const ProtocolVersion uint16 = 37
19+
20+// ConnectionCapabilities describes transport behavior relevant to session
21+// flow without naming a concrete carriage.
22+type ConnectionCapabilities struct {
23+ OutputDataLimit int
24+ PreferredOutputWindow uint8
25+ AsyncSend bool
26+ OwnedSynchronousSend bool
27+ LinkState bool
28+}
29+
30+var (
31+ ErrInvalidHello = errors.New("ports: invalid hello")
32+ ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
33+ ErrInvalidEnvironmentPolicy = errors.New("ports: invalid environment policy")
34+ ErrInvalidClientNotice = errors.New("ports: invalid client notice")
35+ ErrInvalidDetached = errors.New("ports: invalid detached reason")
36+)
37+
38+const (
39+ IntentEphemeral uint8 = 0
40+ IntentNew uint8 = 1
41+ IntentAttach uint8 = 2
42+ IntentResume uint8 = 3
43+)
44+
45+type EnvironmentPolicy uint8
46+
47+const (
48+ EnvironmentPolicyClientOwned EnvironmentPolicy = iota
49+ EnvironmentPolicyDaemonOwned
50+)
51+
52+const (
53+ CapabilityResume uint32 = 1 << 0
54+ CapabilityUDP uint32 = 1 << 1
55+ CapabilityPredict uint32 = 1 << 2
56+)
57+
58+const (
59+ ErrVersionMismatch uint16 = 1
60+ ErrNoSuchSession uint16 = 2
61+ ErrNameTaken uint16 = 3
62+ ErrServerShutdown uint16 = 4
63+ ErrInvalidSessionName uint16 = 5
64+ ErrUnknownCommand uint16 = 6
65+ ErrNotScriptable uint16 = 7
66+ ErrInvalidCommandArgs uint16 = 8
67+ ErrNoSuchTarget uint16 = 9
68+ ErrAmbiguousTarget uint16 = 10
69+ ErrInternal uint16 = 255
70+)
71+
72+const (
73+ ReasonDetach uint8 = 0
74+ ReasonSessionKilled uint8 = 1
75+ ReasonServerShutdown uint8 = 2
76+ ReasonReplaced uint8 = 3
77+)
78+
79+// Hello is sent by the client immediately after connecting.
80+type Hello struct {
81+ Version uint16
82+ Intent uint8
83+ ClientID [16]byte
84+ ResumeToken uint64
85+ Name string
86+ Size domain.Size
87+ PixelWidth int
88+ PixelHeight int
89+ TermEnv string
90+ Cwd string
91+ TrueColor bool
92+ // KittyDirectGraphics is an explicit declaration that the client has
93+ // probed its direct outer terminal and it accepts Kitty graphics output.
94+ KittyDirectGraphics bool
95+ MaxOutputInFlight uint8
96+ Env []string
97+ RemoteTarget *domain.RemoteSessionTarget
98+ ExactTarget *protocol.ExactSessionTarget
99+ PreferredTabID domain.TabStableID
100+ EnvironmentPolicy EnvironmentPolicy
101+ NavigationCapabilities protocol.NavigationCapabilities
102+ StartupOverlay protocol.StartupOverlay
103+ Remote bool
104+}
105+
106+func (h Hello) Geometry() domain.Geometry {
107+ return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
108+}
109+
110+type Input struct {
111+ InputSeq uint64
112+ Data []byte
113+}
114+
115+type ClientNotice struct{ Action uint8 }
116+
117+const (
118+ ClientNoticeClipboardFallback uint8 = 1
119+ ClientNoticeClipboardTooLarge uint8 = 2
120+ ClientNoticeLinkDegraded uint8 = 3
121+ ClientNoticeLinkConnected uint8 = 4
122+)
123+
124+type Detach struct{}
125+type Ping struct{}
126+type Pong struct{}
127+
128+type Welcome struct {
129+ SessionID string
130+ SessionName string
131+ Ephemeral bool
132+ ResumeToken uint64
133+ Capabilities uint32
134+ CommittedIdentity *protocol.CommittedRouteIdentity
135+}
136+
137+type ErrorMsg struct {
138+ Code uint16
139+ Text string
140+}
141+
142+type AttachTarget struct {
143+ Endpoint string
144+ Session string
145+ Intent uint8
146+ RemoteTarget *domain.RemoteSessionTarget
147+ ExactTarget *protocol.ExactSessionTarget
148+ EnvironmentPolicy EnvironmentPolicy
149+ SamePeer bool
150+}
151+
152+type Detached struct{ Reason uint8 }
153+type List struct{}
154+
155+type Kill struct {
156+ Name string
157+ All bool
158+}
159+
160+type SessionState uint8
161+
162+const (
163+ SessionUp SessionState = iota
164+ SessionDown
165+ SessionBroken
166+)
167+
168+type SessionInfo struct {
169+ SessionID string
170+ Name string
171+ State SessionState
172+ Ephemeral bool
173+ Tabs uint16
174+ Attached bool
175+}
176+
177+type Sessions struct{ Sessions []SessionInfo }
178+type OutputResetRequest struct{}
179+
180+func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
181+ return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
182+}
183+
184+func validClientNoticeAction(action uint8) bool {
185+ switch action {
186+ case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
187+ return true
188+ default:
189+ return false
190+ }
191+}
192+
193+func validDetachedReason(reason uint8) bool {
194+ switch reason {
195+ case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
196+ return true
197+ default:
198+ return false
199+ }
200+}
201+
202+func ValidateEnvironmentPolicy(policy EnvironmentPolicy) error {
203+ if !validEnvironmentPolicy(policy) {
204+ return ErrInvalidEnvironmentPolicy
205+ }
206+ return nil
207+}
208+
209+func ValidateClientNotice(m ClientNotice) error {
210+ if !validClientNoticeAction(m.Action) {
211+ return ErrInvalidClientNotice
212+ }
213+ return nil
214+}
215+
216+func ValidateDetached(m Detached) error {
217+ if !validDetachedReason(m.Reason) {
218+ return ErrInvalidDetached
219+ }
220+ return nil
221+}
222+
223+func validateHelloNavigation(h Hello) error {
224+ if h.Intent != IntentAttach && h.Intent != IntentResume {
225+ if h.NavigationCapabilities != 0 || h.StartupOverlay != protocol.StartupOverlayNone {
226+ return protocol.ErrInvalidNavigation
227+ }
228+ return nil
229+ }
230+ return protocol.ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
231+}
232+
233+func ValidateHello(h Hello) error {
234+ if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
235+ return ErrInvalidHello
236+ }
237+ if err := protocol.ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
238+ return fmt.Errorf("%w: geometry", ErrInvalidHello)
239+ }
240+ if !validEnvironmentPolicy(h.EnvironmentPolicy) {
241+ return ErrInvalidHello
242+ }
243+ if err := validateHelloNavigation(h); err != nil {
244+ return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
245+ }
246+ if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
247+ return ErrInvalidHello
248+ }
249+ for _, entry := range h.Env {
250+ if uint64(len(entry)) > math.MaxUint32 {
251+ return ErrInvalidHello
252+ }
253+ }
254+ if h.PreferredTabID != "" {
255+ if h.Intent != IntentAttach && h.Intent != IntentResume {
256+ return ErrInvalidHello
257+ }
258+ if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
259+ return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
260+ }
261+ }
262+ if h.ExactTarget != nil {
263+ if h.Intent != IntentAttach && h.Intent != IntentResume {
264+ return ErrInvalidHello
265+ }
266+ if err := h.ExactTarget.Validate(); err != nil {
267+ return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
268+ }
269+ if h.Name != h.ExactTarget.SessionName {
270+ return ErrInvalidHello
271+ }
272+ if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
273+ return ErrInvalidHello
274+ }
275+ }
276+ if h.RemoteTarget == nil {
277+ return nil
278+ }
279+ if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || (h.Intent != IntentAttach && h.Intent != IntentResume) {
280+ return ErrInvalidHello
281+ }
282+ if err := validateWireRemoteTarget(*h.RemoteTarget); err != nil {
283+ return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
284+ }
285+ if h.Name != h.RemoteTarget.SessionName {
286+ return ErrInvalidHello
287+ }
288+ return nil
289+}
290+
291+func ValidateAttachTarget(m AttachTarget) error {
292+ if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
293+ return ErrInvalidAttachTarget
294+ }
295+ if err := domain.ValidateSessionName(m.Session); err != nil {
296+ return ErrInvalidAttachTarget
297+ }
298+ if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
299+ return ErrInvalidAttachTarget
300+ }
301+ if m.Intent == IntentResume || !validEnvironmentPolicy(m.EnvironmentPolicy) {
302+ return ErrInvalidAttachTarget
303+ }
304+ if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
305+ return ErrInvalidAttachTarget
306+ }
307+ if m.SamePeer && (m.Endpoint != "" || m.RemoteTarget != nil || m.ExactTarget == nil) {
308+ return ErrInvalidAttachTarget
309+ }
310+ if m.RemoteTarget == nil {
311+ return nil
312+ }
313+ if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || m.Intent != IntentAttach {
314+ return ErrInvalidAttachTarget
315+ }
316+ if err := validateWireRemoteTarget(*m.RemoteTarget); err != nil {
317+ return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
318+ }
319+ if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
320+ return ErrInvalidAttachTarget
321+ }
322+ return nil
323+}
M internal/ports/wire.go +125 -601
@@ -18,6 +18,7 @@ import (
18 18
19 renderer "github.com/bnema/vev-vt"19 renderer "github.com/bnema/vev-vt"
20 "github.com/bnema/vev/internal/domain"20 "github.com/bnema/vev/internal/domain"
21+ "github.com/bnema/vev/internal/protocol"
21 )22 )
22 23
23 // errShortPayload is returned when a payload ends before a required field24 // errShortPayload is returned when a payload ends before a required field
@@ -53,338 +54,6 @@ var outputCompressorPool = sync.Pool{New: func() any {
53 return writer54 return writer
54 }}55 }}
55 56
56-// ErrInvalidSize reports a terminal size that cannot be represented by the
57-// protocol or would exceed the bounded screen area.
58-var ErrInvalidSize = errors.New("ports: invalid terminal size")
59-
60-// ErrInvalidHello reports a malformed Hello semantic value.
61-var ErrInvalidHello = errors.New("ports: invalid hello")
62-
63-// ErrInvalidOutput reports a malformed Output semantic value.
64-var ErrInvalidOutput = errors.New("ports: invalid output")
65-
66-// ErrInvalidAck reports a malformed Ack semantic value.
67-var ErrInvalidAck = errors.New("ports: invalid ack")
68-
69-// ErrInvalidAttachTarget reports a malformed attach target.
70-var ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
71-
72-// ErrInvalidNavigation reports an unknown or impossible navigation value.
73-var ErrInvalidNavigation = errors.New("ports: invalid navigation")
74-
75-// Intent values carried by Hello, describing what the client wants to do.
76-const (
77- IntentEphemeral uint8 = 0
78- IntentNew uint8 = 1
79- IntentAttach uint8 = 2
80- IntentResume uint8 = 3
81-)
82-
83-// EnvironmentPolicy controls which side owns the environment used for future
84-// PTY children. Direct CLI attaches keep the historical client-owned policy;
85-// picker handoffs use daemon ownership so a local environment cannot leak into
86-// a remote daemon.
87-type EnvironmentPolicy uint8
88-
89-const (
90- EnvironmentPolicyClientOwned EnvironmentPolicy = iota
91- EnvironmentPolicyDaemonOwned
92-)
93-
94-func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
95- return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
96-}
97-
98-func validNavigationCapabilities(capabilities NavigationCapabilities) bool {
99- return capabilities&^(NavigationCapabilityHomePicker|NavigationCapabilityBack) == 0
100-}
101-
102-func validStartupOverlay(overlay StartupOverlay) bool {
103- return overlay == StartupOverlayNone || overlay == StartupOverlaySessionPicker
104-}
105-
106-// ValidateNavigation validates route capabilities independently of the rest of
107-// the Hello payload. homePickerRoute indicates that the route may open its
108-// home/session picker (for example, a remote target or daemon-owned route).
109-func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOverlay, homePickerRoute bool) error {
110- if !validNavigationCapabilities(capabilities) || !validStartupOverlay(overlay) {
111- return ErrInvalidNavigation
112- }
113- home := capabilities&NavigationCapabilityHomePicker != 0
114- back := capabilities&NavigationCapabilityBack != 0
115- if (home && !homePickerRoute) || back != (overlay == StartupOverlaySessionPicker) {
116- return ErrInvalidNavigation
117- }
118- if homePickerRoute && back {
119- return ErrInvalidNavigation
120- }
121- return nil
122-}
123-
124-func validateHelloNavigation(h Hello) error {
125- if h.Intent != IntentAttach && h.Intent != IntentResume {
126- if h.NavigationCapabilities != 0 || h.StartupOverlay != StartupOverlayNone {
127- return ErrInvalidNavigation
128- }
129- return nil
130- }
131- return ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
132-}
133-
134-// Capability bits advertised in Welcome.
135-const (
136- CapabilityResume uint32 = 1 << 0
137- CapabilityUDP uint32 = 1 << 1
138- CapabilityPredict uint32 = 1 << 2
139-)
140-
141-// ErrorMsg codes.
142-const (
143- ErrVersionMismatch uint16 = 1
144- ErrNoSuchSession uint16 = 2
145- ErrNameTaken uint16 = 3
146- ErrServerShutdown uint16 = 4
147- ErrInvalidSessionName uint16 = 5
148- ErrUnknownCommand uint16 = 6
149- ErrNotScriptable uint16 = 7
150- ErrInvalidCommandArgs uint16 = 8
151- ErrNoSuchTarget uint16 = 9
152- ErrAmbiguousTarget uint16 = 10
153- ErrInternal uint16 = 255
154-)
155-
156-// Detached reasons.
157-const (
158- ReasonDetach uint8 = 0
159- ReasonSessionKilled uint8 = 1
160- ReasonServerShutdown uint8 = 2
161- ReasonReplaced uint8 = 3
162-)
163-
164-// Hello is sent by the client immediately after connecting.
165-type Hello struct {
166- Version uint16
167- Intent uint8
168- ClientID [16]byte
169- ResumeToken uint64
170- Name string
171- Size domain.Size
172- PixelWidth int
173- PixelHeight int
174- TermEnv string
175- Cwd string
176- TrueColor bool
177- // KittyDirectGraphics is an explicit declaration that the client has
178- // probed its direct outer terminal and it accepts Kitty graphics output.
179- // Environment values are never sufficient to set this capability.
180- KittyDirectGraphics bool
181- // MaxOutputInFlight is the requested maximum number of unacknowledged
182- // state-bearing output frames.
183- MaxOutputInFlight uint8
184- // Env is the complete exported client environment for future PTY children.
185- // Entries are opaque strings so their ordering and contents are preserved.
186- Env []string
187- // RemoteTarget is present only for a picker-selected remote attach. It is
188- // carried through reconnects and is validated by the owning daemon before
189- // it creates or publishes any session resources.
190- RemoteTarget *domain.RemoteSessionTarget
191- // ExactTarget is an optional lifecycle/name identity selected by the client
192- // ledger. It is transport-neutral and never replaces the daemon's authority
193- // to validate or commit the target.
194- ExactTarget *ExactSessionTarget
195- // PreferredTabID is a client-owned route cursor. The daemon treats it as an
196- // attachment-local hint and falls back when the stable tab no longer exists.
197- PreferredTabID domain.TabStableID
198- EnvironmentPolicy EnvironmentPolicy
199- NavigationCapabilities NavigationCapabilities
200- StartupOverlay StartupOverlay
201- // Remote identifies a direct remote carriage even when no exact picker
202- // target is present. It is daemon-facing so remote rendering backends can
203- // be disabled consistently for both direct and picker attaches.
204- Remote bool
205-}
206-
207-// Input carries raw bytes typed/pasted by the client, destined for the PTY.
208-type Input struct {
209- InputSeq uint64
210- Data []byte
211-}
212-
213-// Geometry returns the complete controlling-terminal geometry carried by the
214-// handshake. Pixel dimensions are optional as a pair.
215-func (h Hello) Geometry() domain.Geometry {
216- return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
217-}
218-
219-// Resize notifies the daemon of a client-side terminal geometry change.
220-type Resize struct {
221- Size domain.Size
222- PixelWidth int
223- PixelHeight int
224-}
225-
226-// Geometry returns the complete controlling-terminal geometry carried by the
227-// resize message. Pixel dimensions are optional as a pair.
228-func (m Resize) Geometry() domain.Geometry {
229- return domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}.NormalizePixels()
230-}
231-
232-// Theme reports the client's terminal foreground/background colors, ANSI
233-// palette entries, and whether the client terminal supports truecolor.
234-type Theme struct {
235- HasForeground bool
236- Foreground renderer.RGB
237- HasBackground bool
238- Background renderer.RGB
239- TrueColor bool
240- SchemeKnown bool
241- Light bool
242- PaletteKnown uint16
243- Palette [16]renderer.RGB
244-}
245-
246-// ImagePush carries a clipboard image from a remote client, to be written to
247-// a temp file and injected into the focused pane's PTY as a path (see
248-// docs/superpowers/specs/2026-07-04-clipboard-image-transfer-design.md).
249-type ImagePush struct {
250- InputSeq uint64
251- Mime string
252- Data []byte
253-}
254-
255-// ClientNotice reports a fixed client-side event for daemon-rendered user
256-// feedback. Action values are deliberately closed so a client cannot inject
257-// arbitrary display text into a shared session.
258-type ClientNotice struct {
259- Action uint8
260-}
261-
262-const (
263- ClientNoticeClipboardFallback uint8 = 1
264- ClientNoticeClipboardTooLarge uint8 = 2
265- ClientNoticeLinkDegraded uint8 = 3
266- ClientNoticeLinkConnected uint8 = 4
267-)
268-
269-func validClientNoticeAction(action uint8) bool {
270- switch action {
271- case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
272- return true
273- default:
274- return false
275- }
276-}
277-
278-func validDetachedReason(reason uint8) bool {
279- switch reason {
280- case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
281- return true
282- default:
283- return false
284- }
285-}
286-
287-// Detach asks the daemon to detach the current client without killing the
288-// session.
289-type Detach struct{}
290-
291-// Ping is a keepalive/liveness probe sent by either side.
292-type Ping struct{}
293-
294-// Pong answers a Ping.
295-type Pong struct{}
296-
297-// Ack acknowledges receipt/application of an output state in one output epoch.
298-type Ack struct {
299- Epoch uint64
300- State uint64
301-}
302-
303-// Welcome is the daemon's reply to a successful Hello.
304-type Welcome struct {
305- SessionID string
306- SessionName string
307- Ephemeral bool
308- ResumeToken uint64
309- Capabilities uint32
310- CommittedIdentity *CommittedRouteIdentity
311-}
312-
313-// ErrorMsg reports a protocol- or session-level failure to the client.
314-type ErrorMsg struct {
315- Code uint16
316- Text string
317-}
318-
319-// Output carries one ordered output publication for an attachment.
320-type Output struct {
321- Epoch uint64
322- Base uint64
323- New uint64
324- Echo uint64
325- ViewRevision uint64
326- Size domain.Size
327- Full bool
328- Data []byte
329-}
330-
331-// AttachTarget identifies the endpoint and exact session/tab selected for a
332-// new attachment. Endpoint and Session remain the compatibility route fields;
333-// RemoteTarget is the authoritative picker identity when non-nil.
334-type AttachTarget struct {
335- Endpoint string
336- Session string
337- Intent uint8
338- RemoteTarget *domain.RemoteSessionTarget
339- ExactTarget *ExactSessionTarget
340- EnvironmentPolicy EnvironmentPolicy
341- SamePeer bool
342-}
343-
344-// Detached tells a client it has been disconnected from its session and why.
345-type Detached struct {
346- Reason uint8
347-}
348-
349-// List asks the daemon to enumerate its live sessions. It carries no
350-// fields; the request is fully described by its message type.
351-type List struct{}
352-
353-// Kill asks the daemon to terminate one named session or all sessions.
354-type Kill struct {
355- Name string
356- All bool
357-}
358-
359-// SessionState is the client-visible lifecycle state of a session.
360-type SessionState uint8
361-
362-const (
363- SessionUp SessionState = iota
364- SessionDown
365- SessionBroken
366-)
367-
368-// SessionInfo describes one session in a Sessions listing.
369-type SessionInfo struct {
370- SessionID string
371- Name string
372- State SessionState
373- Ephemeral bool
374- Tabs uint16
375- Attached bool
376-}
377-
378-// Sessions is the daemon's reply to a List, enumerating live sessions.
379-type Sessions struct {
380- Sessions []SessionInfo
381-}
382-
383-// OutputResetRequest asks a remote output stream to rebase.
384-type OutputResetRequest struct{}
385-
386-// SessionTabMeta describes one tab in a remote session's metadata snapshot.
387-// ID is stable for the lifetime of its remote tab and ordered only for display.
388 // payloadWriter builds a message payload by appending fields in wire order.57 // payloadWriter builds a message payload by appending fields in wire order.
389 type payloadWriter struct {58 type payloadWriter struct {
390 b []byte59 b []byte
@@ -521,7 +190,7 @@ func skipRemoteTargetSection(r *payloadReader) error {
521 if err != nil {190 if err != nil {
522 return err191 return err
523 }192 }
524- if !validEnvironmentPolicy(EnvironmentPolicy(policy)) {193+ if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {
525 return errInvalidEnum194 return errInvalidEnum
526 }195 }
527 return nil196 return nil
@@ -596,7 +265,7 @@ func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget
596 if err != nil {265 if err != nil {
597 return nil, 0, err266 return nil, 0, err
598 }267 }
599- if !validEnvironmentPolicy(EnvironmentPolicy(policy)) {268+ if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {
600 return nil, 0, errInvalidEnum269 return nil, 0, errInvalidEnum
601 }270 }
602 return target, EnvironmentPolicy(policy), nil271 return target, EnvironmentPolicy(policy), nil
@@ -753,141 +422,14 @@ func PeekHelloVersion(b []byte) (uint16, bool) {
753 422
754 // ValidateHello validates Hello semantics without retaining or allocating any423 // ValidateHello validates Hello semantics without retaining or allocating any
755 // payload data.424 // payload data.
756-func ValidateHello(h Hello) error {
757- if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
758- return ErrInvalidHello
759- }
760- if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
761- return fmt.Errorf("%w: geometry", ErrInvalidHello)
762- }
763- if !validEnvironmentPolicy(h.EnvironmentPolicy) {
764- return ErrInvalidHello
765- }
766- if err := validateHelloNavigation(h); err != nil {
767- return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
768- }
769- if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
770- return ErrInvalidHello
771- }
772- for _, entry := range h.Env {
773- if uint64(len(entry)) > math.MaxUint32 {
774- return ErrInvalidHello
775- }
776- }
777- if h.PreferredTabID != "" {
778- if h.Intent != IntentAttach && h.Intent != IntentResume {
779- return ErrInvalidHello
780- }
781- if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
782- return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
783- }
784- }
785- if h.ExactTarget != nil {
786- if h.Intent != IntentAttach && h.Intent != IntentResume {
787- return ErrInvalidHello
788- }
789- if err := h.ExactTarget.Validate(); err != nil {
790- return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
791- }
792- if h.Name != h.ExactTarget.SessionName {
793- return ErrInvalidHello
794- }
795- if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
796- return ErrInvalidHello
797- }
798- }
799- if h.RemoteTarget == nil {
800- if h.EnvironmentPolicy != EnvironmentPolicyClientOwned && h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
801- return ErrInvalidHello
802- }
803- return nil
804- }
805- if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
806- return ErrInvalidHello
807- }
808- if h.Intent != IntentAttach && h.Intent != IntentResume {
809- return ErrInvalidHello
810- }
811- if err := validateWireRemoteTarget(*h.RemoteTarget); err != nil {
812- return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
813- }
814- if h.Name != h.RemoteTarget.SessionName {
815- return ErrInvalidHello
816- }
817- return nil
818-}
819 425
820 // ValidateOutput validates the final output state before data allocation.426 // ValidateOutput validates the final output state before data allocation.
821-func ValidateOutput(m Output) error {
822- if m.Epoch == 0 {
823- return ErrInvalidOutput
824- }
825- if m.New == 0 {
826- if m.Base != 0 || m.Full {
827- return ErrInvalidOutput
828- }
829- } else if (m.Base == 0 && !m.Full) || (m.Base != 0 && (m.Full || m.New != m.Base+1)) {
830- return ErrInvalidOutput
831- }
832- if err := ValidateSize(m.Size); err != nil {
833- return fmt.Errorf("%w: size", ErrInvalidOutput)
834- }
835- if uint64(len(m.Data)) > math.MaxUint32 || len(m.Data) > MaxOutputDataLen {
836- return ErrInvalidOutput
837- }
838- return nil
839-}
840 427
841 // ValidateAck validates an epoch-scoped output acknowledgement.428 // ValidateAck validates an epoch-scoped output acknowledgement.
842-func ValidateAck(m Ack) error {
843- if m.Epoch == 0 {
844- return ErrInvalidAck
845- }
846- return nil
847-}
848 429
849 // ValidateAttachTarget validates a client-owned route handoff. An empty430 // ValidateAttachTarget validates a client-owned route handoff. An empty
850 // Endpoint selects another session on the currently connected daemon; a431 // Endpoint selects another session on the currently connected daemon; a
851 // non-empty Endpoint selects a discovered remote daemon.432 // non-empty Endpoint selects a discovered remote daemon.
852-func ValidateAttachTarget(m AttachTarget) error {
853- if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
854- return ErrInvalidAttachTarget
855- }
856- if err := domain.ValidateSessionName(m.Session); err != nil {
857- return ErrInvalidAttachTarget
858- }
859- if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
860- return ErrInvalidAttachTarget
861- }
862- if m.Intent == IntentResume {
863- return ErrInvalidAttachTarget
864- }
865- if !validEnvironmentPolicy(m.EnvironmentPolicy) {
866- return ErrInvalidAttachTarget
867- }
868- if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
869- return ErrInvalidAttachTarget
870- }
871- if m.SamePeer && (m.Endpoint != "" || m.RemoteTarget != nil || m.ExactTarget == nil) {
872- return ErrInvalidAttachTarget
873- }
874- if m.RemoteTarget == nil {
875- return nil
876- }
877- if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {
878- return ErrInvalidAttachTarget
879- }
880- if m.Intent != IntentAttach {
881- return ErrInvalidAttachTarget
882- }
883- if err := validateWireRemoteTarget(*m.RemoteTarget); err != nil {
884- return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
885- }
886- if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
887- return ErrInvalidAttachTarget
888- }
889- return nil
890-}
891 433
892 func validateWireRemoteTarget(target domain.RemoteSessionTarget) error {434 func validateWireRemoteTarget(target domain.RemoteSessionTarget) error {
893 if len(target.Endpoint) > math.MaxUint16 || len(target.DisplayOrigin) > math.MaxUint16 || len(target.SessionName) > math.MaxUint16 || len(target.LiveTabID) > math.MaxUint16 || len(target.StoppedTab.RawName) > math.MaxUint16 {435 if len(target.Endpoint) > math.MaxUint16 || len(target.DisplayOrigin) > math.MaxUint16 || len(target.SessionName) > math.MaxUint16 || len(target.LiveTabID) > math.MaxUint16 || len(target.StoppedTab.RawName) > math.MaxUint16 {
@@ -1105,12 +647,12 @@ func UnmarshalHello(b []byte) (Hello, error) {
1105 if err != nil {647 if err != nil {
1106 return Hello{}, err648 return Hello{}, err
1107 }649 }
1108- h.NavigationCapabilities = NavigationCapabilities(capabilities)650+ h.NavigationCapabilities = protocol.NavigationCapabilities(capabilities)
1109 overlay, err := r.getUint8()651 overlay, err := r.getUint8()
1110 if err != nil {652 if err != nil {
1111 return Hello{}, err653 return Hello{}, err
1112 }654 }
1113- h.StartupOverlay = StartupOverlay(overlay)655+ h.StartupOverlay = protocol.StartupOverlay(overlay)
1114 if h.Remote, err = r.getBool(); err != nil {656 if h.Remote, err = r.getBool(); err != nil {
1115 return Hello{}, err657 return Hello{}, err
1116 }658 }
@@ -1146,7 +688,7 @@ func UnmarshalInput(b []byte) (Input, error) {
1146 }688 }
1147 689
1148 // MarshalImagePush encodes m into an ImagePush message payload.690 // MarshalImagePush encodes m into an ImagePush message payload.
1149-func MarshalImagePush(m ImagePush) []byte {691+func MarshalImagePush(m protocol.ImagePush) []byte {
1150 w := payloadWriter{}692 w := payloadWriter{}
1151 w.putUint64(m.InputSeq)693 w.putUint64(m.InputSeq)
1152 w.putString(m.Mime)694 w.putString(m.Mime)
@@ -1157,17 +699,17 @@ func MarshalImagePush(m ImagePush) []byte {
1157 // UnmarshalImagePush decodes an ImagePush message payload. After the fixed699 // UnmarshalImagePush decodes an ImagePush message payload. After the fixed
1158 // input sequence and length-prefixed mime string, the rest of the payload is700 // input sequence and length-prefixed mime string, the rest of the payload is
1159 // data; there is no length prefix for it.701 // data; there is no length prefix for it.
1160-func UnmarshalImagePush(b []byte) (ImagePush, error) {702+func UnmarshalImagePush(b []byte) (protocol.ImagePush, error) {
1161 r := payloadReader{b: b}703 r := payloadReader{b: b}
1162 seq, err := r.getUint64()704 seq, err := r.getUint64()
1163 if err != nil {705 if err != nil {
1164- return ImagePush{}, err706+ return protocol.ImagePush{}, err
1165 }707 }
1166 mime, err := r.getString()708 mime, err := r.getString()
1167 if err != nil {709 if err != nil {
1168- return ImagePush{}, err710+ return protocol.ImagePush{}, err
1169 }711 }
1170- return ImagePush{InputSeq: seq, Mime: mime, Data: r.rest()}, nil712+ return protocol.ImagePush{InputSeq: seq, Mime: mime, Data: r.rest()}, nil
1171 }713 }
1172 714
1173 // MarshalClientNotice encodes a fixed one-byte client notice action.715 // MarshalClientNotice encodes a fixed one-byte client notice action.
@@ -1183,7 +725,7 @@ func UnmarshalClientNotice(b []byte) (ClientNotice, error) {
1183 if err != nil {725 if err != nil {
1184 return ClientNotice{}, err726 return ClientNotice{}, err
1185 }727 }
1186- if !validClientNoticeAction(action) {728+ if ValidateClientNotice(ClientNotice{Action: action}) != nil {
1187 return ClientNotice{}, errors.New("ports: unknown client notice action")729 return ClientNotice{}, errors.New("ports: unknown client notice action")
1188 }730 }
1189 if err := r.done(); err != nil {731 if err := r.done(); err != nil {
@@ -1193,8 +735,8 @@ func UnmarshalClientNotice(b []byte) (ClientNotice, error) {
1193 }735 }
1194 736
1195 // MarshalResize encodes m into a Resize message payload.737 // MarshalResize encodes m into a Resize message payload.
1196-func MarshalResize(m Resize) ([]byte, error) {738+func MarshalResize(m protocol.Resize) ([]byte, error) {
1197- if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {739+ if err := protocol.ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {
1198 return nil, err740 return nil, err
1199 }741 }
1200 w := payloadWriter{}742 w := payloadWriter{}
@@ -1206,7 +748,7 @@ func MarshalResize(m Resize) ([]byte, error) {
1206 }748 }
1207 749
1208 // MarshalTheme encodes m into a 57-byte fixed-width Theme message payload.750 // MarshalTheme encodes m into a 57-byte fixed-width Theme message payload.
1209-func MarshalTheme(m Theme) []byte {751+func MarshalTheme(m protocol.Theme) []byte {
1210 var flags uint8752 var flags uint8
1211 if m.HasForeground {753 if m.HasForeground {
1212 flags |= 0x01754 flags |= 0x01
@@ -1242,45 +784,45 @@ func MarshalTheme(m Theme) []byte {
1242 }784 }
1243 785
1244 // UnmarshalTheme decodes a 57-byte fixed-width Theme message payload.786 // UnmarshalTheme decodes a 57-byte fixed-width Theme message payload.
1245-func UnmarshalTheme(b []byte) (Theme, error) {787+func UnmarshalTheme(b []byte) (protocol.Theme, error) {
1246 r := payloadReader{b: b}788 r := payloadReader{b: b}
1247 flags, err := r.getUint8()789 flags, err := r.getUint8()
1248 if err != nil {790 if err != nil {
1249- return Theme{}, err791+ return protocol.Theme{}, err
1250 }792 }
1251 if flags&^uint8(0x1f) != 0 {793 if flags&^uint8(0x1f) != 0 {
1252- return Theme{}, errInvalidEnum794+ return protocol.Theme{}, errInvalidEnum
1253 }795 }
1254 fgR, err := r.getUint8()796 fgR, err := r.getUint8()
1255 if err != nil {797 if err != nil {
1256- return Theme{}, err798+ return protocol.Theme{}, err
1257 }799 }
1258 fgG, err := r.getUint8()800 fgG, err := r.getUint8()
1259 if err != nil {801 if err != nil {
1260- return Theme{}, err802+ return protocol.Theme{}, err
1261 }803 }
1262 fgB, err := r.getUint8()804 fgB, err := r.getUint8()
1263 if err != nil {805 if err != nil {
1264- return Theme{}, err806+ return protocol.Theme{}, err
1265 }807 }
1266 bgR, err := r.getUint8()808 bgR, err := r.getUint8()
1267 if err != nil {809 if err != nil {
1268- return Theme{}, err810+ return protocol.Theme{}, err
1269 }811 }
1270 bgG, err := r.getUint8()812 bgG, err := r.getUint8()
1271 if err != nil {813 if err != nil {
1272- return Theme{}, err814+ return protocol.Theme{}, err
1273 }815 }
1274 bgB, err := r.getUint8()816 bgB, err := r.getUint8()
1275 if err != nil {817 if err != nil {
1276- return Theme{}, err818+ return protocol.Theme{}, err
1277 }819 }
1278 paletteKnown, err := r.getUint16()820 paletteKnown, err := r.getUint16()
1279 if err != nil {821 if err != nil {
1280- return Theme{}, err822+ return protocol.Theme{}, err
1281 }823 }
1282 824
1283- m := Theme{825+ m := protocol.Theme{
1284 HasForeground: flags&0x01 != 0,826 HasForeground: flags&0x01 != 0,
1285 Foreground: renderer.RGB{R: fgR, G: fgG, B: fgB},827 Foreground: renderer.RGB{R: fgR, G: fgG, B: fgB},
1286 HasBackground: flags&0x02 != 0,828 HasBackground: flags&0x02 != 0,
@@ -1292,50 +834,50 @@ func UnmarshalTheme(b []byte) (Theme, error) {
1292 }834 }
1293 for i := range m.Palette {835 for i := range m.Palette {
1294 if m.Palette[i].R, err = r.getUint8(); err != nil {836 if m.Palette[i].R, err = r.getUint8(); err != nil {
1295- return Theme{}, err837+ return protocol.Theme{}, err
1296 }838 }
1297 if m.Palette[i].G, err = r.getUint8(); err != nil {839 if m.Palette[i].G, err = r.getUint8(); err != nil {
1298- return Theme{}, err840+ return protocol.Theme{}, err
1299 }841 }
1300 if m.Palette[i].B, err = r.getUint8(); err != nil {842 if m.Palette[i].B, err = r.getUint8(); err != nil {
1301- return Theme{}, err843+ return protocol.Theme{}, err
1302 }844 }
1303 }845 }
1304 if err := r.done(); err != nil {846 if err := r.done(); err != nil {
1305- return Theme{}, err847+ return protocol.Theme{}, err
1306 }848 }
1307 return m, nil849 return m, nil
1308 }850 }
1309 851
1310 // UnmarshalResize decodes a Resize message payload.852 // UnmarshalResize decodes a Resize message payload.
1311-func UnmarshalResize(b []byte) (Resize, error) {853+func UnmarshalResize(b []byte) (protocol.Resize, error) {
1312 r := payloadReader{b: b}854 r := payloadReader{b: b}
1313 cols, err := r.getUint16()855 cols, err := r.getUint16()
1314 if err != nil {856 if err != nil {
1315- return Resize{}, err857+ return protocol.Resize{}, err
1316 }858 }
1317 rows, err := r.getUint16()859 rows, err := r.getUint16()
1318 if err != nil {860 if err != nil {
1319- return Resize{}, err861+ return protocol.Resize{}, err
1320 }862 }
1321 pixelWidth, err := r.getUint16()863 pixelWidth, err := r.getUint16()
1322 if err != nil {864 if err != nil {
1323- return Resize{}, err865+ return protocol.Resize{}, err
1324 }866 }
1325 pixelHeight, err := r.getUint16()867 pixelHeight, err := r.getUint16()
1326 if err != nil {868 if err != nil {
1327- return Resize{}, err869+ return protocol.Resize{}, err
1328 }870 }
1329 if err := r.done(); err != nil {871 if err := r.done(); err != nil {
1330- return Resize{}, err872+ return protocol.Resize{}, err
1331 }873 }
1332- m := Resize{874+ m := protocol.Resize{
1333 Size: domain.Size{Cols: int(cols), Rows: int(rows)},875 Size: domain.Size{Cols: int(cols), Rows: int(rows)},
1334 PixelWidth: int(pixelWidth),876 PixelWidth: int(pixelWidth),
1335 PixelHeight: int(pixelHeight),877 PixelHeight: int(pixelHeight),
1336 }878 }
1337- if err := ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {879+ if err := protocol.ValidateGeometry(domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}); err != nil {
1338- return Resize{}, err880+ return protocol.Resize{}, err
1339 }881 }
1340 return m, nil882 return m, nil
1341 }883 }
@@ -1383,8 +925,8 @@ func UnmarshalPong(b []byte) (Pong, error) {
1383 }925 }
1384 926
1385 // MarshalAck encodes m into an epoch/state Ack payload.927 // MarshalAck encodes m into an epoch/state Ack payload.
1386-func MarshalAck(m Ack) ([]byte, error) {928+func MarshalAck(m protocol.Ack) ([]byte, error) {
1387- if err := ValidateAck(m); err != nil {929+ if err := protocol.ValidateAck(m); err != nil {
1388 return nil, err930 return nil, err
1389 }931 }
1390 w := payloadWriter{}932 w := payloadWriter{}
@@ -1394,21 +936,21 @@ func MarshalAck(m Ack) ([]byte, error) {
1394 }936 }
1395 937
1396 // UnmarshalAck decodes an epoch/state Ack payload.938 // UnmarshalAck decodes an epoch/state Ack payload.
1397-func UnmarshalAck(b []byte) (Ack, error) {939+func UnmarshalAck(b []byte) (protocol.Ack, error) {
1398 r := payloadReader{b: b}940 r := payloadReader{b: b}
1399- var m Ack941+ var m protocol.Ack
1400 var err error942 var err error
1401 if m.Epoch, err = r.getUint64(); err != nil {943 if m.Epoch, err = r.getUint64(); err != nil {
1402- return Ack{}, err944+ return protocol.Ack{}, err
1403 }945 }
1404 if m.State, err = r.getUint64(); err != nil {946 if m.State, err = r.getUint64(); err != nil {
1405- return Ack{}, err947+ return protocol.Ack{}, err
1406 }948 }
1407 if err := r.done(); err != nil {949 if err := r.done(); err != nil {
1408- return Ack{}, err950+ return protocol.Ack{}, err
1409 }951 }
1410- if err := ValidateAck(m); err != nil {952+ if err := protocol.ValidateAck(m); err != nil {
1411- return Ack{}, err953+ return protocol.Ack{}, err
1412 }954 }
1413 return m, nil955 return m, nil
1414 }956 }
@@ -1461,7 +1003,7 @@ func UnmarshalWelcome(b []byte) (Welcome, error) {
1461 return Welcome{}, err1003 return Welcome{}, err
1462 }1004 }
1463 if m.CommittedIdentity != nil && (m.CommittedIdentity.Target.SessionName != m.SessionName || m.CommittedIdentity.Ephemeral != m.Ephemeral) {1005 if m.CommittedIdentity != nil && (m.CommittedIdentity.Target.SessionName != m.SessionName || m.CommittedIdentity.Ephemeral != m.Ephemeral) {
1464- return Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", ErrInvalidRouteWire)1006+ return Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", protocol.ErrInvalidRouteWire)
1465 }1007 }
1466 if err := r.done(); err != nil {1008 if err := r.done(); err != nil {
1467 return Welcome{}, err1009 return Welcome{}, err
@@ -1498,8 +1040,8 @@ func UnmarshalErrorMsg(b []byte) (ErrorMsg, error) {
1498 // MarshalOutput encodes m into the epoch/base/new/echo/viewRevision/1040 // MarshalOutput encodes m into the epoch/base/new/echo/viewRevision/
1499 // size/full/compression/decoded-length/data message layout. Compression is1041 // size/full/compression/decoded-length/data message layout. Compression is
1500 // limited to large, full snapshots and is retained only when it saves bytes.1042 // limited to large, full snapshots and is retained only when it saves bytes.
1501-func MarshalOutput(m Output) ([]byte, error) {1043+func MarshalOutput(m protocol.Output) ([]byte, error) {
1502- if err := ValidateOutput(m); err != nil {1044+ if err := protocol.ValidateOutput(m); err != nil {
1503 return nil, err1045 return nil, err
1504 }1046 }
1505 kind, data, err := compressOutputData(m)1047 kind, data, err := compressOutputData(m)
@@ -1521,7 +1063,7 @@ func MarshalOutput(m Output) ([]byte, error) {
1521 return w.b, nil1063 return w.b, nil
1522 }1064 }
1523 1065
1524-func compressOutputData(m Output) (byte, []byte, error) {1066+func compressOutputData(m protocol.Output) (byte, []byte, error) {
1525 if !m.Full || len(m.Data) < outputCompressionThreshold {1067 if !m.Full || len(m.Data) < outputCompressionThreshold {
1526 return outputCompressionNone, m.Data, nil1068 return outputCompressionNone, m.Data, nil
1527 }1069 }
@@ -1546,83 +1088,83 @@ func compressOutputData(m Output) (byte, []byte, error) {
1546 1088
1547 // UnmarshalOutput strictly decodes one final Output payload. Header semantics1089 // UnmarshalOutput strictly decodes one final Output payload. Header semantics
1548 // are validated before its bounded decoded data allocation.1090 // are validated before its bounded decoded data allocation.
1549-func UnmarshalOutput(b []byte) (Output, error) {1091+func UnmarshalOutput(b []byte) (protocol.Output, error) {
1550 if len(b) > MaxFrameLen-1 {1092 if len(b) > MaxFrameLen-1 {
1551- return Output{}, ErrInvalidOutput1093+ return protocol.Output{}, protocol.ErrInvalidOutput
1552 }1094 }
1553 if len(b) < outputPayloadOverhead {1095 if len(b) < outputPayloadOverhead {
1554- return Output{}, errShortPayload1096+ return protocol.Output{}, errShortPayload
1555 }1097 }
1556 r := payloadReader{b: b}1098 r := payloadReader{b: b}
1557- var m Output1099+ var m protocol.Output
1558 var err error1100 var err error
1559 if m.Epoch, err = r.getUint64(); err != nil {1101 if m.Epoch, err = r.getUint64(); err != nil {
1560- return Output{}, err1102+ return protocol.Output{}, err
1561 }1103 }
1562 if m.Base, err = r.getUint64(); err != nil {1104 if m.Base, err = r.getUint64(); err != nil {
1563- return Output{}, err1105+ return protocol.Output{}, err
1564 }1106 }
1565 if m.New, err = r.getUint64(); err != nil {1107 if m.New, err = r.getUint64(); err != nil {
1566- return Output{}, err1108+ return protocol.Output{}, err
1567 }1109 }
1568 if m.Echo, err = r.getUint64(); err != nil {1110 if m.Echo, err = r.getUint64(); err != nil {
1569- return Output{}, err1111+ return protocol.Output{}, err
1570 }1112 }
1571 if m.ViewRevision, err = r.getUint64(); err != nil {1113 if m.ViewRevision, err = r.getUint64(); err != nil {
1572- return Output{}, err1114+ return protocol.Output{}, err
1573 }1115 }
1574 cols, err := r.getUint16()1116 cols, err := r.getUint16()
1575 if err != nil {1117 if err != nil {
1576- return Output{}, err1118+ return protocol.Output{}, err
1577 }1119 }
1578 rows, err := r.getUint16()1120 rows, err := r.getUint16()
1579 if err != nil {1121 if err != nil {
1580- return Output{}, err1122+ return protocol.Output{}, err
1581 }1123 }
1582 m.Size = domain.Size{Cols: int(cols), Rows: int(rows)}1124 m.Size = domain.Size{Cols: int(cols), Rows: int(rows)}
1583 if m.Full, err = r.getBool(); err != nil {1125 if m.Full, err = r.getBool(); err != nil {
1584- return Output{}, err1126+ return protocol.Output{}, err
1585 }1127 }
1586- if err := ValidateOutput(m); err != nil {1128+ if err := protocol.ValidateOutput(m); err != nil {
1587- return Output{}, err1129+ return protocol.Output{}, err
1588 }1130 }
1589 kind, err := r.getUint8()1131 kind, err := r.getUint8()
1590 if err != nil {1132 if err != nil {
1591- return Output{}, err1133+ return protocol.Output{}, err
1592 }1134 }
1593 decodedLen, err := r.getUint32()1135 decodedLen, err := r.getUint32()
1594 if err != nil {1136 if err != nil {
1595- return Output{}, err1137+ return protocol.Output{}, err
1596 }1138 }
1597- if int64(decodedLen) > int64(MaxOutputDataLen) {1139+ if int64(decodedLen) > int64(protocol.MaxOutputDataLen) {
1598- return Output{}, ErrInvalidOutput1140+ return protocol.Output{}, protocol.ErrInvalidOutput
1599 }1141 }
1600 data, err := r.getLongBytes()1142 data, err := r.getLongBytes()
1601 if err != nil {1143 if err != nil {
1602- return Output{}, err1144+ return protocol.Output{}, err
1603 }1145 }
1604 if err := r.done(); err != nil {1146 if err := r.done(); err != nil {
1605- return Output{}, err1147+ return protocol.Output{}, err
1606 }1148 }
1607 switch kind {1149 switch kind {
1608 case outputCompressionNone:1150 case outputCompressionNone:
1609 if uint32(len(data)) != decodedLen {1151 if uint32(len(data)) != decodedLen {
1610- return Output{}, ErrInvalidOutput1152+ return protocol.Output{}, protocol.ErrInvalidOutput
1611 }1153 }
1612 m.Data = data1154 m.Data = data
1613 case outputCompressionZlib:1155 case outputCompressionZlib:
1614 if !m.Full {1156 if !m.Full {
1615- return Output{}, ErrInvalidOutput1157+ return protocol.Output{}, protocol.ErrInvalidOutput
1616 }1158 }
1617 m.Data, err = decompressOutputData(data, int(decodedLen))1159 m.Data, err = decompressOutputData(data, int(decodedLen))
1618 if err != nil {1160 if err != nil {
1619- return Output{}, ErrInvalidOutput1161+ return protocol.Output{}, protocol.ErrInvalidOutput
1620 }1162 }
1621 default:1163 default:
1622- return Output{}, errInvalidEnum1164+ return protocol.Output{}, errInvalidEnum
1623 }1165 }
1624- if err := ValidateOutput(m); err != nil {1166+ if err := protocol.ValidateOutput(m); err != nil {
1625- return Output{}, err1167+ return protocol.Output{}, err
1626 }1168 }
1627 return m, nil1169 return m, nil
1628 }1170 }
@@ -1742,7 +1284,7 @@ func UnmarshalDetached(b []byte) (Detached, error) {
1742 if err != nil {1284 if err != nil {
1743 return Detached{}, err1285 return Detached{}, err
1744 }1286 }
1745- if !validDetachedReason(reason) {1287+ if ValidateDetached(Detached{Reason: reason}) != nil {
1746 return Detached{}, errInvalidEnum1288 return Detached{}, errInvalidEnum
1747 }1289 }
1748 if err := r.done(); err != nil {1290 if err := r.done(); err != nil {
@@ -1948,7 +1490,7 @@ func getPreviewStyle(r *payloadReader) (renderer.Style, error) {
1948 return renderer.Style{}, err1490 return renderer.Style{}, err
1949 }1491 }
1950 if flags&0x80 != 0 {1492 if flags&0x80 != 0 {
1951- return renderer.Style{}, ErrRemotePreviewUnsupportedStyle1493+ return renderer.Style{}, protocol.ErrRemotePreviewUnsupportedStyle
1952 }1494 }
1953 attrs, err := r.getUint16()1495 attrs, err := r.getUint16()
1954 if err != nil {1496 if err != nil {
@@ -1989,8 +1531,8 @@ func getPreviewStyle(r *payloadReader) (renderer.Style, error) {
1989 ForegroundRGB: fgrgb, BackgroundRGB: bgrgb, UnderlineColorRGB: ulrgb}, nil1531 ForegroundRGB: fgrgb, BackgroundRGB: bgrgb, UnderlineColorRGB: ulrgb}, nil
1990 }1532 }
1991 1533
1992-func MarshalRemotePreviewRequest(request RemotePreviewRequest) []byte {1534+func MarshalRemotePreviewRequest(request protocol.RemotePreviewRequest) []byte {
1993- if ValidateRemotePreviewRequest(request) != nil {1535+ if protocol.ValidateRemotePreviewRequest(request) != nil {
1994 return nil1536 return nil
1995 }1537 }
1996 w := payloadWriter{}1538 w := payloadWriter{}
@@ -2005,12 +1547,12 @@ func MarshalRemotePreviewRequest(request RemotePreviewRequest) []byte {
2005 return w.b1547 return w.b
2006 }1548 }
2007 1549
2008-func UnmarshalRemotePreviewRequest(data []byte) (RemotePreviewRequest, error) {1550+func UnmarshalRemotePreviewRequest(data []byte) (protocol.RemotePreviewRequest, error) {
2009- if len(data) > RemotePreviewMaxBytes {1551+ if len(data) > protocol.RemotePreviewMaxBytes {
2010- return RemotePreviewRequest{}, ErrInvalidRemotePreviewRequest1552+ return protocol.RemotePreviewRequest{}, protocol.ErrInvalidRemotePreviewRequest
2011 }1553 }
2012 r := payloadReader{b: data}1554 r := payloadReader{b: data}
2013- var q RemotePreviewRequest1555+ var q protocol.RemotePreviewRequest
2014 var err error1556 var err error
2015 if q.Version, err = r.getUint16(); err != nil {1557 if q.Version, err = r.getUint16(); err != nil {
2016 return q, err1558 return q, err
@@ -2043,7 +1585,7 @@ func UnmarshalRemotePreviewRequest(data []byte) (RemotePreviewRequest, error) {
2043 if err := r.done(); err != nil {1585 if err := r.done(); err != nil {
2044 return q, err1586 return q, err
2045 }1587 }
2046- if err := ValidateRemotePreviewRequest(q); err != nil {1588+ if err := protocol.ValidateRemotePreviewRequest(q); err != nil {
2047 return q, err1589 return q, err
2048 }1590 }
2049 return q, nil1591 return q, nil
@@ -2051,8 +1593,8 @@ func UnmarshalRemotePreviewRequest(data []byte) (RemotePreviewRequest, error) {
2051 1593
2052 const previewCellWireSize = 4 + 1 + 1 + 2 + 2 + 2 + 1 + 2 + 3 + 3 + 31594 const previewCellWireSize = 4 + 1 + 1 + 2 + 2 + 2 + 1 + 2 + 3 + 3 + 3
2053 1595
2054-func MarshalRemotePreview(preview RemotePreview) []byte {1596+func MarshalRemotePreview(preview protocol.RemotePreview) []byte {
2055- if ValidateRemotePreview(preview) != nil {1597+ if protocol.ValidateRemotePreview(preview) != nil {
2056 return nil1598 return nil
2057 }1599 }
2058 w := payloadWriter{}1600 w := payloadWriter{}
@@ -2076,12 +1618,12 @@ func MarshalRemotePreview(preview RemotePreview) []byte {
2076 return w.b1618 return w.b
2077 }1619 }
2078 1620
2079-func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {1621+func UnmarshalRemotePreview(data []byte) (protocol.RemotePreview, error) {
2080- if len(data) > RemotePreviewMaxBytes {1622+ if len(data) > protocol.RemotePreviewMaxBytes {
2081- return RemotePreview{}, ErrRemotePreviewTooLarge1623+ return protocol.RemotePreview{}, protocol.ErrRemotePreviewTooLarge
2082 }1624 }
2083 r := payloadReader{b: data}1625 r := payloadReader{b: data}
2084- var p RemotePreview1626+ var p protocol.RemotePreview
2085 var err error1627 var err error
2086 if p.Version, err = r.getUint16(); err != nil {1628 if p.Version, err = r.getUint16(); err != nil {
2087 return p, err1629 return p, err
@@ -2090,7 +1632,7 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
2090 if err != nil {1632 if err != nil {
2091 return p, err1633 return p, err
2092 }1634 }
2093- p.Status = RemotePreviewStatus(status)1635+ p.Status = protocol.RemotePreviewStatus(status)
2094 id, err := r.getBytes(16)1636 id, err := r.getBytes(16)
2095 if err != nil {1637 if err != nil {
2096 return p, err1638 return p, err
@@ -2114,8 +1656,8 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
2114 if err != nil {1656 if err != nil {
2115 return p, err1657 return p, err
2116 }1658 }
2117- if count > RemotePreviewMaxCells || uint64(count) > uint64(len(r.b)/previewCellWireSize) {1659+ if count > protocol.RemotePreviewMaxCells || uint64(count) > uint64(len(r.b)/previewCellWireSize) {
2118- return p, ErrRemotePreviewTooLarge1660+ return p, protocol.ErrRemotePreviewTooLarge
2119 }1661 }
2120 if count != 0 {1662 if count != 0 {
2121 p.Cells = make([]renderer.Cell, 0, int(count))1663 p.Cells = make([]renderer.Cell, 0, int(count))
@@ -2129,7 +1671,7 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
2129 return p, e1671 return p, e
2130 }1672 }
2131 if flags&^uint8(1) != 0 {1673 if flags&^uint8(1) != 0 {
2132- return p, ErrInvalidRemotePreview1674+ return p, protocol.ErrInvalidRemotePreview
2133 }1675 }
2134 style, e := getPreviewStyle(&r)1676 style, e := getPreviewStyle(&r)
2135 if e != nil {1677 if e != nil {
@@ -2141,18 +1683,18 @@ func UnmarshalRemotePreview(data []byte) (RemotePreview, error) {
2141 if err := r.done(); err != nil {1683 if err := r.done(); err != nil {
2142 return p, err1684 return p, err
2143 }1685 }
2144- if err := ValidateRemotePreview(p); err != nil {1686+ if err := protocol.ValidateRemotePreview(p); err != nil {
2145 return p, err1687 return p, err
2146 }1688 }
2147 return p, nil1689 return p, nil
2148 }1690 }
2149 1691
2150 // MarshalNavigationDirective encodes one bounded navigation directive.1692 // MarshalNavigationDirective encodes one bounded navigation directive.
2151-func MarshalNavigationDirective(directive NavigationDirective) []byte {1693+func MarshalNavigationDirective(directive protocol.NavigationDirective) []byte {
2152- if directive.Action != NavigationOpenHomePicker && directive.Action != NavigationBack {1694+ if directive.Action != protocol.NavigationOpenHomePicker && directive.Action != protocol.NavigationBack {
2153 return nil1695 return nil
2154 }1696 }
2155- if directive.Action == NavigationOpenHomePicker && directive.LeaseID.IsZero() || directive.Action == NavigationBack && !directive.LeaseID.IsZero() {1697+ if directive.Action == protocol.NavigationOpenHomePicker && directive.LeaseID.IsZero() || directive.Action == protocol.NavigationBack && !directive.LeaseID.IsZero() {
2156 return nil1698 return nil
2157 }1699 }
2158 w := payloadWriter{}1700 w := payloadWriter{}
@@ -2162,51 +1704,33 @@ func MarshalNavigationDirective(directive NavigationDirective) []byte {
2162 }1704 }
2163 1705
2164 // UnmarshalNavigationDirective decodes one strict navigation directive.1706 // UnmarshalNavigationDirective decodes one strict navigation directive.
2165-func UnmarshalNavigationDirective(b []byte) (NavigationDirective, error) {1707+func UnmarshalNavigationDirective(b []byte) (protocol.NavigationDirective, error) {
2166 r := payloadReader{b: b}1708 r := payloadReader{b: b}
2167 value, err := r.getUint8()1709 value, err := r.getUint8()
2168 if err != nil {1710 if err != nil {
2169- return NavigationDirective{}, ErrInvalidNavigation1711+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
2170 }1712 }
2171- lease, err := r.getBytes(len(ParkedRouteLeaseID{}))1713+ lease, err := r.getBytes(len(protocol.ParkedRouteLeaseID{}))
2172 if err != nil {1714 if err != nil {
2173- return NavigationDirective{}, ErrInvalidNavigation1715+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
2174 }1716 }
2175 if err := r.done(); err != nil {1717 if err := r.done(); err != nil {
2176- return NavigationDirective{}, ErrInvalidNavigation1718+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
2177 }1719 }
2178- directive := NavigationDirective{Action: NavigationAction(value)}1720+ directive := protocol.NavigationDirective{Action: protocol.NavigationAction(value)}
2179 copy(directive.LeaseID[:], lease)1721 copy(directive.LeaseID[:], lease)
2180 if MarshalNavigationDirective(directive) == nil {1722 if MarshalNavigationDirective(directive) == nil {
2181- return NavigationDirective{}, ErrInvalidNavigation1723+ return protocol.NavigationDirective{}, protocol.ErrInvalidNavigation
2182 }1724 }
2183 return directive, nil1725 return directive, nil
2184 }1726 }
2185 1727
2186 // ValidateParkedRouteRequest enforces the closed action/target shape before a1728 // ValidateParkedRouteRequest enforces the closed action/target shape before a
2187 // request reaches either side's route state machine.1729 // request reaches either side's route state machine.
2188-func ValidateParkedRouteRequest(request ParkedRouteRequest) error {
2189- if request.RequestID == 0 || request.LeaseID.IsZero() {
2190- return ErrInvalidNavigation
2191- }
2192- switch request.Action {
2193- case ParkedRoutePrepare, ParkedRouteResume:
2194- if request.Target != nil {
2195- return ErrInvalidNavigation
2196- }
2197- case ParkedRouteSwitch:
2198- if request.Target == nil || validateWireRemoteTarget(*request.Target) != nil {
2199- return ErrInvalidNavigation
2200- }
2201- default:
2202- return ErrInvalidNavigation
2203- }
2204- return nil
2205-}
2206 1730
2207 // MarshalParkedRouteRequest encodes one retained-route operation.1731 // MarshalParkedRouteRequest encodes one retained-route operation.
2208-func MarshalParkedRouteRequest(request ParkedRouteRequest) []byte {1732+func MarshalParkedRouteRequest(request protocol.ParkedRouteRequest) []byte {
2209- if ValidateParkedRouteRequest(request) != nil {1733+ if protocol.ValidateParkedRouteRequest(request) != nil {
2210 return nil1734 return nil
2211 }1735 }
2212 w := payloadWriter{}1736 w := payloadWriter{}
@@ -2218,35 +1742,35 @@ func MarshalParkedRouteRequest(request ParkedRouteRequest) []byte {
2218 }1742 }
2219 1743
2220 // UnmarshalParkedRouteRequest decodes one strict retained-route operation.1744 // UnmarshalParkedRouteRequest decodes one strict retained-route operation.
2221-func UnmarshalParkedRouteRequest(b []byte) (ParkedRouteRequest, error) {1745+func UnmarshalParkedRouteRequest(b []byte) (protocol.ParkedRouteRequest, error) {
2222 r := payloadReader{b: b}1746 r := payloadReader{b: b}
2223 requestID, err := r.getUint64()1747 requestID, err := r.getUint64()
2224 if err != nil {1748 if err != nil {
2225- return ParkedRouteRequest{}, ErrInvalidNavigation1749+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
2226 }1750 }
2227- lease, err := r.getBytes(len(ParkedRouteLeaseID{}))1751+ lease, err := r.getBytes(len(protocol.ParkedRouteLeaseID{}))
2228 if err != nil {1752 if err != nil {
2229- return ParkedRouteRequest{}, ErrInvalidNavigation1753+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
2230 }1754 }
2231 action, err := r.getUint8()1755 action, err := r.getUint8()
2232 if err != nil {1756 if err != nil {
2233- return ParkedRouteRequest{}, ErrInvalidNavigation1757+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
2234 }1758 }
2235 target, policy, err := unmarshalRemoteTargetSection(&r)1759 target, policy, err := unmarshalRemoteTargetSection(&r)
2236 if err != nil || policy != EnvironmentPolicyDaemonOwned || r.done() != nil {1760 if err != nil || policy != EnvironmentPolicyDaemonOwned || r.done() != nil {
2237- return ParkedRouteRequest{}, ErrInvalidNavigation1761+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
2238 }1762 }
2239- request := ParkedRouteRequest{RequestID: requestID, Action: ParkedRouteAction(action), Target: target}1763+ request := protocol.ParkedRouteRequest{RequestID: requestID, Action: protocol.ParkedRouteAction(action), Target: target}
2240 copy(request.LeaseID[:], lease)1764 copy(request.LeaseID[:], lease)
2241- if ValidateParkedRouteRequest(request) != nil {1765+ if protocol.ValidateParkedRouteRequest(request) != nil {
2242- return ParkedRouteRequest{}, ErrInvalidNavigation1766+ return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
2243 }1767 }
2244 return request, nil1768 return request, nil
2245 }1769 }
2246 1770
2247 // MarshalParkedRouteResponse encodes one correlated retained-route outcome.1771 // MarshalParkedRouteResponse encodes one correlated retained-route outcome.
2248-func MarshalParkedRouteResponse(response ParkedRouteResponse) []byte {1772+func MarshalParkedRouteResponse(response protocol.ParkedRouteResponse) []byte {
2249- if response.RequestID == 0 || response.Status < ParkedRouteReady || response.Status > ParkedRouteStaleTarget {1773+ if response.RequestID == 0 || response.Status < protocol.ParkedRouteReady || response.Status > protocol.ParkedRouteStaleTarget {
2250 return nil1774 return nil
2251 }1775 }
2252 w := payloadWriter{}1776 w := payloadWriter{}
@@ -2256,19 +1780,19 @@ func MarshalParkedRouteResponse(response ParkedRouteResponse) []byte {
2256 }1780 }
2257 1781
2258 // UnmarshalParkedRouteResponse decodes one strict retained-route outcome.1782 // UnmarshalParkedRouteResponse decodes one strict retained-route outcome.
2259-func UnmarshalParkedRouteResponse(b []byte) (ParkedRouteResponse, error) {1783+func UnmarshalParkedRouteResponse(b []byte) (protocol.ParkedRouteResponse, error) {
2260 r := payloadReader{b: b}1784 r := payloadReader{b: b}
2261 requestID, err := r.getUint64()1785 requestID, err := r.getUint64()
2262 if err != nil {1786 if err != nil {
2263- return ParkedRouteResponse{}, ErrInvalidNavigation1787+ return protocol.ParkedRouteResponse{}, protocol.ErrInvalidNavigation
2264 }1788 }
2265 status, err := r.getUint8()1789 status, err := r.getUint8()
2266 if err != nil || r.done() != nil {1790 if err != nil || r.done() != nil {
2267- return ParkedRouteResponse{}, ErrInvalidNavigation1791+ return protocol.ParkedRouteResponse{}, protocol.ErrInvalidNavigation
2268 }1792 }
2269- response := ParkedRouteResponse{RequestID: requestID, Status: ParkedRouteStatus(status)}1793+ response := protocol.ParkedRouteResponse{RequestID: requestID, Status: protocol.ParkedRouteStatus(status)}
2270 if MarshalParkedRouteResponse(response) == nil {1794 if MarshalParkedRouteResponse(response) == nil {
2271- return ParkedRouteResponse{}, ErrInvalidNavigation1795+ return protocol.ParkedRouteResponse{}, protocol.ErrInvalidNavigation
2272 }1796 }
2273 return response, nil1797 return response, nil
2274 }1798 }
M internal/ports/wire_final_test.go +11 -10
@@ -8,6 +8,7 @@ import (
8 "testing"8 "testing"
9 9
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/protocol"
11 )12 )
12 13
13 func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) {14 func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) {
@@ -81,7 +82,7 @@ func TestFinalHelloSemanticValidation(t *testing.T) {
81 }82 }
82 83
83 func TestFinalResizeGoldenStrict(t *testing.T) {84 func TestFinalResizeGoldenStrict(t *testing.T) {
84- msg := Resize{Size: domain.Size{Cols: 80, Rows: 24}}85+ msg := protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}
85 want := []byte{0, 80, 0, 24, 0, 0, 0, 0}86 want := []byte{0, 80, 0, 24, 0, 0, 0, 0}
86 got, err := MarshalResize(msg)87 got, err := MarshalResize(msg)
87 if err != nil {88 if err != nil {
@@ -97,14 +98,14 @@ func TestFinalResizeGoldenStrict(t *testing.T) {
97 assertAllPrefixesFail(t, got, UnmarshalResize)98 assertAllPrefixesFail(t, got, UnmarshalResize)
98 assertTrailingGarbageFails(t, got, UnmarshalResize)99 assertTrailingGarbageFails(t, got, UnmarshalResize)
99 for _, size := range []domain.Size{{}, {Cols: 513, Rows: 512}} {100 for _, size := range []domain.Size{{}, {Cols: 513, Rows: 512}} {
100- if got, err := MarshalResize(Resize{Size: size}); err == nil || got != nil {101+ if got, err := MarshalResize(protocol.Resize{Size: size}); err == nil || got != nil {
101 t.Fatalf("MarshalResize(%+v) = (%x, %v), want nil payload and error", size, got, err)102 t.Fatalf("MarshalResize(%+v) = (%x, %v), want nil payload and error", size, got, err)
102 }103 }
103 }104 }
104 }105 }
105 106
106 func TestFinalOutputGoldenStrict(t *testing.T) {107 func TestFinalOutputGoldenStrict(t *testing.T) {
107- msg := Output{108+ msg := protocol.Output{
108 Epoch: 1, Base: 0, New: 7, Echo: 8, ViewRevision: 9,109 Epoch: 1, Base: 0, New: 7, Echo: 8, ViewRevision: 9,
109 Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("ok"),110 Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("ok"),
110 }111 }
@@ -134,8 +135,8 @@ func TestFinalOutputGoldenStrict(t *testing.T) {
134 }135 }
135 136
136 func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) {137 func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) {
137- valid := Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true}138+ valid := protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true}
138- for _, bad := range []Output{139+ for _, bad := range []protocol.Output{
139 {},140 {},
140 {Epoch: 1, Base: 0, New: 1, Size: valid.Size},141 {Epoch: 1, Base: 0, New: 1, Size: valid.Size},
141 {Epoch: 1, Base: 0, New: 1, Full: true},142 {Epoch: 1, Base: 0, New: 1, Full: true},
@@ -170,7 +171,7 @@ func TestFinalOutputSemanticValidationBeforeDataAllocation(t *testing.T) {
170 }171 }
171 172
172 func TestFinalAckGoldenStrict(t *testing.T) {173 func TestFinalAckGoldenStrict(t *testing.T) {
173- msg := Ack{Epoch: 2, State: 7}174+ msg := protocol.Ack{Epoch: 2, State: 7}
174 want := []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 7}175 want := []byte{0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 7}
175 got, err := MarshalAck(msg)176 got, err := MarshalAck(msg)
176 if err != nil {177 if err != nil {
@@ -180,7 +181,7 @@ func TestFinalAckGoldenStrict(t *testing.T) {
180 t.Fatalf("Ack bytes = %x, want %x", got, want)181 t.Fatalf("Ack bytes = %x, want %x", got, want)
181 }182 }
182 back, err := UnmarshalAck(got)183 back, err := UnmarshalAck(got)
183- if err != nil || back != (Ack{Epoch: 2, State: 7}) {184+ if err != nil || back != (protocol.Ack{Epoch: 2, State: 7}) {
184 t.Fatalf("Ack = %+v, error %v", back, err)185 t.Fatalf("Ack = %+v, error %v", back, err)
185 }186 }
186 assertAllPrefixesFail(t, got, UnmarshalAck)187 assertAllPrefixesFail(t, got, UnmarshalAck)
@@ -188,7 +189,7 @@ func TestFinalAckGoldenStrict(t *testing.T) {
188 if _, err := UnmarshalAck(make([]byte, 16)); err == nil {189 if _, err := UnmarshalAck(make([]byte, 16)); err == nil {
189 t.Fatal("UnmarshalAck accepted zero epoch")190 t.Fatal("UnmarshalAck accepted zero epoch")
190 }191 }
191- if got, err := MarshalAck(Ack{State: 7}); err == nil || got != nil {192+ if got, err := MarshalAck(protocol.Ack{State: 7}); err == nil || got != nil {
192 t.Fatalf("MarshalAck accepted zero epoch: payload=%x err=%v", got, err)193 t.Fatalf("MarshalAck accepted zero epoch: payload=%x err=%v", got, err)
193 }194 }
194 }195 }
@@ -231,7 +232,7 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
231 func TestAttachTargetExactTargetWireStrict(t *testing.T) {232 func TestAttachTargetExactTargetWireStrict(t *testing.T) {
232 var lifecycle domain.SessionLifecycleID233 var lifecycle domain.SessionLifecycleID
233 lifecycle[0] = 1234 lifecycle[0] = 1
234- target := AttachTarget{Session: "work", Intent: IntentAttach, ExactTarget: &ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}235+ target := AttachTarget{Session: "work", Intent: IntentAttach, ExactTarget: &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}
235 payload := MarshalAttachTarget(target)236 payload := MarshalAttachTarget(target)
236 want := append([]byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 1, 1}, make([]byte, 15)...)237 want := append([]byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 1, 1}, make([]byte, 15)...)
237 want = append(want, 0, 4, 'w', 'o', 'r', 'k', 1)238 want = append(want, 0, 4, 'w', 'o', 'r', 'k', 1)
@@ -245,7 +246,7 @@ func TestAttachTargetExactTargetWireStrict(t *testing.T) {
245 assertAllPrefixesFail(t, payload, UnmarshalAttachTarget)246 assertAllPrefixesFail(t, payload, UnmarshalAttachTarget)
246 assertTrailingGarbageFails(t, payload, UnmarshalAttachTarget)247 assertTrailingGarbageFails(t, payload, UnmarshalAttachTarget)
247 mismatched := target248 mismatched := target
248- mismatched.ExactTarget = &ExactSessionTarget{LifecycleID: lifecycle, SessionName: "other"}249+ mismatched.ExactTarget = &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "other"}
249 if got := MarshalAttachTarget(mismatched); got != nil {250 if got := MarshalAttachTarget(mismatched); got != nil {
250 t.Fatalf("MarshalAttachTarget accepted mismatched target: %x", got)251 t.Fatalf("MarshalAttachTarget accepted mismatched target: %x", got)
251 }252 }
M internal/ports/wire_test.go +17 -16
@@ -9,6 +9,7 @@ import (
9 9
10 renderer "github.com/bnema/vev-vt"10 renderer "github.com/bnema/vev-vt"
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12+ "github.com/bnema/vev/internal/protocol"
12 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
13 )14 )
14 15
@@ -243,15 +244,15 @@ func TestInputGoldenAndRoundTrip(t *testing.T) {
243 func TestImagePushGoldenAndRoundTrip(t *testing.T) {244 func TestImagePushGoldenAndRoundTrip(t *testing.T) {
244 tests := []struct {245 tests := []struct {
245 name string246 name string
246- msg ImagePush247+ msg protocol.ImagePush
247 want []byte248 want []byte
248 }{249 }{
249 {250 {
250 name: "png data",251 name: "png data",
251- msg: ImagePush{InputSeq: 7, Mime: "image/png", Data: []byte{0x01, 0x02, 0x03}},252+ msg: protocol.ImagePush{InputSeq: 7, Mime: "image/png", Data: []byte{0x01, 0x02, 0x03}},
252 want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x01, 0x02, 0x03},253 want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x07, 0x00, 0x09, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x01, 0x02, 0x03},
253 },254 },
254- {name: "empty", msg: ImagePush{}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},255+ {name: "empty", msg: protocol.ImagePush{}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
255 }256 }
256 257
257 for _, tt := range tests {258 for _, tt := range tests {
@@ -288,17 +289,17 @@ func TestThemeGoldenAndRoundTrip(t *testing.T) {
288 zeroes := make([]byte, 57)289 zeroes := make([]byte, 57)
289 tests := []struct {290 tests := []struct {
290 name string291 name string
291- msg Theme292+ msg protocol.Theme
292 want []byte293 want []byte
293 }{294 }{
294 {295 {
295 name: "empty",296 name: "empty",
296- msg: Theme{},297+ msg: protocol.Theme{},
297 want: zeroes,298 want: zeroes,
298 },299 },
299 {300 {
300 name: "foreground only",301 name: "foreground only",
301- msg: Theme{302+ msg: protocol.Theme{
302 HasForeground: true,303 HasForeground: true,
303 Foreground: renderer.RGB{R: 10, G: 20, B: 30},304 Foreground: renderer.RGB{R: 10, G: 20, B: 30},
304 Background: renderer.RGB{R: 40, G: 50, B: 60},305 Background: renderer.RGB{R: 40, G: 50, B: 60},
@@ -307,14 +308,14 @@ func TestThemeGoldenAndRoundTrip(t *testing.T) {
307 },308 },
308 {309 {
309 name: "light flag without known scheme",310 name: "light flag without known scheme",
310- msg: Theme{311+ msg: protocol.Theme{
311 Light: true,312 Light: true,
312 },313 },
313 want: append([]byte{0x10}, zeroes[1:]...),314 want: append([]byte{0x10}, zeroes[1:]...),
314 },315 },
315 {316 {
316 name: "full palette",317 name: "full palette",
317- msg: Theme{318+ msg: protocol.Theme{
318 HasForeground: true,319 HasForeground: true,
319 Foreground: renderer.RGB{R: 1, G: 2, B: 3},320 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
320 HasBackground: true,321 HasBackground: true,
@@ -356,7 +357,7 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T)
356 // A generation clear retains defaults/capabilities but has no palette bits.357 // A generation clear retains defaults/capabilities but has no palette bits.
357 // This literal payload locks the existing 57-byte Theme layout while the358 // This literal payload locks the existing 57-byte Theme layout while the
358 // full definitive-palette golden above locks the final publication.359 // full definitive-palette golden above locks the final publication.
359- cleared := Theme{360+ cleared := protocol.Theme{
360 HasForeground: true,361 HasForeground: true,
361 Foreground: renderer.RGB{R: 1, G: 2, B: 3},362 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
362 HasBackground: true,363 HasBackground: true,
@@ -370,7 +371,7 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T)
370 }371 }
371 372
372 func TestResizeGoldenAndRoundTrip(t *testing.T) {373 func TestResizeGoldenAndRoundTrip(t *testing.T) {
373- msg := Resize{Size: domain.Size{Cols: 100, Rows: 40}}374+ msg := protocol.Resize{Size: domain.Size{Cols: 100, Rows: 40}}
374 want := []byte{0x00, 0x64, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00}375 want := []byte{0x00, 0x64, 0x00, 0x28, 0x00, 0x00, 0x00, 0x00}
375 376
376 got, err := MarshalResize(msg)377 got, err := MarshalResize(msg)
@@ -440,8 +441,8 @@ func TestPongGoldenAndRoundTrip(t *testing.T) {
440 func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {441 func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {
441 got := MarshalWelcome(Welcome{442 got := MarshalWelcome(Welcome{
442 SessionName: "work",443 SessionName: "work",
443- CommittedIdentity: &CommittedRouteIdentity{444+ CommittedIdentity: &protocol.CommittedRouteIdentity{
444- Target: ExactSessionTarget{SessionName: "different"},445+ Target: protocol.ExactSessionTarget{SessionName: "different"},
445 },446 },
446 })447 })
447 require.Nil(t, got)448 require.Nil(t, got)
@@ -452,8 +453,8 @@ func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
452 SessionID: "session",453 SessionID: "session",
453 SessionName: "work",454 SessionName: "work",
454 Ephemeral: true,455 Ephemeral: true,
455- CommittedIdentity: &CommittedRouteIdentity{456+ CommittedIdentity: &protocol.CommittedRouteIdentity{
456- Target: ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"},457+ Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"},
457 Ephemeral: true,458 Ephemeral: true,
458 },459 },
459 }460 }
@@ -675,7 +676,7 @@ func TestErrorMsgGoldenAndRoundTrip(t *testing.T) {
675 }676 }
676 677
677 func TestOutputGoldenAndRoundTrip(t *testing.T) {678 func TestOutputGoldenAndRoundTrip(t *testing.T) {
678- msg := Output{Epoch: 1, Base: 0, New: 2, Echo: 3, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("hello\n")}679+ msg := protocol.Output{Epoch: 1, Base: 0, New: 2, Echo: 3, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("hello\n")}
679 want := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a}680 want := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x06, 0x68, 0x65, 0x6c, 0x6c, 0x6f, 0x0a}
680 681
681 got, err := MarshalOutput(msg)682 got, err := MarshalOutput(msg)
@@ -698,7 +699,7 @@ func TestOutputGoldenAndRoundTrip(t *testing.T) {
698 }699 }
699 700
700 func TestAckGoldenAndRoundTrip(t *testing.T) {701 func TestAckGoldenAndRoundTrip(t *testing.T) {
701- msg := Ack{Epoch: 1, State: 0x0102030405060708}702+ msg := protocol.Ack{Epoch: 1, State: 0x0102030405060708}
702 want := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}703 want := []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}
703 704
704 got, err := MarshalAck(msg)705 got, err := MarshalAck(msg)
A internal/protocol/navigation.go +106 -0
@@ -0,0 +1,106 @@
1+package protocol
2+
3+import "github.com/bnema/vev/internal/domain"
4+
5+type NavigationCapabilities uint8
6+
7+const (
8+ NavigationCapabilityHomePicker NavigationCapabilities = 1 << iota
9+ NavigationCapabilityBack
10+)
11+
12+type StartupOverlay uint8
13+
14+const (
15+ StartupOverlayNone StartupOverlay = iota
16+ StartupOverlaySessionPicker
17+)
18+
19+type NavigationAction uint8
20+
21+const (
22+ NavigationOpenHomePicker NavigationAction = 1
23+ NavigationBack NavigationAction = 2
24+)
25+
26+type ParkedRouteLeaseID [16]byte
27+
28+func (id ParkedRouteLeaseID) IsZero() bool { return id == ParkedRouteLeaseID{} }
29+
30+type NavigationDirective struct {
31+ Action NavigationAction
32+ LeaseID ParkedRouteLeaseID
33+}
34+
35+type ParkedRouteAction uint8
36+
37+const (
38+ ParkedRoutePrepare ParkedRouteAction = iota + 1
39+ ParkedRouteResume
40+ ParkedRouteSwitch
41+)
42+
43+type ParkedRouteRequest struct {
44+ RequestID uint64
45+ LeaseID ParkedRouteLeaseID
46+ Action ParkedRouteAction
47+ Target *domain.RemoteSessionTarget
48+}
49+
50+type ParkedRouteStatus uint8
51+
52+const (
53+ ParkedRouteReady ParkedRouteStatus = iota + 1
54+ ParkedRouteResumed
55+ ParkedRouteSwitched
56+ ParkedRouteRejected
57+ ParkedRouteExpired
58+ ParkedRouteStaleTarget
59+)
60+
61+type ParkedRouteResponse struct {
62+ RequestID uint64
63+ Status ParkedRouteStatus
64+}
65+
66+func validNavigationCapabilities(capabilities NavigationCapabilities) bool {
67+ return capabilities&^(NavigationCapabilityHomePicker|NavigationCapabilityBack) == 0
68+}
69+
70+func validStartupOverlay(overlay StartupOverlay) bool {
71+ return overlay == StartupOverlayNone || overlay == StartupOverlaySessionPicker
72+}
73+
74+func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOverlay, homePickerRoute bool) error {
75+ if !validNavigationCapabilities(capabilities) || !validStartupOverlay(overlay) {
76+ return ErrInvalidNavigation
77+ }
78+ home := capabilities&NavigationCapabilityHomePicker != 0
79+ back := capabilities&NavigationCapabilityBack != 0
80+ if (home && !homePickerRoute) || back != (overlay == StartupOverlaySessionPicker) {
81+ return ErrInvalidNavigation
82+ }
83+ if homePickerRoute && back {
84+ return ErrInvalidNavigation
85+ }
86+ return nil
87+}
88+
89+func ValidateParkedRouteRequest(request ParkedRouteRequest) error {
90+ if request.RequestID == 0 || request.LeaseID.IsZero() {
91+ return ErrInvalidNavigation
92+ }
93+ switch request.Action {
94+ case ParkedRoutePrepare, ParkedRouteResume:
95+ if request.Target != nil {
96+ return ErrInvalidNavigation
97+ }
98+ case ParkedRouteSwitch:
99+ if request.Target == nil || validateRemoteTarget(*request.Target) != nil {
100+ return ErrInvalidNavigation
101+ }
102+ default:
103+ return ErrInvalidNavigation
104+ }
105+ return nil
106+}
R internal/ports/remote_preview.go → internal/protocol/preview.go +35 -1 renamed, 82% similar
@@ -1,4 +1,4 @@
1-package ports1+package protocol
2 2
3 import (3 import (
4 "errors"4 "errors"
@@ -10,6 +10,40 @@ import (
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 )11 )
12 12
13+// RemotePreviewSchemaVersion is independent from the attachment protocol version.
14+const RemotePreviewSchemaVersion uint16 = 1
15+
16+type RemotePreviewStatus uint8
17+
18+const (
19+ RemotePreviewOK RemotePreviewStatus = iota
20+ RemotePreviewUnavailable
21+ RemotePreviewNoSuchTarget
22+ RemotePreviewStale
23+ RemotePreviewMalformed
24+ RemotePreviewTooLarge
25+)
26+
27+type RemotePreviewRequest struct {
28+ Version uint16
29+ Target domain.RemoteSessionTarget
30+ Width uint16
31+ Height uint16
32+}
33+
34+// RemotePreview is a bounded row-major styled-cell viewport. It is never
35+// persisted, logged, or traced.
36+type RemotePreview struct {
37+ Version uint16
38+ Status RemotePreviewStatus
39+ LifecycleID domain.SessionLifecycleID
40+ TabID domain.TabStableID
41+ Revision uint64
42+ Width uint16
43+ Height uint16
44+ Cells []renderer.Cell
45+}
46+
13 const (47 const (
14 RemotePreviewMaxWidth = 25648 RemotePreviewMaxWidth = 256
15 RemotePreviewMaxHeight = 12849 RemotePreviewMaxHeight = 128
R internal/ports/routes.go → internal/protocol/routes.go +21 -3 renamed, 93% similar
@@ -1,4 +1,4 @@
1-package ports1+package protocol
2 2
3 import (3 import (
4 "errors"4 "errors"
@@ -9,6 +9,10 @@ import (
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 )10 )
11 11
12+// ErrInvalidRouteWire preserves the public error classification used by
13+// strict route codecs while route values live in the semantic protocol.
14+var ErrInvalidRouteWire = errors.New("invalid route wire message")
15+
12 // RouteOrigin identifies how a client reached a daemon. The origin is client16 // RouteOrigin identifies how a client reached a daemon. The origin is client
13 // composition metadata, not an authority granted to the daemon.17 // composition metadata, not an authority granted to the daemon.
14 type RouteOrigin uint818 type RouteOrigin uint8
@@ -181,10 +185,10 @@ type RouteAttentionSubscription struct {
181 Targets []RouteAttentionTarget185 Targets []RouteAttentionTarget
182 }186 }
183 187
184-func (r RouteRef) empty() bool { return r.Key == 0 && r.Generation == 0 }188+func (r RouteRef) IsZero() bool { return r.Key == 0 && r.Generation == 0 }
185 189
186 func (r RouteRef) Validate() error {190 func (r RouteRef) Validate() error {
187- if r.empty() {191+ if r.IsZero() {
188 return nil192 return nil
189 }193 }
190 if r.Key == 0 || r.Generation == 0 {194 if r.Key == 0 || r.Generation == 0 {
@@ -266,6 +270,13 @@ func (c SamePeerSwitchFailureCode) valid() bool {
266 return c == SamePeerSwitchStaleTarget || c == SamePeerSwitchUnavailable270 return c == SamePeerSwitchStaleTarget || c == SamePeerSwitchUnavailable
267 }271 }
268 272
273+func (c SamePeerSwitchFailureCode) Validate() error {
274+ if !c.valid() {
275+ return ErrInvalidRouteWire
276+ }
277+ return nil
278+}
279+
269 // SamePeerSwitchFailure leaves the source attachment unchanged. RequestID280 // SamePeerSwitchFailure leaves the source attachment unchanged. RequestID
270 // rejects a delayed failure after a later user selection.281 // rejects a delayed failure after a later user selection.
271 type SamePeerSwitchFailure struct {282 type SamePeerSwitchFailure struct {
@@ -307,6 +318,13 @@ func (c RouteFailureCode) valid() bool {
307 }318 }
308 }319 }
309 320
321+func (c RouteFailureCode) Validate() error {
322+ if !c.valid() {
323+ return ErrInvalidRouteWire
324+ }
325+ return nil
326+}
327+
310 const (328 const (
311 // RouteSnapshotMaxEntries bounds one immutable publication and the amount329 // RouteSnapshotMaxEntries bounds one immutable publication and the amount
312 // of work a receiver performs before returning to its transport loop. The330 // of work a receiver performs before returning to its transport loop. The
A internal/protocol/session.go +101 -0
@@ -0,0 +1,101 @@
1+// Package protocol defines vev's typed, transport-neutral client/daemon
2+// contract. Binary framing and codecs live outside this package.
3+package protocol
4+
5+import (
6+ "errors"
7+ "fmt"
8+ "math"
9+
10+ renderer "github.com/bnema/vev-vt"
11+ "github.com/bnema/vev/internal/domain"
12+)
13+
14+// MaxOutputDataLen is the largest decoded terminal byte stream accepted by one
15+// output publication in protocol version 37.
16+const MaxOutputDataLen = (16 << 20) - 55
17+
18+var (
19+ ErrInvalidSize = errors.New("ports: invalid terminal size")
20+ ErrInvalidOutput = errors.New("ports: invalid output")
21+ ErrInvalidAck = errors.New("ports: invalid ack")
22+ ErrInvalidNavigation = errors.New("ports: invalid navigation")
23+)
24+
25+type Resize struct {
26+ Size domain.Size
27+ PixelWidth int
28+ PixelHeight int
29+}
30+
31+func (m Resize) Geometry() domain.Geometry {
32+ return domain.Geometry{Size: m.Size, PixelWidth: m.PixelWidth, PixelHeight: m.PixelHeight}.NormalizePixels()
33+}
34+
35+type Theme struct {
36+ HasForeground bool
37+ Foreground renderer.RGB
38+ HasBackground bool
39+ Background renderer.RGB
40+ TrueColor bool
41+ SchemeKnown bool
42+ Light bool
43+ PaletteKnown uint16
44+ Palette [16]renderer.RGB
45+}
46+
47+type ImagePush struct {
48+ InputSeq uint64
49+ Mime string
50+ Data []byte
51+}
52+
53+type Ack struct {
54+ Epoch uint64
55+ State uint64
56+}
57+
58+type Output struct {
59+ Epoch uint64
60+ Base uint64
61+ New uint64
62+ Echo uint64
63+ ViewRevision uint64
64+ Size domain.Size
65+ Full bool
66+ Data []byte
67+}
68+
69+func ValidateOutput(m Output) error {
70+ if m.Epoch == 0 {
71+ return ErrInvalidOutput
72+ }
73+ if m.New == 0 {
74+ if m.Base != 0 || m.Full {
75+ return ErrInvalidOutput
76+ }
77+ } else if (m.Base == 0 && !m.Full) || (m.Base != 0 && (m.Full || m.New != m.Base+1)) {
78+ return ErrInvalidOutput
79+ }
80+ if err := ValidateSize(m.Size); err != nil {
81+ return fmt.Errorf("%w: size", ErrInvalidOutput)
82+ }
83+ if uint64(len(m.Data)) > math.MaxUint32 || len(m.Data) > MaxOutputDataLen {
84+ return ErrInvalidOutput
85+ }
86+ return nil
87+}
88+
89+func ValidateAck(m Ack) error {
90+ if m.Epoch == 0 {
91+ return ErrInvalidAck
92+ }
93+ return nil
94+}
95+
96+func validateRemoteTarget(target domain.RemoteSessionTarget) error {
97+ if len(target.Endpoint) > math.MaxUint16 || len(target.DisplayOrigin) > math.MaxUint16 || len(target.SessionName) > math.MaxUint16 || len(target.LiveTabID) > math.MaxUint16 || len(target.StoppedTab.RawName) > math.MaxUint16 {
98+ return errors.New("remote target string too long")
99+ }
100+ return target.Validate()
101+}
A internal/protocol/session_test.go +49 -0
@@ -0,0 +1,49 @@
1+package protocol
2+
3+import (
4+ "errors"
5+ "testing"
6+
7+ renderer "github.com/bnema/vev-vt"
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/stretchr/testify/require"
10+)
11+
12+func TestOutputSemanticValidation(t *testing.T) {
13+ valid := Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}
14+ require.NoError(t, ValidateOutput(valid))
15+
16+ invalid := valid
17+ invalid.Epoch = 0
18+ require.ErrorIs(t, ValidateOutput(invalid), ErrInvalidOutput)
19+
20+ invalid = valid
21+ invalid.Data = make([]byte, MaxOutputDataLen+1)
22+ require.ErrorIs(t, ValidateOutput(invalid), ErrInvalidOutput)
23+}
24+
25+func TestRouteAndPreviewSemanticValidation(t *testing.T) {
26+ require.Error(t, ValidateRouteLabel("\x1b[31m", false))
27+ require.NoError(t, RouteRef{}.Validate())
28+ require.Error(t, (RouteRef{Key: 1}).Validate())
29+
30+ target := domain.RemoteSessionTarget{
31+ Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{1},
32+ SessionName: "work", LiveTabID: "tab-1",
33+ }
34+ request := RemotePreviewRequest{Version: RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}
35+ require.NoError(t, ValidateRemotePreviewRequest(request))
36+ preview := RemotePreview{
37+ Version: RemotePreviewSchemaVersion, Status: RemotePreviewOK,
38+ LifecycleID: target.LifecycleID, TabID: target.LiveTabID, Revision: 1,
39+ Width: 1, Height: 1, Cells: []renderer.Cell{{Rune: 'x'}},
40+ }
41+ require.NoError(t, ValidateRemotePreview(preview))
42+ preview.Cells[0].Rune = '\n'
43+ require.ErrorIs(t, ValidateRemotePreview(preview), ErrInvalidRemotePreview)
44+}
45+
46+func TestPresentationErrorsPreserveClassification(t *testing.T) {
47+ require.True(t, errors.Is(ValidateAck(Ack{}), ErrInvalidAck))
48+ require.True(t, errors.Is(ValidateParkedRouteRequest(ParkedRouteRequest{}), ErrInvalidNavigation))
49+}
R internal/ports/wire_size.go → internal/protocol/size.go +1 -1 renamed, 98% similar
@@ -1,4 +1,4 @@
1-package ports1+package protocol
2 2
3 import (3 import (
4 "math"4 "math"
M internal/testutil/replaytest/replay.go +4 -3
@@ -9,9 +9,10 @@ import (
9 9
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 )13 )
13 14
14-func mustMarshalOutput(m ports.Output) []byte {15+func mustMarshalOutput(m protocol.Output) []byte {
15 payload, err := ports.MarshalOutput(m)16 payload, err := ports.MarshalOutput(m)
16 if err != nil {17 if err != nil {
17 panic(err)18 panic(err)
@@ -20,8 +21,8 @@ func mustMarshalOutput(m ports.Output) []byte {
20 }21 }
21 22
22 var transcript = []ports.Frame{23 var transcript = []ports.Frame{
23- {Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("\x1b[2J\x1b[Hone\r\ntwo")})},24+ {Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("\x1b[2J\x1b[Hone\r\ntwo")})},
24- {Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{Epoch: 1, Base: 1, New: 2, Echo: 7, Size: domain.Size{Cols: 80, Rows: 24}, Data: []byte("\x1b[2;1HTWO")})},25+ {Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{Epoch: 1, Base: 1, New: 2, Echo: 7, Size: domain.Size{Cols: 80, Rows: 24}, Data: []byte("\x1b[2;1HTWO")})},
25 }26 }
26 27
27 // Transcript returns a deep copy of the canonical replay transcript so an28 // Transcript returns a deep copy of the canonical replay transcript so an
M internal/usecase/client/client.go +67 -66
@@ -23,6 +23,7 @@ import (
23 "github.com/bnema/vev-vt/protocol/terminalquery"23 "github.com/bnema/vev-vt/protocol/terminalquery"
24 "github.com/bnema/vev/internal/domain"24 "github.com/bnema/vev/internal/domain"
25 "github.com/bnema/vev/internal/ports"25 "github.com/bnema/vev/internal/ports"
26+ "github.com/bnema/vev/internal/protocol"
26 "github.com/bnema/vev/internal/usecase/theme"27 "github.com/bnema/vev/internal/usecase/theme"
27 )28 )
28 29
@@ -55,7 +56,7 @@ func (e *DetachedError) Error() string { return "vev: " + e.Text }
55 // without relying on another OSC response.56 // without relying on another OSC response.
56 type terminalThemeState struct {57 type terminalThemeState struct {
57 mu sync.Mutex58 mu sync.Mutex
58- theme ports.Theme59+ theme protocol.Theme
59 }60 }
60 61
61 func (s *terminalThemeState) setTrueColor(enabled bool) {62 func (s *terminalThemeState) setTrueColor(enabled bool) {
@@ -64,14 +65,14 @@ func (s *terminalThemeState) setTrueColor(enabled bool) {
64 s.mu.Unlock()65 s.mu.Unlock()
65 }66 }
66 67
67-func (s *terminalThemeState) update(update func(*ports.Theme)) ports.Theme {68+func (s *terminalThemeState) update(update func(*protocol.Theme)) protocol.Theme {
68 s.mu.Lock()69 s.mu.Lock()
69 defer s.mu.Unlock()70 defer s.mu.Unlock()
70 update(&s.theme)71 update(&s.theme)
71 return s.theme72 return s.theme
72 }73 }
73 74
74-func (s *terminalThemeState) snapshot() ports.Theme {75+func (s *terminalThemeState) snapshot() protocol.Theme {
75 s.mu.Lock()76 s.mu.Lock()
76 defer s.mu.Unlock()77 defer s.mu.Unlock()
77 return s.theme78 return s.theme
@@ -171,7 +172,7 @@ type Dependencies struct {
171 Remote bool172 Remote bool
172 // Origin is explicit composition metadata for the initial route. A zero173 // Origin is explicit composition metadata for the initial route. A zero
173 // value is normalized to local for direct package callers and old tests.174 // value is normalized to local for direct package callers and old tests.
174- Origin ports.RouteOrigin175+ Origin protocol.RouteOrigin
175 OriginKey string176 OriginKey string
176 }177 }
177 178
@@ -182,15 +183,15 @@ type AttachRequest struct {
182 Intent uint8183 Intent uint8
183 SessionName string184 SessionName string
184 Remote bool185 Remote bool
185- Origin ports.RouteOrigin186+ Origin protocol.RouteOrigin
186 OriginKey string187 OriginKey string
187 RemoteTarget *domain.RemoteSessionTarget188 RemoteTarget *domain.RemoteSessionTarget
188- ExactTarget *ports.ExactSessionTarget189+ ExactTarget *protocol.ExactSessionTarget
189 PreferredTabID domain.TabStableID190 PreferredTabID domain.TabStableID
190 HostLabel string191 HostLabel string
191 EnvironmentPolicy ports.EnvironmentPolicy192 EnvironmentPolicy ports.EnvironmentPolicy
192- NavigationCapabilities ports.NavigationCapabilities193+ NavigationCapabilities protocol.NavigationCapabilities
193- StartupOverlay ports.StartupOverlay194+ StartupOverlay protocol.StartupOverlay
194 }195 }
195 196
196 // attachRoute captures the dialer, request, and resume token needed to197 // attachRoute captures the dialer, request, and resume token needed to
@@ -211,9 +212,9 @@ type Runner struct {
211 remoteHostLearner ports.RemoteHostLearner212 remoteHostLearner ports.RemoteHostLearner
212 attachHandoff AttachHandoffFunc213 attachHandoff AttachHandoffFunc
213 remote bool214 remote bool
214- origin ports.RouteOrigin215+ origin protocol.RouteOrigin
215 ledger *routeLedger216 ledger *routeLedger
216- routeFailure *ports.RouteNavigationFailure217+ routeFailure *protocol.RouteNavigationFailure
217 218
218 inputMu sync.Mutex219 inputMu sync.Mutex
219 input *terminalInputPump220 input *terminalInputPump
@@ -360,7 +361,7 @@ func validateAttachRequest(request AttachRequest) error {
360 }361 }
361 }362 }
362 if request.OriginKey != "" {363 if request.OriginKey != "" {
363- if err := ports.ValidateRouteLabel(request.OriginKey, false); err != nil {364+ if err := protocol.ValidateRouteLabel(request.OriginKey, false); err != nil {
364 return fmt.Errorf("vev: invalid route origin key: %w", err)365 return fmt.Errorf("vev: invalid route origin key: %w", err)
365 }366 }
366 }367 }
@@ -378,7 +379,7 @@ func validateAttachRequest(request AttachRequest) error {
378 if err := (SessionTarget{Intent: request.Intent, SessionName: request.SessionName}).validate(); err != nil {379 if err := (SessionTarget{Intent: request.Intent, SessionName: request.SessionName}).validate(); err != nil {
379 return fmt.Errorf("vev: invalid session target: %w", err)380 return fmt.Errorf("vev: invalid session target: %w", err)
380 }381 }
381- if err := ports.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil {382+ if err := protocol.ValidateNavigation(request.NavigationCapabilities, request.StartupOverlay, request.RemoteTarget != nil || request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned); err != nil {
382 return fmt.Errorf("vev: invalid navigation route: %w", err)383 return fmt.Errorf("vev: invalid navigation route: %w", err)
383 }384 }
384 if request.RemoteTarget == nil {385 if request.RemoteTarget == nil {
@@ -452,7 +453,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
452 routeNavigationResumeFallback := false453 routeNavigationResumeFallback := false
453 var routeNavigationSelection *routeNavigationSelection454 var routeNavigationSelection *routeNavigationSelection
454 var routeNavigationFallback *attachRoute455 var routeNavigationFallback *attachRoute
455- var routeNavigationAction *ports.RouteNavigationAction456+ var routeNavigationAction *protocol.RouteNavigationAction
456 backoff := defaultReconnectBackoff.initial457 backoff := defaultReconnectBackoff.initial
457 themeState := &terminalThemeState{}458 themeState := &terminalThemeState{}
458 var rememberOnce sync.Once459 var rememberOnce sync.Once
@@ -513,8 +514,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
513 } else {514 } else {
514 attemptRequest.Intent = ports.IntentAttach515 attemptRequest.Intent = ports.IntentAttach
515 }516 }
516- attemptRequest.StartupOverlay = ports.StartupOverlayNone517+ attemptRequest.StartupOverlay = protocol.StartupOverlayNone
517- attemptRequest.NavigationCapabilities &^= ports.NavigationCapabilityBack | ports.NavigationCapabilityHomePicker518+ attemptRequest.NavigationCapabilities &^= protocol.NavigationCapabilityBack | protocol.NavigationCapabilityHomePicker
518 resumeToken = route.resumeToken519 resumeToken = route.resumeToken
519 returnResumeFallback = route.resumeToken != 0520 returnResumeFallback = route.resumeToken != 0
520 remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote)521 remote = syncReconnectRemote(reconnect, attemptRequest.Remote || r.remote)
@@ -524,8 +525,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
524 dialer = homeRoute.dialer525 dialer = homeRoute.dialer
525 attemptRequest = homeRoute.request526 attemptRequest = homeRoute.request
526 attemptRequest.Intent = ports.IntentAttach527 attemptRequest.Intent = ports.IntentAttach
527- attemptRequest.NavigationCapabilities = ports.NavigationCapabilityBack528+ attemptRequest.NavigationCapabilities = protocol.NavigationCapabilityBack
528- attemptRequest.StartupOverlay = ports.StartupOverlaySessionPicker529+ attemptRequest.StartupOverlay = protocol.StartupOverlaySessionPicker
529 attemptRequest.RemoteTarget = nil530 attemptRequest.RemoteTarget = nil
530 attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned531 attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned
531 attemptRequest.Remote = homeRoute.request.Remote || r.remote532 attemptRequest.Remote = homeRoute.request.Remote || r.remote
@@ -556,8 +557,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
556 }557 }
557 request := cloneAttachRequest(homeRoute.request)558 request := cloneAttachRequest(homeRoute.request)
558 request.Intent = ports.IntentAttach559 request.Intent = ports.IntentAttach
559- request.NavigationCapabilities = ports.NavigationCapabilityBack560+ request.NavigationCapabilities = protocol.NavigationCapabilityBack
560- request.StartupOverlay = ports.StartupOverlaySessionPicker561+ request.StartupOverlay = protocol.StartupOverlaySessionPicker
561 request.RemoteTarget = nil562 request.RemoteTarget = nil
562 request.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned563 request.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned
563 request.Remote = false564 request.Remote = false
@@ -739,10 +740,10 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
739 // Back is transient to the picker overlay. Re-derive the home-picker740 // Back is transient to the picker overlay. Re-derive the home-picker
740 // capability from the selected route instead of trusting request741 // capability from the selected route instead of trusting request
741 // metadata, which may have been cleared after a daemon-side switch.742 // metadata, which may have been cleared after a daemon-side switch.
742- attemptRequest.StartupOverlay = ports.StartupOverlayNone743+ attemptRequest.StartupOverlay = protocol.StartupOverlayNone
743 attemptRequest.NavigationCapabilities = 0744 attemptRequest.NavigationCapabilities = 0
744- if homeRoute != nil && selection.selected.presentation.kind == ports.RouteKindRemote {745+ if homeRoute != nil && selection.selected.presentation.kind == protocol.RouteKindRemote {
745- attemptRequest.NavigationCapabilities = ports.NavigationCapabilityHomePicker746+ attemptRequest.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
746 }747 }
747 resumeToken = selection.selected.resumeToken748 resumeToken = selection.selected.resumeToken
748 routeNavigationPending = true749 routeNavigationPending = true
@@ -754,7 +755,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
754 }755 }
755 if result.action != 0 {756 if result.action != 0 {
756 switch result.action {757 switch result.action {
757- case ports.NavigationOpenHomePicker:758+ case protocol.NavigationOpenHomePicker:
758 // The daemon only sends this action after accepting a Hello that759 // The daemon only sends this action after accepting a Hello that
759 // advertised Home support. The durable client-side prerequisite is760 // advertised Home support. The durable client-side prerequisite is
760 // the captured route itself; request metadata may have been rebased by761 // the captured route itself; request metadata may have been rebased by
@@ -770,7 +771,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
770 homeNavigationPending = true771 homeNavigationPending = true
771 enterHomePicker()772 enterHomePicker()
772 continue773 continue
773- case ports.NavigationBack:774+ case protocol.NavigationBack:
774 // As with Home, the daemon accepted Back in Hello before sending775 // As with Home, the daemon accepted Back in Hello before sending
775 // this action. The retained concrete route is the durable client776 // this action. The retained concrete route is the durable client
776 // prerequisite; request metadata may have been rebased meanwhile.777 // prerequisite; request metadata may have been rebased meanwhile.
@@ -796,7 +797,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
796 }797 }
797 routeRequest.Intent = ports.IntentAttach798 routeRequest.Intent = ports.IntentAttach
798 routeRequest.NavigationCapabilities = 0799 routeRequest.NavigationCapabilities = 0
799- routeRequest.StartupOverlay = ports.StartupOverlayNone800+ routeRequest.StartupOverlay = protocol.StartupOverlayNone
800 homeRoute = &attachRoute{dialer: dialer, request: routeRequest}801 homeRoute = &attachRoute{dialer: dialer, request: routeRequest}
801 }802 }
802 }803 }
@@ -832,9 +833,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
832 nextRequest.ExactTarget = result.target.ExactTarget833 nextRequest.ExactTarget = result.target.ExactTarget
833 }834 }
834 if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned) {835 if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned) {
835- nextRequest.NavigationCapabilities |= ports.NavigationCapabilityHomePicker836+ nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker
836 }837 }
837- if attemptRequest.StartupOverlay == ports.StartupOverlaySessionPicker {838+ if attemptRequest.StartupOverlay == protocol.StartupOverlaySessionPicker {
838 returnRoute = nil839 returnRoute = nil
839 returnResumeFallback = false840 returnResumeFallback = false
840 }841 }
@@ -882,7 +883,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
882 if routeNavigationPending && routeNavigationFallback != nil {883 if routeNavigationPending && routeNavigationFallback != nil {
883 if routeNavigationAction != nil {884 if routeNavigationAction != nil {
884 action := *routeNavigationAction885 action := *routeNavigationAction
885- r.routeFailure = &ports.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: routeFailureCode(result.err)}886+ r.routeFailure = &protocol.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: routeFailureCode(result.err)}
886 routeNavigationAction = nil887 routeNavigationAction = nil
887 }888 }
888 route := *routeNavigationFallback889 route := *routeNavigationFallback
@@ -1084,14 +1085,14 @@ type attachAttempt struct {
1084 type attachResult struct {1085 type attachResult struct {
1085 resumeToken uint641086 resumeToken uint64
1086 sessionName string1087 sessionName string
1087- committedIdentity *ports.CommittedRouteIdentity1088+ committedIdentity *protocol.CommittedRouteIdentity
1088- routePosition *ports.RoutePosition1089+ routePosition *protocol.RoutePosition
1089 welcomed bool1090 welcomed bool
1090 transportClosed bool1091 transportClosed bool
1091 target *ports.AttachTarget1092 target *ports.AttachTarget
1092- action ports.NavigationAction1093+ action protocol.NavigationAction
1093 homePickerTarget bool1094 homePickerTarget bool
1094- routeAction *ports.RouteNavigationAction1095+ routeAction *protocol.RouteNavigationAction
1095 err error1096 err error
1096 }1097 }
1097 1098
@@ -1213,7 +1214,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1213 themeState.setTrueColor(trueColor)1214 themeState.setTrueColor(trueColor)
1214 exactTarget := request.ExactTarget1215 exactTarget := request.ExactTarget
1215 if exactTarget == nil && request.RemoteTarget != nil {1216 if exactTarget == nil && request.RemoteTarget != nil {
1216- exactTarget = &ports.ExactSessionTarget{1217+ exactTarget = &protocol.ExactSessionTarget{
1217 LifecycleID: request.RemoteTarget.LifecycleID,1218 LifecycleID: request.RemoteTarget.LifecycleID,
1218 SessionName: request.RemoteTarget.SessionName,1219 SessionName: request.RemoteTarget.SessionName,
1219 }1220 }
@@ -1257,8 +1258,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1257 }, transition); err != nil {1258 }, transition); err != nil {
1258 return handshakeFailure("awaiting welcome", err)1259 return handshakeFailure("awaiting welcome", err)
1259 }1260 }
1260- var committedIdentity *ports.CommittedRouteIdentity1261+ var committedIdentity *protocol.CommittedRouteIdentity
1261- var routePosition *ports.RoutePosition1262+ var routePosition *protocol.RoutePosition
1262 result := func(welcomed bool, err error) attachResult {1263 result := func(welcomed bool, err error) attachResult {
1263 return attachResult{1264 return attachResult{
1264 resumeToken: resumeToken,1265 resumeToken: resumeToken,
@@ -1391,7 +1392,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1391 delete(timers, id)1392 delete(timers, id)
1392 }1393 }
1393 }1394 }
1394- publish := func(snapshot ports.Theme) error {1395+ publish := func(snapshot protocol.Theme) error {
1395 frame := ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(snapshot)}1396 frame := ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(snapshot)}
1396 // The initial cleared snapshot is sent before the sender exists. This1397 // The initial cleared snapshot is sent before the sender exists. This
1397 // keeps the generation publication ordered before the first query and1398 // keeps the generation publication ordered before the first query and
@@ -1417,7 +1418,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1417 return fmt.Errorf("publishing theme: %w", err)1418 return fmt.Errorf("publishing theme: %w", err)
1418 }1419 }
1419 case paletteActionPublishFinal:1420 case paletteActionPublishFinal:
1420- themeState.update(func(current *ports.Theme) { *current = action.theme })1421+ themeState.update(func(current *protocol.Theme) { *current = action.theme })
1421 if err := publish(action.theme); err != nil {1422 if err := publish(action.theme); err != nil {
1422 return fmt.Errorf("publishing theme: %w", err)1423 return fmt.Errorf("publishing theme: %w", err)
1423 }1424 }
@@ -1487,8 +1488,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1487 var nextSamePeerRequestID uint641488 var nextSamePeerRequestID uint64
1488 type parkedRoutePending struct {1489 type parkedRoutePending struct {
1489 requestID uint641490 requestID uint64
1490- action ports.ParkedRouteAction1491+ action protocol.ParkedRouteAction
1491- leaseID ports.ParkedRouteLeaseID1492+ leaseID protocol.ParkedRouteLeaseID
1492 fallbackTarget *ports.AttachTarget1493 fallbackTarget *ports.AttachTarget
1493 timer ports.Timer1494 timer ports.Timer
1494 }1495 }
@@ -1502,7 +1503,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1502 // Before the asynchronous sender starts, preserve transport ordering by1503 // Before the asynchronous sender starts, preserve transport ordering by
1503 // requesting the reset synchronously after the initial Theme publication.1504 // requesting the reset synchronously after the initial Theme publication.
1504 resetErr := sendHandshake(func() error {1505 resetErr := sendHandshake(func() error {
1505- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1506+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1506 if err != nil {1507 if err != nil {
1507 return err1508 return err
1508 }1509 }
@@ -1628,7 +1629,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1628 return fmt.Errorf("reading terminal geometry for parked route: %w", err)1629 return fmt.Errorf("reading terminal geometry for parked route: %w", err)
1629 }1630 }
1630 geometry = geometry.NormalizePixels()1631 geometry = geometry.NormalizePixels()
1631- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1632+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1632 if err != nil {1633 if err != nil {
1633 return fmt.Errorf("encoding terminal size for parked route: %w", err)1634 return fmt.Errorf("encoding terminal size for parked route: %w", err)
1634 }1635 }
@@ -1639,12 +1640,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1639 return context.Canceled1640 return context.Canceled
1640 }1641 }
1641 }1642 }
1642- sendParkedRouteRequest := func(action ports.ParkedRouteAction, leaseID ports.ParkedRouteLeaseID, target *domain.RemoteSessionTarget) error {1643+ sendParkedRouteRequest := func(action protocol.ParkedRouteAction, leaseID protocol.ParkedRouteLeaseID, target *domain.RemoteSessionTarget) error {
1643 if parkedPending != nil {1644 if parkedPending != nil {
1644 return errors.New("vev: parked-route request already pending")1645 return errors.New("vev: parked-route request already pending")
1645 }1646 }
1646 nextParkedRequestID++1647 nextParkedRequestID++
1647- request := ports.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}1648+ request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}
1648 payload := ports.MarshalParkedRouteRequest(request)1649 payload := ports.MarshalParkedRouteRequest(request)
1649 if payload == nil {1650 if payload == nil {
1650 return errors.New("vev: invalid parked-route request")1651 return errors.New("vev: invalid parked-route request")
@@ -1660,7 +1661,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1660 return context.Canceled1661 return context.Canceled
1661 }1662 }
1662 }1663 }
1663- handleParkedPicker := func(leaseID ports.ParkedRouteLeaseID) (attachResult, bool) {1664+ handleParkedPicker := func(leaseID protocol.ParkedRouteLeaseID) (attachResult, bool) {
1664 pickerCtx, cancelPicker := context.WithCancel(ctx)1665 pickerCtx, cancelPicker := context.WithCancel(ctx)
1665 pickerDone := make(chan struct{})1666 pickerDone := make(chan struct{})
1666 go func() {1667 go func() {
@@ -1687,7 +1688,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1687 if err := sendParkedRouteSize(); err != nil {1688 if err := sendParkedRouteSize(); err != nil {
1688 return welcomedResult(err), true1689 return welcomedResult(err), true
1689 }1690 }
1690- if err := sendParkedRouteRequest(ports.ParkedRouteSwitch, leaseID, &target); err != nil {1691+ if err := sendParkedRouteRequest(protocol.ParkedRouteSwitch, leaseID, &target); err != nil {
1691 return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true1692 return welcomedResult(fmt.Errorf("vev: switching parked route: %w", err)), true
1692 }1693 }
1693 fallback := *selection.target1694 fallback := *selection.target
@@ -1704,7 +1705,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1704 if err := sendParkedRouteSize(); err != nil {1705 if err := sendParkedRouteSize(); err != nil {
1705 return welcomedResult(err), true1706 return welcomedResult(err), true
1706 }1707 }
1707- if err := sendParkedRouteRequest(ports.ParkedRouteResume, leaseID, nil); err != nil {1708+ if err := sendParkedRouteRequest(protocol.ParkedRouteResume, leaseID, nil); err != nil {
1708 return welcomedResult(fmt.Errorf("vev: resuming parked route: %w", err)), true1709 return welcomedResult(fmt.Errorf("vev: resuming parked route: %w", err)), true
1709 }1710 }
1710 return attachResult{}, false1711 return attachResult{}, false
@@ -1725,7 +1726,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1725 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)1726 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)
1726 }1727 }
1727 geometry = geometry.NormalizePixels()1728 geometry = geometry.NormalizePixels()
1728- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1729+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1729 if err != nil {1730 if err != nil {
1730 return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err)1731 return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err)
1731 }1732 }
@@ -1761,8 +1762,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1761 }1762 }
1762 return nil1763 return nil
1763 }1764 }
1764- sendRouteFailure := func(action ports.RouteNavigationAction, code ports.RouteFailureCode) error {1765+ sendRouteFailure := func(action protocol.RouteNavigationAction, code protocol.RouteFailureCode) error {
1765- payload, err := ports.MarshalRouteNavigationFailure(ports.RouteNavigationFailure{1766+ payload, err := ports.MarshalRouteNavigationFailure(protocol.RouteNavigationFailure{
1766 Key: action.Key,1767 Key: action.Key,
1767 Generation: action.Generation,1768 Generation: action.Generation,
1768 Code: code,1769 Code: code,
@@ -1861,7 +1862,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1861 }1862 }
1862 case event := <-paletteEvents:1863 case event := <-paletteEvents:
1863 if event.kind == paletteEventScheme {1864 if event.kind == paletteEventScheme {
1864- retained := themeState.update(func(current *ports.Theme) {1865+ retained := themeState.update(func(current *protocol.Theme) {
1865 current.SchemeKnown = true1866 current.SchemeKnown = true
1866 current.Light = event.light1867 current.Light = event.light
1867 })1868 })
@@ -1965,7 +1966,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1965 }1966 }
1966 nextSamePeerRequestID++1967 nextSamePeerRequestID++
1967 request := a.runner.ledger.samePeerHandoff(request, target)1968 request := a.runner.ledger.samePeerHandoff(request, target)
1968- payload, err := ports.MarshalSamePeerSwitchRequest(ports.SamePeerSwitchRequest{1969+ payload, err := ports.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{
1969 RequestID: nextSamePeerRequestID,1970 RequestID: nextSamePeerRequestID,
1970 Target: *target.ExactTarget,1971 Target: *target.ExactTarget,
1971 PreferredTabID: request.PreferredTabID,1972 PreferredTabID: request.PreferredTabID,
@@ -1996,26 +1997,26 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1996 pending := *parkedPending1997 pending := *parkedPending
1997 clearParkedPending()1998 clearParkedPending()
1998 switch pending.action {1999 switch pending.action {
1999- case ports.ParkedRoutePrepare:2000+ case protocol.ParkedRoutePrepare:
2000- if response.Status != ports.ParkedRouteReady {2001+ if response.Status != protocol.ParkedRouteReady {
2001 fallback := welcomedResult(nil)2002 fallback := welcomedResult(nil)
2002- fallback.action = ports.NavigationOpenHomePicker2003+ fallback.action = protocol.NavigationOpenHomePicker
2003 return fallback2004 return fallback
2004 }2005 }
2005 if selection, done := handleParkedPicker(pending.leaseID); done {2006 if selection, done := handleParkedPicker(pending.leaseID); done {
2006 return selection2007 return selection
2007 }2008 }
2008- case ports.ParkedRouteResume:2009+ case protocol.ParkedRouteResume:
2009- if response.Status != ports.ParkedRouteResumed {2010+ if response.Status != protocol.ParkedRouteResumed {
2010 return welcomedResult(errors.New("vev: parked route could not resume"))2011 return welcomedResult(errors.New("vev: parked route could not resume"))
2011 }2012 }
2012 awaitParkedFull()2013 awaitParkedFull()
2013- case ports.ParkedRouteSwitch:2014+ case protocol.ParkedRouteSwitch:
2014 switch response.Status {2015 switch response.Status {
2015- case ports.ParkedRouteSwitched:2016+ case protocol.ParkedRouteSwitched:
2016 awaitParkedFull()2017 awaitParkedFull()
2017 continue2018 continue
2018- case ports.ParkedRouteStaleTarget:2019+ case protocol.ParkedRouteStaleTarget:
2019 if transition != nil {2020 if transition != nil {
2020 transition.stop()2021 transition.stop()
2021 }2022 }
@@ -2038,12 +2039,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2038 return welcomedResult(fmt.Errorf("vev: decoding navigation action: %w", derr))2039 return welcomedResult(fmt.Errorf("vev: decoding navigation action: %w", derr))
2039 }2040 }
2040 _, datagramRoute := transport.(ports.DatagramTransport)2041 _, datagramRoute := transport.(ports.DatagramTransport)
2041- if directive.Action == ports.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {2042+ if directive.Action == protocol.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {
2042 stopForeground()2043 stopForeground()
2043 if err := flushSender(); err != nil {2044 if err := flushSender(); err != nil {
2044 return welcomedResult(fmt.Errorf("vev: parking remote foreground: %w", err))2045 return welcomedResult(fmt.Errorf("vev: parking remote foreground: %w", err))
2045 }2046 }
2046- if err := sendParkedRouteRequest(ports.ParkedRoutePrepare, directive.LeaseID, nil); err != nil {2047+ if err := sendParkedRouteRequest(protocol.ParkedRoutePrepare, directive.LeaseID, nil); err != nil {
2047 return welcomedResult(fmt.Errorf("vev: preparing parked route: %w", err))2048 return welcomedResult(fmt.Errorf("vev: preparing parked route: %w", err))
2048 }2049 }
2049 continue2050 continue
@@ -2057,14 +2058,14 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2057 return welcomedResult(fmt.Errorf("vev: decoding route navigation action: %w", derr))2058 return welcomedResult(fmt.Errorf("vev: decoding route navigation action: %w", derr))
2058 }2059 }
2059 if a.runner.ledger == nil {2060 if a.runner.ledger == nil {
2060- if ferr := sendRouteFailure(action, ports.RouteFailureUnavailable); ferr != nil {2061+ if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {
2061 return welcomedResult(ferr)2062 return welcomedResult(ferr)
2062 }2063 }
2063 continue2064 continue
2064 }2065 }
2065 _, valid, noOp := a.runner.ledger.navigationRecord(action)2066 _, valid, noOp := a.runner.ledger.navigationRecord(action)
2066 if !valid {2067 if !valid {
2067- if ferr := sendRouteFailure(action, ports.RouteFailureStaleSelection); ferr != nil {2068+ if ferr := sendRouteFailure(action, protocol.RouteFailureStaleSelection); ferr != nil {
2068 return welcomedResult(ferr)2069 return welcomedResult(ferr)
2069 }2070 }
2070 continue2071 continue
@@ -2244,7 +2245,7 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
2244 if state == 0 {2245 if state == 0 {
2245 return true2246 return true
2246 }2247 }
2247- payload, err := ports.MarshalAck(ports.Ack{Epoch: epoch, State: state})2248+ payload, err := ports.MarshalAck(protocol.Ack{Epoch: epoch, State: state})
2248 if err != nil {2249 if err != nil {
2249 select {2250 select {
2250 case errCh <- fmt.Errorf("encoding output ACK: %w", err):2251 case errCh <- fmt.Errorf("encoding output ACK: %w", err):
@@ -2802,7 +2803,7 @@ func (p *stdinPump) run() {
2802 },2803 },
2803 sendImage: func(mime string, data []byte) {2804 sendImage: func(mime string, data []byte) {
2804 inputSeq++2805 inputSeq++
2805- send(ports.Frame{Type: ports.MsgImagePush, Payload: ports.MarshalImagePush(ports.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})2806+ send(ports.Frame{Type: ports.MsgImagePush, Payload: ports.MarshalImagePush(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})
2806 },2807 },
2807 next: coalescer.Scan,2808 next: coalescer.Scan,
2808 }2809 }
@@ -2974,7 +2975,7 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- po
2974 return2975 return
2975 }2976 }
2976 geometry = geometry.NormalizePixels()2977 geometry = geometry.NormalizePixels()
2977- payload, err := ports.MarshalResize(ports.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})2978+ payload, err := ports.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
2978 if err != nil {2979 if err != nil {
2979 log.Error("encoding terminal resize", "error", err)2980 log.Error("encoding terminal resize", "error", err)
2980 continue2981 continue
M internal/usecase/client/client_test.go +100 -99
@@ -18,6 +18,7 @@ import (
18 "github.com/bnema/vev/internal/domain"18 "github.com/bnema/vev/internal/domain"
19 "github.com/bnema/vev/internal/ports"19 "github.com/bnema/vev/internal/ports"
20 portsmocks "github.com/bnema/vev/internal/ports/mocks"20 portsmocks "github.com/bnema/vev/internal/ports/mocks"
21+ "github.com/bnema/vev/internal/protocol"
21 "github.com/bnema/vev/internal/usecase/client"22 "github.com/bnema/vev/internal/usecase/client"
22 )23 )
23 24
@@ -436,8 +437,8 @@ func TestAttachPublishesCommittedRouteSnapshot(t *testing.T) {
436 {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{437 {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
437 SessionID: "s1",438 SessionID: "s1",
438 SessionName: "main",439 SessionName: "main",
439- CommittedIdentity: &ports.CommittedRouteIdentity{440+ CommittedIdentity: &protocol.CommittedRouteIdentity{
440- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "main"},441+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "main"},
441 },442 },
442 }))},443 }))},
443 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},444 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
@@ -446,13 +447,13 @@ func TestAttachPublishesCommittedRouteSnapshot(t *testing.T) {
446 err := runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), client.AttachRequest{447 err := runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), client.AttachRequest{
447 Intent: ports.IntentAttach,448 Intent: ports.IntentAttach,
448 SessionName: "main",449 SessionName: "main",
449- Origin: ports.RouteOriginLocal,450+ Origin: protocol.RouteOriginLocal,
450 OriginKey: "local",451 OriginKey: "local",
451 })452 })
452 require.NoError(t, err)453 require.NoError(t, err)
453 454
454- var snapshot ports.RecentRouteSnapshot455+ var snapshot protocol.RecentRouteSnapshot
455- var subscription ports.RouteAttentionSubscription456+ var subscription protocol.RouteAttentionSubscription
456 foundSnapshot := false457 foundSnapshot := false
457 foundSubscription := false458 foundSubscription := false
458 for _, frame := range tr.Sends() {459 for _, frame := range tr.Sends() {
@@ -486,26 +487,26 @@ func TestLocalOnlyHandoffBetweenLocalSessionsKeepsClientRunning(t *testing.T) {
486 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {487 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
487 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{488 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
488 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,489 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
489- CommittedIdentity: &ports.CommittedRouteIdentity{490+ CommittedIdentity: &protocol.CommittedRouteIdentity{
490- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},491+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
491 },492 },
492 }))493 }))
493 }494 }
494- secondTarget := ports.ExactSessionTarget{LifecycleID: secondLifecycle, SessionName: "second"}495+ secondTarget := protocol.ExactSessionTarget{LifecycleID: secondLifecycle, SessionName: "second"}
495 first := &recordingTransport{recvs: []recvItem{496 first := &recordingTransport{recvs: []recvItem{
496 {f: welcome("first", firstLifecycle)},497 {f: welcome("first", firstLifecycle)},
497 {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{498 {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{
498 Session: "second", Intent: ports.IntentAttach, ExactTarget: &secondTarget,499 Session: "second", Intent: ports.IntentAttach, ExactTarget: &secondTarget,
499 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, SamePeer: true,500 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, SamePeer: true,
500 }))},501 }))},
501- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{Target: secondTarget}))},502+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: secondTarget}))},
502 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},503 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
503 }}504 }}
504 dialer := &sequenceDialer{trs: []ports.Transport{first}}505 dialer := &sequenceDialer{trs: []ports.Transport{first}}
505 506
506 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{507 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{
507 Intent: ports.IntentAttach, SessionName: "first",508 Intent: ports.IntentAttach, SessionName: "first",
508- Origin: ports.RouteOriginLocal, OriginKey: "local",509+ Origin: protocol.RouteOriginLocal, OriginKey: "local",
509 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,510 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
510 })511 })
511 512
@@ -524,8 +525,8 @@ func TestAttachHelloPreservesCompleteAttachRequest(t *testing.T) {
524 request := client.AttachRequest{525 request := client.AttachRequest{
525 Intent: ports.IntentAttach, SessionName: "work", Remote: true,526 Intent: ports.IntentAttach, SessionName: "work", Remote: true,
526 RemoteTarget: target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,527 RemoteTarget: target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
527- NavigationCapabilities: ports.NavigationCapabilityHomePicker,528+ NavigationCapabilities: protocol.NavigationCapabilityHomePicker,
528- StartupOverlay: ports.StartupOverlayNone,529+ StartupOverlay: protocol.StartupOverlayNone,
529 }530 }
530 tr := &recordingTransport{recvs: []recvItem{531 tr := &recordingTransport{recvs: []recvItem{
531 {f: welcomeFrame(0)},532 {f: welcomeFrame(0)},
@@ -548,12 +549,12 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
548 549
549 sourceLifecycle := domain.SessionLifecycleID{1}550 sourceLifecycle := domain.SessionLifecycleID{1}
550 targetLifecycle := domain.SessionLifecycleID{2}551 targetLifecycle := domain.SessionLifecycleID{2}
551- target := ports.ExactSessionTarget{LifecycleID: targetLifecycle, SessionName: "stopped"}552+ target := protocol.ExactSessionTarget{LifecycleID: targetLifecycle, SessionName: "stopped"}
552 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {553 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
553 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{554 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
554 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,555 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
555- CommittedIdentity: &ports.CommittedRouteIdentity{556+ CommittedIdentity: &protocol.CommittedRouteIdentity{
556- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},557+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
557 },558 },
558 }))559 }))
559 }560 }
@@ -572,7 +573,7 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
572 573
573 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{574 err := runTestClient(context.Background(), testDependencies(dialer, term, realClock{}, nil, nil), client.AttachRequest{
574 Intent: ports.IntentAttach, SessionName: "source",575 Intent: ports.IntentAttach, SessionName: "source",
575- Origin: ports.RouteOriginLocal, OriginKey: "local",576+ Origin: protocol.RouteOriginLocal, OriginKey: "local",
576 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,577 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
577 })578 })
578 579
@@ -589,7 +590,7 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
589 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {590 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
590 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{591 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
591 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,592 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
592- CommittedIdentity: &ports.CommittedRouteIdentity{Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},593+ CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
593 }))594 }))
594 }595 }
595 remoteTarget := domain.RemoteSessionTarget{596 remoteTarget := domain.RemoteSessionTarget{
@@ -605,7 +606,7 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
605 }}606 }}
606 remote := &recordingTransport{recvs: []recvItem{607 remote := &recordingTransport{recvs: []recvItem{
607 {f: welcome("work", remoteLifecycle)},608 {f: welcome("work", remoteLifecycle)},
608- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},609+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
609 }}610 }}
610 local2 := &recordingTransport{recvs: []recvItem{611 local2 := &recordingTransport{recvs: []recvItem{
611 {f: welcome("local", localLifecycle)},612 {f: welcome("local", localLifecycle)},
@@ -615,10 +616,10 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
615 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}616 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}
616 deps := testDependencies(localDialer, term, realClock{}, nil, nil)617 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
617 deps.AttachHandoff = func(ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {618 deps.AttachHandoff = func(ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
618- target := ports.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}619+ target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}
619 return remoteDialer, client.AttachRequest{620 return remoteDialer, client.AttachRequest{
620 Intent: ports.IntentAttach, SessionName: "work", Remote: true,621 Intent: ports.IntentAttach, SessionName: "work", Remote: true,
621- Origin: ports.RouteOriginRemote, OriginKey: "remote", ExactTarget: &target,622+ Origin: protocol.RouteOriginRemote, OriginKey: "remote", ExactTarget: &target,
622 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,623 EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
623 // Deliberately omit the copied Home capability. The accepted server624 // Deliberately omit the copied Home capability. The accepted server
624 // action remains valid because Runner retained the concrete home route.625 // action remains valid because Runner retained the concrete home route.
@@ -626,15 +627,15 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
626 }627 }
627 628
628 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{629 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
629- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",630+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
630 }))631 }))
631- require.Equal(t, ports.StartupOverlaySessionPicker, helloFromSend(t, local2).StartupOverlay)632+ require.Equal(t, protocol.StartupOverlaySessionPicker, helloFromSend(t, local2).StartupOverlay)
632 }633 }
633 634
634 func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.Frame {635 func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
635 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{636 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
636 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,637 SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,
637- CommittedIdentity: &ports.CommittedRouteIdentity{Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},638+ CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
638 }))639 }))
639 }640 }
640 641
@@ -661,15 +662,15 @@ func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clo
661 selection := *target.RemoteTarget662 selection := *target.RemoteTarget
662 return dialer, client.AttachRequest{663 return dialer, client.AttachRequest{
663 Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,664 Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,
664- Origin: ports.RouteOriginDiscovery, OriginKey: target.Endpoint,665+ Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
665 RemoteTarget: &selection, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,666 RemoteTarget: &selection, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
666 }, nil667 }, nil
667 }668 }
668 return deps669 return deps
669 }670 }
670 671
671-func hybridParkedRequestHandler(requests chan<- ports.ParkedRouteRequest, requestErrors chan<- error, signals map[ports.ParkedRouteAction]chan struct{}) func(ports.Frame) {672+func hybridParkedRequestHandler(requests chan<- protocol.ParkedRouteRequest, requestErrors chan<- error, signals map[protocol.ParkedRouteAction]chan struct{}) func(ports.Frame) {
672- once := make(map[ports.ParkedRouteAction]*sync.Once, len(signals))673+ once := make(map[protocol.ParkedRouteAction]*sync.Once, len(signals))
673 for action := range signals {674 for action := range signals {
674 once[action] = &sync.Once{}675 once[action] = &sync.Once{}
675 }676 }
@@ -711,20 +712,20 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
711 localInitial := hybridLocalBootstrap(localLifecycle, sourceTarget)712 localInitial := hybridLocalBootstrap(localLifecycle, sourceTarget)
712 parkSent := make(chan struct{})713 parkSent := make(chan struct{})
713 switchSent := make(chan struct{})714 switchSent := make(chan struct{})
714- parkedRequests := make(chan ports.ParkedRouteRequest, 2)715+ parkedRequests := make(chan protocol.ParkedRouteRequest, 2)
715 parkedRequestErrors := make(chan error, 2)716 parkedRequestErrors := make(chan error, 2)
716 remote := &recordingTransport{recvs: []recvItem{717 remote := &recordingTransport{recvs: []recvItem{
717 {f: hybridWelcomeFrame("source", remoteSourceLifecycle)},718 {f: hybridWelcomeFrame("source", remoteSourceLifecycle)},
718- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},719+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
719- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: parkSent},720+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
720- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{Target: ports.ExactSessionTarget{LifecycleID: remoteTargetLifecycle, SessionName: "target"}})), wait: switchSent},721+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: remoteTargetLifecycle, SessionName: "target"}})), wait: switchSent},
721- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteSwitched}))},722+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteSwitched}))},
722- {f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 2, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("target frame")}))},723+ {f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 2, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("target frame")}))},
723 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},724 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
724 }}725 }}
725- remote.onSend = hybridParkedRequestHandler(parkedRequests, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{726+ remote.onSend = hybridParkedRequestHandler(parkedRequests, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
726- ports.ParkedRoutePrepare: parkSent,727+ protocol.ParkedRoutePrepare: parkSent,
727- ports.ParkedRouteSwitch: switchSent,728+ protocol.ParkedRouteSwitch: switchSent,
728 })729 })
729 localPicker := &recordingTransport{recvs: []recvItem{730 localPicker := &recordingTransport{recvs: []recvItem{
730 {f: hybridWelcomeFrame("local", localLifecycle)},731 {f: hybridWelcomeFrame("local", localLifecycle)},
@@ -744,7 +745,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
744 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})745 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
745 746
746 err := runTestClient(context.Background(), deps, client.AttachRequest{747 err := runTestClient(context.Background(), deps, client.AttachRequest{
747- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",748+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
748 })749 })
749 750
750 require.NoError(t, err)751 require.NoError(t, err)
@@ -755,8 +756,8 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
755 }756 }
756 prepareRequest := <-parkedRequests757 prepareRequest := <-parkedRequests
757 switchRequest := <-parkedRequests758 switchRequest := <-parkedRequests
758- require.Equal(t, ports.ParkedRoutePrepare, prepareRequest.Action)759+ require.Equal(t, protocol.ParkedRoutePrepare, prepareRequest.Action)
759- require.Equal(t, ports.ParkedRouteSwitch, switchRequest.Action)760+ require.Equal(t, protocol.ParkedRouteSwitch, switchRequest.Action)
760 require.NotNil(t, switchRequest.Target)761 require.NotNil(t, switchRequest.Target)
761 require.Equal(t, targetTarget, *switchRequest.Target)762 require.Equal(t, targetTarget, *switchRequest.Target)
762 frames := remote.Sends()763 frames := remote.Sends()
@@ -767,7 +768,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
767 }768 }
768 request, requestErr := ports.UnmarshalParkedRouteRequest(frame.Payload)769 request, requestErr := ports.UnmarshalParkedRouteRequest(frame.Payload)
769 require.NoError(t, requestErr)770 require.NoError(t, requestErr)
770- if request.Action == ports.ParkedRouteSwitch {771+ if request.Action == protocol.ParkedRouteSwitch {
771 switchIndex = i772 switchIndex = i
772 break773 break
773 }774 }
@@ -801,13 +802,13 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
801 parkedRequestErrors := make(chan error, 1)802 parkedRequestErrors := make(chan error, 1)
802 sourceRemote := &recordingTransport{recvs: []recvItem{803 sourceRemote := &recordingTransport{recvs: []recvItem{
803 {f: hybridWelcomeFrame("source", sourceLifecycle)},804 {f: hybridWelcomeFrame("source", sourceLifecycle)},
804- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},805+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
805- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: prepareSent},806+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: prepareSent},
806- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteExpired})), wait: switchSent},807+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteExpired})), wait: switchSent},
807 }}808 }}
808- sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{809+ sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
809- ports.ParkedRoutePrepare: prepareSent,810+ protocol.ParkedRoutePrepare: prepareSent,
810- ports.ParkedRouteSwitch: switchSent,811+ protocol.ParkedRouteSwitch: switchSent,
811 })812 })
812 localPicker := &recordingTransport{recvs: []recvItem{813 localPicker := &recordingTransport{recvs: []recvItem{
813 {f: hybridWelcomeFrame("local", localLifecycle)},814 {f: hybridWelcomeFrame("local", localLifecycle)},
@@ -827,7 +828,7 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
827 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})828 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
828 829
829 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{830 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
830- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",831+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
831 }))832 }))
832 select {833 select {
833 case requestErr := <-parkedRequestErrors:834 case requestErr := <-parkedRequestErrors:
@@ -878,12 +879,12 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
878 remote := &recordingTransport{879 remote := &recordingTransport{
879 recvs: []recvItem{880 recvs: []recvItem{
880 {f: hybridWelcomeFrame("work", remoteLifecycle)},881 {f: hybridWelcomeFrame("work", remoteLifecycle)},
881- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},882+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
882 },883 },
883 stall: remoteClosed,884 stall: remoteClosed,
884 }885 }
885- remote.onSend = hybridParkedRequestHandler(nil, nil, map[ports.ParkedRouteAction]chan struct{}{886+ remote.onSend = hybridParkedRequestHandler(nil, nil, map[protocol.ParkedRouteAction]chan struct{}{
886- ports.ParkedRoutePrepare: prepareSent,887+ protocol.ParkedRoutePrepare: prepareSent,
887 })888 })
888 889
889 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial}}890 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial}}
@@ -899,7 +900,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
899 result := make(chan error, 1)900 result := make(chan error, 1)
900 go func() {901 go func() {
901 result <- runTestClient(ctx, deps, client.AttachRequest{902 result <- runTestClient(ctx, deps, client.AttachRequest{
902- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",903+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
903 })904 })
904 }()905 }()
905 select {906 select {
@@ -949,26 +950,26 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
949 parkedRequestErrors := make(chan error, 2)950 parkedRequestErrors := make(chan error, 2)
950 remote := &recordingTransport{recvs: []recvItem{951 remote := &recordingTransport{recvs: []recvItem{
951 {f: hybridWelcomeFrame("work", remoteLifecycle)},952 {f: hybridWelcomeFrame("work", remoteLifecycle)},
952- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},953+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
953- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: parkSent},954+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
954- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteResumed})), wait: resumeSent},955+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed})), wait: resumeSent},
955- {f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("source frame")}))},956+ {f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("source frame")}))},
956 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},957 {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
957 }}958 }}
958- remote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{959+ remote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
959- ports.ParkedRoutePrepare: parkSent,960+ protocol.ParkedRoutePrepare: parkSent,
960- ports.ParkedRouteResume: resumeSent,961+ protocol.ParkedRouteResume: resumeSent,
961 })962 })
962 localPicker := &recordingTransport{recvs: []recvItem{963 localPicker := &recordingTransport{recvs: []recvItem{
963 {f: hybridWelcomeFrame("local", localLifecycle)},964 {f: hybridWelcomeFrame("local", localLifecycle)},
964- {f: navigationDirectiveFrame(ports.NavigationBack)},965+ {f: navigationDirectiveFrame(protocol.NavigationBack)},
965 }}966 }}
966 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}967 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
967 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}968 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
968 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})969 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
969 970
970 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{971 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
971- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",972+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
972 }))973 }))
973 select {974 select {
974 case requestErr := <-parkedRequestErrors:975 case requestErr := <-parkedRequestErrors:
@@ -1000,12 +1001,12 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
1000 defer close(keepSourceOpen)1001 defer close(keepSourceOpen)
1001 sourceRemote := &recordingTransport{recvs: []recvItem{1002 sourceRemote := &recordingTransport{recvs: []recvItem{
1002 {f: hybridWelcomeFrame("source", sourceLifecycle)},1003 {f: hybridWelcomeFrame("source", sourceLifecycle)},
1003- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},1004+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
1004- {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})), wait: parkSent},1005+ {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
1005 {err: io.EOF, wait: keepSourceOpen},1006 {err: io.EOF, wait: keepSourceOpen},
1006 }}1007 }}
1007- sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[ports.ParkedRouteAction]chan struct{}{1008+ sourceRemote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
1008- ports.ParkedRoutePrepare: parkSent,1009+ protocol.ParkedRoutePrepare: parkSent,
1009 })1010 })
1010 localPicker := &recordingTransport{recvs: []recvItem{1011 localPicker := &recordingTransport{recvs: []recvItem{
1011 {f: hybridWelcomeFrame("local", localLifecycle)},1012 {f: hybridWelcomeFrame("local", localLifecycle)},
@@ -1027,7 +1028,7 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
1027 })1028 })
1028 1029
1029 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{1030 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
1030- Intent: ports.IntentAttach, SessionName: "local", Origin: ports.RouteOriginLocal, OriginKey: "local",1031+ Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1031 }))1032 }))
1032 select {1033 select {
1033 case requestErr := <-parkedRequestErrors:1034 case requestErr := <-parkedRequestErrors:
@@ -1053,8 +1054,8 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
1053 SessionName: name,1054 SessionName: name,
1054 ResumeToken: token,1055 ResumeToken: token,
1055 Capabilities: ports.CapabilityResume,1056 Capabilities: ports.CapabilityResume,
1056- CommittedIdentity: &ports.CommittedRouteIdentity{1057+ CommittedIdentity: &protocol.CommittedRouteIdentity{
1057- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},1058+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
1058 },1059 },
1059 }))1060 }))
1060 }1061 }
@@ -1076,28 +1077,28 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
1076 }}1077 }}
1077 local2 := &recordingTransport{recvs: []recvItem{1078 local2 := &recordingTransport{recvs: []recvItem{
1078 {f: welcome("test", localLifecycle, 11)},1079 {f: welcome("test", localLifecycle, 11)},
1079- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{1080+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
1080- Target: ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},1081+ Target: protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},
1081 }))},1082 }))},
1082- {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 2}))},1083+ {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(protocol.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 2}))},
1083 }}1084 }}
1084 local3 := &recordingTransport{recvs: []recvItem{1085 local3 := &recordingTransport{recvs: []recvItem{
1085 {f: welcome("test", localLifecycle, 11)},1086 {f: welcome("test", localLifecycle, 11)},
1086- {f: navigationDirectiveFrame(ports.NavigationBack)},1087+ {f: navigationDirectiveFrame(protocol.NavigationBack)},
1087 }}1088 }}
1088 remote1 := &recordingTransport{recvs: []recvItem{1089 remote1 := &recordingTransport{recvs: []recvItem{
1089 {f: welcome("remote-manual", remoteLifecycle, 22)},1090 {f: welcome("remote-manual", remoteLifecycle, 22)},
1090- {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}))},1091+ {f: frameOf(ports.MsgNavigateRecentRoute, mustMarshalRouteAction(protocol.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}))},
1091 }}1092 }}
1092 remote2 := &recordingTransport{recvs: []recvItem{1093 remote2 := &recordingTransport{recvs: []recvItem{
1093 {f: welcome("remote-manual", remoteLifecycle, 22)},1094 {f: welcome("remote-manual", remoteLifecycle, 22)},
1094- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{1095+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
1095- Target: ports.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"},1096+ Target: protocol.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"},
1096 }))},1097 }))},
1097- {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(ports.RoutePosition{1098+ {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(protocol.RoutePosition{
1098- Target: ports.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, ActiveTabID: "tab-2",1099+ Target: protocol.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, ActiveTabID: "tab-2",
1099 }))},1100 }))},
1100- {f: navigationDirectiveFrame(ports.NavigationOpenHomePicker)},1101+ {f: navigationDirectiveFrame(protocol.NavigationOpenHomePicker)},
1101 }}1102 }}
1102 remote3 := &recordingTransport{recvs: []recvItem{1103 remote3 := &recordingTransport{recvs: []recvItem{
1103 {f: welcome("remote-new", remoteNewLifecycle, 22)},1104 {f: welcome("remote-new", remoteNewLifecycle, 22)},
@@ -1111,32 +1112,32 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
1111 require.Equal(t, remoteHandoff, target)1112 require.Equal(t, remoteHandoff, target)
1112 return remoteDialer, client.AttachRequest{1113 return remoteDialer, client.AttachRequest{
1113 Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,1114 Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,
1114- Origin: ports.RouteOriginDiscovery, OriginKey: target.Endpoint,1115+ Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
1115 RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,1116 RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
1116 }, nil1117 }, nil
1117 }1118 }
1118 1119
1119 err := runTestClient(context.Background(), deps, client.AttachRequest{1120 err := runTestClient(context.Background(), deps, client.AttachRequest{
1120- Intent: ports.IntentAttach, SessionName: "test", Origin: ports.RouteOriginLocal, OriginKey: "local",1121+ Intent: ports.IntentAttach, SessionName: "test", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1121 })1122 })
1122 remoteHello := helloFromSend(t, remote2)1123 remoteHello := helloFromSend(t, remote2)
1123 require.NoError(t, err)1124 require.NoError(t, err)
1124 1125
1125- require.Equal(t, ports.NavigationCapabilityHomePicker, remoteHello.NavigationCapabilities)1126+ require.Equal(t, protocol.NavigationCapabilityHomePicker, remoteHello.NavigationCapabilities)
1126 remoteNewHello := helloFromSend(t, remote3)1127 remoteNewHello := helloFromSend(t, remote3)
1127 require.Equal(t, "remote-new", remoteNewHello.Name)1128 require.Equal(t, "remote-new", remoteNewHello.Name)
1128- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, remoteNewHello.ExactTarget)1129+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: remoteNewLifecycle, SessionName: "remote-new"}, remoteNewHello.ExactTarget)
1129 }1130 }
1130 1131
1131-func navigationDirectiveFrame(action ports.NavigationAction) ports.Frame {1132+func navigationDirectiveFrame(action protocol.NavigationAction) ports.Frame {
1132- directive := ports.NavigationDirective{Action: action}1133+ directive := protocol.NavigationDirective{Action: action}
1133- if action == ports.NavigationOpenHomePicker {1134+ if action == protocol.NavigationOpenHomePicker {
1134- directive.LeaseID = ports.ParkedRouteLeaseID{1}1135+ directive.LeaseID = protocol.ParkedRouteLeaseID{1}
1135 }1136 }
1136 return frameOf(ports.MsgNavigationAction, ports.MarshalNavigationDirective(directive))1137 return frameOf(ports.MsgNavigationAction, ports.MarshalNavigationDirective(directive))
1137 }1138 }
1138 1139
1139-func mustMarshalCommittedIdentity(identity ports.CommittedRouteIdentity) []byte {1140+func mustMarshalCommittedIdentity(identity protocol.CommittedRouteIdentity) []byte {
1140 payload, err := ports.MarshalCommittedRouteIdentity(identity)1141 payload, err := ports.MarshalCommittedRouteIdentity(identity)
1141 if err != nil {1142 if err != nil {
1142 panic(err)1143 panic(err)
@@ -1144,7 +1145,7 @@ func mustMarshalCommittedIdentity(identity ports.CommittedRouteIdentity) []byte
1144 return payload1145 return payload
1145 }1146 }
1146 1147
1147-func mustMarshalRouteAction(action ports.RouteNavigationAction) []byte {1148+func mustMarshalRouteAction(action protocol.RouteNavigationAction) []byte {
1148 payload, err := ports.MarshalRouteNavigationAction(action)1149 payload, err := ports.MarshalRouteNavigationAction(action)
1149 if err != nil {1150 if err != nil {
1150 panic(err)1151 panic(err)
@@ -1152,7 +1153,7 @@ func mustMarshalRouteAction(action ports.RouteNavigationAction) []byte {
1152 return payload1153 return payload
1153 }1154 }
1154 1155
1155-func mustMarshalRoutePosition(position ports.RoutePosition) []byte {1156+func mustMarshalRoutePosition(position protocol.RoutePosition) []byte {
1156 payload, err := ports.MarshalRoutePosition(position)1157 payload, err := ports.MarshalRoutePosition(position)
1157 if err != nil {1158 if err != nil {
1158 panic(err)1159 panic(err)
@@ -1256,7 +1257,7 @@ func TestAttachHappyPath(t *testing.T) {
1256 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1257 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1257 unblock := scriptRecv(tr,1258 unblock := scriptRecv(tr,
1258 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"}))},1259 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"}))},
1259- recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("hello world")}))},1260+ recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("hello world")}))},
1260 recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1261 recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
1261 )1262 )
1262 defer unblock()1263 defer unblock()
@@ -1476,7 +1477,7 @@ func TestAttachForwardsResize(t *testing.T) {
1476 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()1477 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
1477 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1478 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1478 // The resize frame is forwarded via the sender goroutine.1479 // The resize frame is forwarded via the sender goroutine.
1479- gotResize := make(chan ports.Resize, 1)1480+ gotResize := make(chan protocol.Resize, 1)
1480 tr.EXPECT().Send(isType(ports.MsgResize)).RunAndReturn(func(f ports.Frame) error {1481 tr.EXPECT().Send(isType(ports.MsgResize)).RunAndReturn(func(f ports.Frame) error {
1481 r, _ := ports.UnmarshalResize(f.Payload)1482 r, _ := ports.UnmarshalResize(f.Payload)
1482 gotResize <- r1483 gotResize <- r
@@ -1739,16 +1740,16 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
1739 term := newRunTerminal()1740 term := newRunTerminal()
1740 defer term.in.unblock()1741 defer term.in.unblock()
1741 lifecycle := domain.SessionLifecycleID{7, 8, 9}1742 lifecycle := domain.SessionLifecycleID{7, 8, 9}
1742- target := ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}1743+ target := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
1743 welcome := func(token uint64) ports.Frame {1744 welcome := func(token uint64) ports.Frame {
1744 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{1745 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
1745 SessionID: "work", SessionName: "work", ResumeToken: token, Capabilities: ports.CapabilityResume,1746 SessionID: "work", SessionName: "work", ResumeToken: token, Capabilities: ports.CapabilityResume,
1746- CommittedIdentity: &ports.CommittedRouteIdentity{Target: target},1747+ CommittedIdentity: &protocol.CommittedRouteIdentity{Target: target},
1747 }))1748 }))
1748 }1749 }
1749 tr1 := &recordingTransport{recvs: []recvItem{1750 tr1 := &recordingTransport{recvs: []recvItem{
1750 {f: welcome(11)},1751 {f: welcome(11)},
1751- {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(ports.RoutePosition{Target: target, ActiveTabID: "tab-2"}))},1752+ {f: frameOf(ports.MsgRoutePosition, mustMarshalRoutePosition(protocol.RoutePosition{Target: target, ActiveTabID: "tab-2"}))},
1752 {err: io.EOF},1753 {err: io.EOF},
1753 }}1754 }}
1754 tr2 := &recordingTransport{recvs: []recvItem{1755 tr2 := &recordingTransport{recvs: []recvItem{
@@ -1760,7 +1761,7 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
1760 result := make(chan error, 1)1761 result := make(chan error, 1)
1761 go func() {1762 go func() {
1762 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{1763 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{
1763- Intent: ports.IntentAttach, SessionName: "work", Origin: ports.RouteOriginLocal, OriginKey: "local",1764+ Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1764 })1765 })
1765 }()1766 }()
1766 clock.fireReconnect(t)1767 clock.fireReconnect(t)
@@ -1782,15 +1783,15 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
1782 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {1783 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {
1783 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{1784 return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{
1784 SessionID: name, SessionName: name, ResumeToken: token, Capabilities: ports.CapabilityResume,1785 SessionID: name, SessionName: name, ResumeToken: token, Capabilities: ports.CapabilityResume,
1785- CommittedIdentity: &ports.CommittedRouteIdentity{1786+ CommittedIdentity: &protocol.CommittedRouteIdentity{
1786- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},1787+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
1787 },1788 },
1788 }))1789 }))
1789 }1790 }
1790 tr1 := &recordingTransport{recvs: []recvItem{1791 tr1 := &recordingTransport{recvs: []recvItem{
1791 {f: welcome("old", oldLifecycle, 11)},1792 {f: welcome("old", oldLifecycle, 11)},
1792- {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(ports.CommittedRouteIdentity{1793+ {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{
1793- Target: ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},1794+ Target: protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"},
1794 }))},1795 }))},
1795 {err: io.EOF},1796 {err: io.EOF},
1796 }}1797 }}
@@ -1804,7 +1805,7 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
1804 go func() {1805 go func() {
1805 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{1806 result <- runTestClient(context.Background(), testDependencies(dialer, term, clock, nil, nil), client.AttachRequest{
1806 Intent: ports.IntentAttach, SessionName: "old", Remote: true,1807 Intent: ports.IntentAttach, SessionName: "old", Remote: true,
1807- Origin: ports.RouteOriginDiscovery, OriginKey: "remote",1808+ Origin: protocol.RouteOriginDiscovery, OriginKey: "remote",
1808 RemoteTarget: remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,1809 RemoteTarget: remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
1809 })1810 })
1810 }()1811 }()
@@ -1814,7 +1815,7 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
1814 reconnectHello := helloFromSend(t, tr2)1815 reconnectHello := helloFromSend(t, tr2)
1815 require.Equal(t, ports.IntentResume, reconnectHello.Intent)1816 require.Equal(t, ports.IntentResume, reconnectHello.Intent)
1816 require.Equal(t, "new", reconnectHello.Name)1817 require.Equal(t, "new", reconnectHello.Name)
1817- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, reconnectHello.ExactTarget)1818+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, reconnectHello.ExactTarget)
1818 require.Nil(t, reconnectHello.RemoteTarget)1819 require.Nil(t, reconnectHello.RemoteTarget)
1819 }1820 }
1820 1821
M internal/usecase/client/clipboard_intercept_internal_test.go +5 -4
@@ -9,6 +9,7 @@ import (
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
10 10
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 )13 )
13 14
14 // ciFakeReader is a directly-controllable ports.ClipboardReader for15 // ciFakeReader is a directly-controllable ports.ClipboardReader for
@@ -26,17 +27,17 @@ func (r *ciFakeReader) ReadImage(context.Context) (string, []byte, error) {
26 return r.mime, r.data, r.err27 return r.mime, r.data, r.err
27 }28 }
28 29
29-func newTestClipboardIntercept(reader ports.ClipboardReader) (*clipboardIntercept, *pasteCoalescer, *pcFakeClock, *pcCollector, *[]ports.ImagePush) {30+func newTestClipboardIntercept(reader ports.ClipboardReader) (*clipboardIntercept, *pasteCoalescer, *pcFakeClock, *pcCollector, *[]protocol.ImagePush) {
30 clk := &pcFakeClock{}31 clk := &pcFakeClock{}
31 col := &pcCollector{}32 col := &pcCollector{}
32 pc := newPasteCoalescer(clk, col.emit)33 pc := newPasteCoalescer(clk, col.emit)
33- var images []ports.ImagePush34+ var images []protocol.ImagePush
34 ci := &clipboardIntercept{35 ci := &clipboardIntercept{
35 coalescer: pc,36 coalescer: pc,
36 reader: reader,37 reader: reader,
37 log: slog.New(slog.NewTextHandler(nil, &slog.HandlerOptions{Level: slog.LevelError + 1})),38 log: slog.New(slog.NewTextHandler(nil, &slog.HandlerOptions{Level: slog.LevelError + 1})),
38 sendImage: func(mime string, data []byte) {39 sendImage: func(mime string, data []byte) {
39- images = append(images, ports.ImagePush{Mime: mime, Data: data})40+ images = append(images, protocol.ImagePush{Mime: mime, Data: data})
40 },41 },
41 next: pc.Scan,42 next: pc.Scan,
42 }43 }
@@ -53,7 +54,7 @@ func TestClipboardInterceptCtrlVWithImageSendsNoPassthrough(t *testing.T) {
53 ci.Scan([]byte("b"))54 ci.Scan([]byte("b"))
54 55
55 require.Equal(t, 1, reader.n, "clipboard must be read exactly once per Ctrl+V")56 require.Equal(t, 1, reader.n, "clipboard must be read exactly once per Ctrl+V")
56- require.Equal(t, []ports.ImagePush{{Mime: "image/png", Data: []byte("PNGDATA")}}, *images)57+ require.Equal(t, []protocol.ImagePush{{Mime: "image/png", Data: []byte("PNGDATA")}}, *images)
57 require.Equal(t, [][]byte{[]byte("a"), []byte("b")}, col.snapshot(), "0x16 itself must never reach the coalescer/pane")58 require.Equal(t, [][]byte{[]byte("a"), []byte("b")}, col.snapshot(), "0x16 itself must never reach the coalescer/pane")
58 }59 }
59 60
M internal/usecase/client/clipboard_intercept_test.go +4 -3
@@ -14,6 +14,7 @@ import (
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16 portsmocks "github.com/bnema/vev/internal/ports/mocks"16 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
17 "github.com/bnema/vev/internal/usecase/client"18 "github.com/bnema/vev/internal/usecase/client"
18 )19 )
19 20
@@ -22,7 +23,7 @@ import (
22 // transport that captures every MsgInput/MsgImagePush send, and a Recv that23 // transport that captures every MsgInput/MsgImagePush send, and a Recv that
23 // answers Welcome then, once triggered, Detached — mirroring the pattern used24 // answers Welcome then, once triggered, Detached — mirroring the pattern used
24 // by the OSC-color-response test elsewhere in this package.25 // by the OSC-color-response test elsewhere in this package.
25-func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReader, stdin []byte, triggerByte byte) (gotInput chan []byte, gotImage chan ports.ImagePush) {26+func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReader, stdin []byte, triggerByte byte) (gotInput chan []byte, gotImage chan protocol.ImagePush) {
26 t.Helper()27 t.Helper()
27 var out bytes.Buffer28 var out bytes.Buffer
28 var restoreCount int3229 var restoreCount int32
@@ -39,7 +40,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
39 tm.EXPECT().ResizeEvents().Return(resizeCh).Maybe()40 tm.EXPECT().ResizeEvents().Return(resizeCh).Maybe()
40 41
41 gotInput = make(chan []byte, 8)42 gotInput = make(chan []byte, 8)
42- gotImage = make(chan ports.ImagePush, 1)43+ gotImage = make(chan protocol.ImagePush, 1)
43 allowDetach := make(chan struct{})44 allowDetach := make(chan struct{})
44 45
45 tr := portsmocks.NewMockTransport(t)46 tr := portsmocks.NewMockTransport(t)
@@ -198,7 +199,7 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
198 }199 }
199 }200 }
200 beforeOutput := out.String()201 beforeOutput := out.String()
201- transport.recv <- frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("incremental")}))202+ transport.recv <- frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("incremental")}))
202 transport.recv <- frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))203 transport.recv <- frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
203 204
204 select {205 select {
M internal/usecase/client/navigation_test.go +7 -6
@@ -5,6 +5,7 @@ import (
5 5
6 "github.com/bnema/vev/internal/domain"6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/ports"7 "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
8 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
9 )10 )
10 11
@@ -37,13 +38,13 @@ func TestValidateAttachRequestNavigationTable(t *testing.T) {
37 valid bool38 valid bool
38 }{39 }{
39 {name: "ordinary route", request: AttachRequest{}, valid: true},40 {name: "ordinary route", request: AttachRequest{}, valid: true},
40- {name: "home picker without remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", NavigationCapabilities: ports.NavigationCapabilityHomePicker}, valid: false},41+ {name: "home picker without remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
41- {name: "home picker on daemon-owned remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Remote: true, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, NavigationCapabilities: ports.NavigationCapabilityHomePicker}, valid: true},42+ {name: "home picker on daemon-owned remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Remote: true, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: true},
42- {name: "back on client-owned route", request: AttachRequest{StartupOverlay: ports.StartupOverlaySessionPicker, NavigationCapabilities: ports.NavigationCapabilityBack}, valid: true},43+ {name: "back on client-owned route", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: true},
43- {name: "back on remote-target route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", RemoteTarget: remoteRoute, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, StartupOverlay: ports.StartupOverlaySessionPicker, NavigationCapabilities: ports.NavigationCapabilityBack}, valid: false},44+ {name: "back on remote-target route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", RemoteTarget: remoteRoute, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
44 {name: "unknown capability", request: AttachRequest{NavigationCapabilities: 4}, valid: false},45 {name: "unknown capability", request: AttachRequest{NavigationCapabilities: 4}, valid: false},
45- {name: "back without startup picker", request: AttachRequest{NavigationCapabilities: ports.NavigationCapabilityBack}, valid: false},46+ {name: "back without startup picker", request: AttachRequest{NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
46- {name: "startup picker without back", request: AttachRequest{StartupOverlay: ports.StartupOverlaySessionPicker}, valid: false},47+ {name: "startup picker without back", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
47 }48 }
48 49
49 for _, tt := range tests {50 for _, tt := range tests {
M internal/usecase/client/output_state.go +2 -2
@@ -1,6 +1,6 @@
1 package client1 package client
2 2
3-import "github.com/bnema/vev/internal/ports"3+import "github.com/bnema/vev/internal/protocol"
4 4
5 // outputApplyState is the client-side dependency chain for one attachment.5 // outputApplyState is the client-side dependency chain for one attachment.
6 // State-bearing frames are accepted only when their epoch, base, and view6 // State-bearing frames are accepted only when their epoch, base, and view
@@ -12,7 +12,7 @@ type outputApplyState struct {
12 initialized bool12 initialized bool
13 }13 }
14 14
15-func (s outputApplyState) next(output ports.Output) (outputApplyState, bool) {15+func (s outputApplyState) next(output protocol.Output) (outputApplyState, bool) {
16 if output.Epoch == 0 {16 if output.Epoch == 0 {
17 return outputApplyState{}, false17 return outputApplyState{}, false
18 }18 }
M internal/usecase/client/output_state_test.go +12 -12
@@ -4,28 +4,28 @@ import (
4 "testing"4 "testing"
5 5
6 "github.com/bnema/vev/internal/domain"6 "github.com/bnema/vev/internal/domain"
7- "github.com/bnema/vev/internal/ports"7+ "github.com/bnema/vev/internal/protocol"
8 )8 )
9 9
10 func TestOutputApplyStateTransitions(t *testing.T) {10 func TestOutputApplyStateTransitions(t *testing.T) {
11- full := ports.Output{Epoch: 1, Base: 0, New: 1, Full: true, ViewRevision: 2, Size: domain.Size{Cols: 80, Rows: 24}}11+ full := protocol.Output{Epoch: 1, Base: 0, New: 1, Full: true, ViewRevision: 2, Size: domain.Size{Cols: 80, Rows: 24}}
12 tests := []struct {12 tests := []struct {
13 name string13 name string
14 state outputApplyState14 state outputApplyState
15- output ports.Output15+ output protocol.Output
16 accepted bool16 accepted bool
17 want outputApplyState17 want outputApplyState
18 }{18 }{
19 {name: "first full", output: full, accepted: true, want: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}},19 {name: "first full", output: full, accepted: true, want: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}},
20- {name: "first gap", output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: false},20+ {name: "first gap", output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: false},
21- {name: "increment", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 1, state: 2, viewRevision: 2, initialized: true}},21+ {name: "increment", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 1, state: 2, viewRevision: 2, initialized: true}},
22- {name: "base gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 1, Base: 0, New: 2, Full: true, ViewRevision: 2}, accepted: false},22+ {name: "base gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 1, Base: 0, New: 2, Full: true, ViewRevision: 2}, accepted: false},
23- {name: "revision gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 3}, accepted: false},23+ {name: "revision gap", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 3}, accepted: false},
24- {name: "handoff side effect crosses newer epoch replay gate", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}},24+ {name: "handoff side effect crosses newer epoch replay gate", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}},
25- {name: "handoff side effect crosses stale epoch replay gate", state: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}, output: ports.Output{Epoch: 1, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}},25+ {name: "handoff side effect crosses stale epoch replay gate", state: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}, output: protocol.Output{Epoch: 1, ViewRevision: 2}, accepted: true, want: outputApplyState{epoch: 2, state: 4, viewRevision: 5, initialized: true}},
26- {name: "side effect cannot invent state", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, Base: 1}, accepted: false},26+ {name: "side effect cannot invent state", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, Base: 1}, accepted: false},
27- {name: "side effect cannot claim full replay", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, Full: true}, accepted: false},27+ {name: "side effect cannot claim full replay", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, Full: true}, accepted: false},
28- {name: "new epoch reset", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: ports.Output{Epoch: 2, Base: 0, New: 1, Full: true, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 2, state: 1, viewRevision: 3, initialized: true}},28+ {name: "new epoch reset", state: outputApplyState{epoch: 1, state: 1, viewRevision: 2, initialized: true}, output: protocol.Output{Epoch: 2, Base: 0, New: 1, Full: true, ViewRevision: 3}, accepted: true, want: outputApplyState{epoch: 2, state: 1, viewRevision: 3, initialized: true}},
29 }29 }
30 for _, tt := range tests {30 for _, tt := range tests {
31 t.Run(tt.name, func(t *testing.T) {31 t.Run(tt.name, func(t *testing.T) {
M internal/usecase/client/palette_attach_generation_test.go +9 -8
@@ -16,6 +16,7 @@ import (
16 renderer "github.com/bnema/vev-vt"16 renderer "github.com/bnema/vev-vt"
17 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
18 "github.com/bnema/vev/internal/ports"18 "github.com/bnema/vev/internal/ports"
19+ "github.com/bnema/vev/internal/protocol"
19 )20 )
20 21
21 // attachPaletteClock deliberately exposes each independently-created timer.22 // attachPaletteClock deliberately exposes each independently-created timer.
@@ -115,8 +116,8 @@ func (*attachPaletteTerminalHarness) Flush() error { r
115 type attachPaletteTransport struct {116 type attachPaletteTransport struct {
116 mu sync.Mutex117 mu sync.Mutex
117 frames []ports.Frame118 frames []ports.Frame
118- themes []ports.Theme119+ themes []protocol.Theme
119- themeCh chan ports.Theme120+ themeCh chan protocol.Theme
120 inputCh chan ports.Input121 inputCh chan ports.Input
121 detached chan ports.Frame122 detached chan ports.Frame
122 themeSendErr error123 themeSendErr error
@@ -125,7 +126,7 @@ type attachPaletteTransport struct {
125 126
126 func newAttachPaletteTransport() *attachPaletteTransport {127 func newAttachPaletteTransport() *attachPaletteTransport {
127 return &attachPaletteTransport{128 return &attachPaletteTransport{
128- themeCh: make(chan ports.Theme, 16),129+ themeCh: make(chan protocol.Theme, 16),
129 inputCh: make(chan ports.Input, 16),130 inputCh: make(chan ports.Input, 16),
130 detached: make(chan ports.Frame, 1),131 detached: make(chan ports.Frame, 1),
131 }132 }
@@ -168,10 +169,10 @@ func (t *attachPaletteTransport) Recv() (ports.Frame, error) {
168 return <-t.detached, nil169 return <-t.detached, nil
169 }170 }
170 func (*attachPaletteTransport) Close() error { return nil }171 func (*attachPaletteTransport) Close() error { return nil }
171-func (t *attachPaletteTransport) snapshots() []ports.Theme {172+func (t *attachPaletteTransport) snapshots() []protocol.Theme {
172 t.mu.Lock()173 t.mu.Lock()
173 defer t.mu.Unlock()174 defer t.mu.Unlock()
174- return append([]ports.Theme(nil), t.themes...)175+ return append([]protocol.Theme(nil), t.themes...)
175 }176 }
176 177
177 type attachPaletteHarness struct {178 type attachPaletteHarness struct {
@@ -228,7 +229,7 @@ func (h *attachPaletteHarness) nextWrite(t *testing.T) string {
228 t.Helper()229 t.Helper()
229 return <-h.writer.wrote230 return <-h.writer.wrote
230 }231 }
231-func (h *attachPaletteHarness) nextTheme(t *testing.T) ports.Theme {232+func (h *attachPaletteHarness) nextTheme(t *testing.T) protocol.Theme {
232 t.Helper()233 t.Helper()
233 return <-h.transport.themeCh234 return <-h.transport.themeCh
234 }235 }
@@ -428,7 +429,7 @@ func TestStdinPumpSameReadSchemeAndOldMarkerCannotAdvanceReplacement(t *testing.
428 defer cancel()429 defer cancel()
429 430
430 coordinator := newPaletteGenerationCoordinator()431 coordinator := newPaletteGenerationCoordinator()
431- coordinator.start(ports.Theme{}, false)432+ coordinator.start(protocol.Theme{}, false)
432 var activeGeneration atomic.Uint64433 var activeGeneration atomic.Uint64
433 activeGeneration.Store(uint64(coordinator.current.id))434 activeGeneration.Store(uint64(coordinator.current.id))
434 435
@@ -466,7 +467,7 @@ func TestStdinPumpSameReadSchemeAndOldMarkerCannotAdvanceReplacement(t *testing.
466 // This models attachAttempt handling the scheme event. The replacement is467 // This models attachAttempt handling the scheme event. The replacement is
467 // draining, so a marker tagged with its ID would incorrectly start its468 // draining, so a marker tagged with its ID would incorrectly start its
468 // color batch before the real drain boundary arrives.469 // color batch before the real drain boundary arrives.
469- coordinator.start(ports.Theme{SchemeKnown: true, Light: scheme.light}, true)470+ coordinator.start(protocol.Theme{SchemeKnown: true, Light: scheme.light}, true)
470 newID := coordinator.current.id471 newID := coordinator.current.id
471 activeGeneration.Store(uint64(newID))472 activeGeneration.Store(uint64(newID))
472 close(schemeHandled)473 close(schemeHandled)
M internal/usecase/client/palette_attach_test.go +2 -1
@@ -14,6 +14,7 @@ import (
14 renderer "github.com/bnema/vev-vt"14 renderer "github.com/bnema/vev-vt"
15 "github.com/bnema/vev/internal/domain"15 "github.com/bnema/vev/internal/domain"
16 "github.com/bnema/vev/internal/ports"16 "github.com/bnema/vev/internal/ports"
17+ "github.com/bnema/vev/internal/protocol"
17 )18 )
18 19
19 type paletteAttachClock struct{}20 type paletteAttachClock struct{}
@@ -78,7 +79,7 @@ func (t *paletteAttachTerminal) Flush() error { return n
78 type paletteAttachTransport struct {79 type paletteAttachTransport struct {
79 mu sync.Mutex80 mu sync.Mutex
80 sent []ports.Frame81 sent []ports.Frame
81- themes []ports.Theme82+ themes []protocol.Theme
82 finals int83 finals int
83 finalSet chan struct{}84 finalSet chan struct{}
84 }85 }
M internal/usecase/client/palette_generation.go +7 -7
@@ -4,7 +4,7 @@ import (
4 "time"4 "time"
5 5
6 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
7- "github.com/bnema/vev/internal/ports"7+ "github.com/bnema/vev/internal/protocol"
8 )8 )
9 9
10 // Palette queries always have a DECRQM boundary. The first marker drains10 // Palette queries always have a DECRQM boundary. The first marker drains
@@ -38,7 +38,7 @@ const (
38 type paletteGeneration struct {38 type paletteGeneration struct {
39 id paletteGenerationID39 id paletteGenerationID
40 phase paletteGenerationPhase40 phase paletteGenerationPhase
41- theme ports.Theme41+ theme protocol.Theme
42 expectedMarkers int42 expectedMarkers int
43 awaitingLateDrainBoundary bool43 awaitingLateDrainBoundary bool
44 }44 }
@@ -83,7 +83,7 @@ type paletteGenerationAction struct {
83 kind paletteGenerationActionKind83 kind paletteGenerationActionKind
84 id paletteGenerationID84 id paletteGenerationID
85 bytes string85 bytes string
86- theme ports.Theme86+ theme protocol.Theme
87 deadline time.Duration87 deadline time.Duration
88 }88 }
89 89
@@ -93,7 +93,7 @@ type paletteGenerationCoordinator struct {
93 next paletteGenerationID93 next paletteGenerationID
94 current paletteGeneration94 current paletteGeneration
95 active bool95 active bool
96- finalized ports.Theme96+ finalized protocol.Theme
97 }97 }
98 98
99 func newPaletteGenerationCoordinator() *paletteGenerationCoordinator {99 func newPaletteGenerationCoordinator() *paletteGenerationCoordinator {
@@ -104,7 +104,7 @@ func newPaletteGenerationCoordinator() *paletteGenerationCoordinator {
104 // batch or a replacement drain. The retained foreground/background and104 // batch or a replacement drain. The retained foreground/background and
105 // capability avoid a full neutral flash, while the accumulator begins with no105 // capability avoid a full neutral flash, while the accumulator begins with no
106 // defaults or palette values so a definitive result never mixes generations.106 // defaults or palette values so a definitive result never mixes generations.
107-func (c *paletteGenerationCoordinator) start(retained ports.Theme, replacement bool) []paletteGenerationAction {107+func (c *paletteGenerationCoordinator) start(retained protocol.Theme, replacement bool) []paletteGenerationAction {
108 actions := c.cancelCurrent()108 actions := c.cancelCurrent()
109 c.next++109 c.next++
110 id := c.next110 id := c.next
@@ -115,7 +115,7 @@ func (c *paletteGenerationCoordinator) start(retained ports.Theme, replacement b
115 c.current = paletteGeneration{115 c.current = paletteGeneration{
116 id: id,116 id: id,
117 phase: generationCollecting,117 phase: generationCollecting,
118- theme: ports.Theme{118+ theme: protocol.Theme{
119 TrueColor: retained.TrueColor,119 TrueColor: retained.TrueColor,
120 SchemeKnown: retained.SchemeKnown,120 SchemeKnown: retained.SchemeKnown,
121 Light: retained.Light,121 Light: retained.Light,
@@ -250,7 +250,7 @@ func (c *paletteGenerationCoordinator) palette(id paletteGenerationID, slot uint
250 c.handle(paletteGenerationEvent{id: id, kind: paletteEventPalette, slot: slot, rgb: rgb})250 c.handle(paletteGenerationEvent{id: id, kind: paletteEventPalette, slot: slot, rgb: rgb})
251 }251 }
252 252
253-func (c *paletteGenerationCoordinator) finalizedTheme() ports.Theme { return c.finalized }253+func (c *paletteGenerationCoordinator) finalizedTheme() protocol.Theme { return c.finalized }
254 254
255 // paletteMarkerScanner consumes only complete DECRQM 2031 replies and retains255 // paletteMarkerScanner consumes only complete DECRQM 2031 replies and retains
256 // possible prefixes across reads, forwarding all ordinary input exactly once.256 // possible prefixes across reads, forwarding all ordinary input exactly once.
M internal/usecase/client/palette_generation_test.go +3 -3
@@ -7,7 +7,7 @@ import (
7 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
8 8
9 renderer "github.com/bnema/vev-vt"9 renderer "github.com/bnema/vev-vt"
10- "github.com/bnema/vev/internal/ports"10+ "github.com/bnema/vev/internal/protocol"
11 )11 )
12 12
13 // manualPaletteClock applies only declarative coordinator timer actions. Its13 // manualPaletteClock applies only declarative coordinator timer actions. Its
@@ -52,8 +52,8 @@ func (c *manualPaletteClock) fireCompletion(g *paletteGenerationCoordinator, id
52 return g.handle(paletteGenerationEvent{id: id, kind: paletteEventCompletionDeadline})52 return g.handle(paletteGenerationEvent{id: id, kind: paletteEventCompletionDeadline})
53 }53 }
54 54
55-func generationTheme() ports.Theme {55+func generationTheme() protocol.Theme {
56- return ports.Theme{56+ return protocol.Theme{
57 TrueColor: true,57 TrueColor: true,
58 HasForeground: true,58 HasForeground: true,
59 Foreground: renderer.RGB{R: 1, G: 2, B: 3},59 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
M internal/usecase/client/reconnect_toast_test.go +14 -13
@@ -20,6 +20,7 @@ import (
20 "github.com/bnema/vev/internal/domain"20 "github.com/bnema/vev/internal/domain"
21 "github.com/bnema/vev/internal/ports"21 "github.com/bnema/vev/internal/ports"
22 portsmocks "github.com/bnema/vev/internal/ports/mocks"22 portsmocks "github.com/bnema/vev/internal/ports/mocks"
23+ "github.com/bnema/vev/internal/protocol"
23 )24 )
24 25
25 type reconnectToastTerminalHarness struct {26 type reconnectToastTerminalHarness struct {
@@ -393,7 +394,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
393 require.Contains(t, firstToast, "┌")394 require.Contains(t, firstToast, "┌")
394 require.Contains(t, firstToast, "probing UDP path")395 require.Contains(t, firstToast, "probing UDP path")
395 396
396- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{397+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
397 Epoch: 1,398 Epoch: 1,
398 Base: 0,399 Base: 0,
399 New: 2,400 New: 2,
@@ -412,7 +413,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
412 413
413 beforeAwaitingReset := out.String()414 beforeAwaitingReset := out.String()
414 beforeStatelessFlushes := flushes.Load()415 beforeStatelessFlushes := flushes.Load()
415- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{416+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
416 Epoch: 1,417 Epoch: 1,
417 Base: 0,418 Base: 0,
418 New: 0,419 New: 0,
@@ -424,7 +425,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
424 }, time.Second, time.Millisecond)425 }, time.Second, time.Millisecond)
425 426
426 afterStateless := out.String()427 afterStateless := out.String()
427- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{428+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
428 Epoch: 1,429 Epoch: 1,
429 Base: 2,430 Base: 2,
430 New: 3,431 New: 3,
@@ -435,7 +436,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
435 require.Contains(t, out.String(), "intervening incremental")436 require.Contains(t, out.String(), "intervening incremental")
436 require.Contains(t, afterStateless[len(beforeAwaitingReset):], "stateless side effect")437 require.Contains(t, afterStateless[len(beforeAwaitingReset):], "stateless side effect")
437 438
438- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{439+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
439 Epoch: 2,440 Epoch: 2,
440 Base: 0,441 Base: 0,
441 New: 4,442 New: 4,
@@ -448,7 +449,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
448 require.NotContains(t, out.String()[len(beforeAwaitingReset):], strings.Repeat(" ", reconnectToastBoundsFor(term.size, "probing UDP path").Width))449 require.NotContains(t, out.String()[len(beforeAwaitingReset):], strings.Repeat(" ", reconnectToastBoundsFor(term.size, "probing UDP path").Width))
449 450
450 beforeIncrementFlushes := flushes.Load()451 beforeIncrementFlushes := flushes.Load()
451- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{452+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
452 Epoch: 2,453 Epoch: 2,
453 Base: 4,454 Base: 4,
454 New: 5,455 New: 5,
@@ -491,7 +492,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
491 require.NotContains(t, out.String(), offlineMessage)492 require.NotContains(t, out.String(), offlineMessage)
492 493
493 beforeReset := out.String()494 beforeReset := out.String()
494- for _, output := range []ports.Output{495+ for _, output := range []protocol.Output{
495 {Epoch: 1, Base: 1, New: 2, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("first skipped increment")},496 {Epoch: 1, Base: 1, New: 2, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("first skipped increment")},
496 {Epoch: 1, Base: 2, New: 3, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("second skipped increment")},497 {Epoch: 1, Base: 2, New: 3, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("second skipped increment")},
497 } {498 } {
@@ -507,7 +508,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
507 // Handoff cleanup is an ordered terminal side effect, not a replay state.508 // Handoff cleanup is an ordered terminal side effect, not a replay state.
508 // It must cross the outstanding reset gate, flush before the control handoff,509 // It must cross the outstanding reset gate, flush before the control handoff,
509 // and must not manufacture an independent ACK.510 // and must not manufacture an independent ACK.
510- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{511+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
511 Epoch: 2,512 Epoch: 2,
512 Size: domain.Size{Cols: 1, Rows: 1},513 Size: domain.Size{Cols: 1, Rows: 1},
513 Data: []byte("handoff graphics cleanup"),514 Data: []byte("handoff graphics cleanup"),
@@ -518,7 +519,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
518 require.False(t, sentAck, "state-independent cleanup must not be ACKed")519 require.False(t, sentAck, "state-independent cleanup must not be ACKed")
519 520
520 beforeResetFlushes := flushes.Load()521 beforeResetFlushes := flushes.Load()
521- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{522+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
522 Epoch: 2,523 Epoch: 2,
523 Base: 0,524 Base: 0,
524 New: 4,525 New: 4,
@@ -533,7 +534,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
533 requireAckedState(t, tr.sends, 2, 4)534 requireAckedState(t, tr.sends, 2, 4)
534 535
535 beforeIncrementFlushes := flushes.Load()536 beforeIncrementFlushes := flushes.Load()
536- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{537+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
537 Epoch: 2,538 Epoch: 2,
538 Base: 4,539 Base: 4,
539 New: 5,540 New: 5,
@@ -579,7 +580,7 @@ func requireSentFrame(t *testing.T, sends *reconnectToastSentFrames, description
579 }580 }
580 }581 }
581 582
582-func requireResize(t *testing.T, sends *reconnectToastSentFrames) ports.Resize {583+func requireResize(t *testing.T, sends *reconnectToastSentFrames) protocol.Resize {
583 t.Helper()584 t.Helper()
584 frame := requireSentFrame(t, sends, "Resize frame", func(frame ports.Frame) bool {585 frame := requireSentFrame(t, sends, "Resize frame", func(frame ports.Frame) bool {
585 return frame.Type == ports.MsgResize586 return frame.Type == ports.MsgResize
@@ -706,7 +707,7 @@ func TestLocalReconnectStatusFlushesDaemonOutputBeforeObservationAndAck(t *testi
706 require.Eventually(t, func() bool { return flushes.Load() > beforeStatus }, time.Second, time.Millisecond)707 require.Eventually(t, func() bool { return flushes.Load() > beforeStatus }, time.Second, time.Millisecond)
707 beforeOutput := flushes.Load()708 beforeOutput := flushes.Load()
708 709
709- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{710+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
710 Epoch: 1,711 Epoch: 1,
711 Base: 0,712 Base: 0,
712 New: 2,713 New: 2,
@@ -758,7 +759,7 @@ func TestLocalReconnectStatusTransitionsDoNotRequestAuthoritativeReset(t *testin
758 }759 }
759 760
760 const output = "local status output remains visible"761 const output = "local status output remains visible"
761- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{762+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
762 Epoch: 1,763 Epoch: 1,
763 Base: 0,764 Base: 0,
764 New: 2,765 New: 2,
@@ -826,7 +827,7 @@ func TestReconnectOverlayRedrawFailureDoesNotObserveOrAckOutput(t *testing.T) {
826 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}827 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
827 requireReconnectToastOutput(t, out.completed)828 requireReconnectToastOutput(t, out.completed)
828 arm()829 arm()
829- tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(ports.Output{830+ tr.recvCh <- reconnectToastRecv{frame: ports.Frame{Type: ports.MsgOutput, Payload: mustMarshalOutput(protocol.Output{
830 Epoch: 1,831 Epoch: 1,
831 Base: 0,832 Base: 0,
832 New: 2,833 New: 2,
M internal/usecase/client/remote_attach_validation_test.go +2 -1
@@ -9,6 +9,7 @@ import (
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11 portsmocks "github.com/bnema/vev/internal/ports/mocks"11 portsmocks "github.com/bnema/vev/internal/ports/mocks"
12+ "github.com/bnema/vev/internal/protocol"
12 )13 )
13 14
14 func TestMalformedAttachRequestsNeverDial(t *testing.T) {15 func TestMalformedAttachRequestsNeverDial(t *testing.T) {
@@ -19,7 +20,7 @@ func TestMalformedAttachRequestsNeverDial(t *testing.T) {
19 {name: "missing session name", request: AttachRequest{Intent: ports.IntentAttach}},20 {name: "missing session name", request: AttachRequest{Intent: ports.IntentAttach}},
20 {name: "unsafe session name", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "bad name"}},21 {name: "unsafe session name", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "bad name"}},
21 {name: "daemon-owned without remote", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "main", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}},22 {name: "daemon-owned without remote", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "main", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}},
22- {name: "ephemeral with navigation capability", request: AttachRequest{Intent: ports.IntentEphemeral, NavigationCapabilities: ports.NavigationCapabilityHomePicker}},23+ {name: "ephemeral with navigation capability", request: AttachRequest{Intent: ports.IntentEphemeral, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}},
23 }24 }
24 for _, tt := range tests {25 for _, tt := range tests {
25 t.Run(tt.name, func(t *testing.T) {26 t.Run(tt.name, func(t *testing.T) {
M internal/usecase/client/routes.go +58 -57
@@ -8,6 +8,7 @@ import (
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 )12 )
12 13
13 // maxRouteLedgerEntries is the product history cap. It is deliberately14 // maxRouteLedgerEntries is the product history cap. It is deliberately
@@ -36,11 +37,11 @@ type routeIdentity struct {
36 37
37 func (i routeIdentity) empty() bool { return i.key == 0 && i.generation == 0 }38 func (i routeIdentity) empty() bool { return i.key == 0 && i.generation == 0 }
38 39
39-func (i routeIdentity) wire() ports.RouteRef {40+func (i routeIdentity) wire() protocol.RouteRef {
40- return ports.RouteRef{Key: uint64(i.key), Generation: uint64(i.generation)}41+ return protocol.RouteRef{Key: uint64(i.key), Generation: uint64(i.generation)}
41 }42 }
42 43
43-func identityFromWire(ref ports.RouteRef) routeIdentity {44+func identityFromWire(ref protocol.RouteRef) routeIdentity {
44 return routeIdentity{key: routeKey(ref.Key), generation: routeGeneration(ref.Generation)}45 return routeIdentity{key: routeKey(ref.Key), generation: routeGeneration(ref.Generation)}
45 }46 }
46 47
@@ -50,16 +51,16 @@ func identityFromWire(ref ports.RouteRef) routeIdentity {
50 type routePresentation struct {51 type routePresentation struct {
51 name string52 name string
52 hostLabel string53 hostLabel string
53- kind ports.RouteKind54+ kind protocol.RouteKind
54 ephemeral bool55 ephemeral bool
55 attention bool56 attention bool
56- reachability ports.RouteReachability57+ reachability protocol.RouteReachability
57 }58 }
58 59
59 type routeCandidate struct {60 type routeCandidate struct {
60- origin ports.RouteOrigin61+ origin protocol.RouteOrigin
61 originKey string62 originKey string
62- target ports.ExactSessionTarget63+ target protocol.ExactSessionTarget
63 presentation routePresentation64 presentation routePresentation
64 dialer ports.Dialer65 dialer ports.Dialer
65 request AttachRequest66 request AttachRequest
@@ -69,9 +70,9 @@ type routeCandidate struct {
69 70
70 type routeRecord struct {71 type routeRecord struct {
71 identity routeIdentity72 identity routeIdentity
72- origin ports.RouteOrigin73+ origin protocol.RouteOrigin
73 originKey string74 originKey string
74- target ports.ExactSessionTarget75+ target protocol.ExactSessionTarget
75 presentation routePresentation76 presentation routePresentation
76 dialer ports.Dialer77 dialer ports.Dialer
77 request AttachRequest78 request AttachRequest
@@ -98,7 +99,7 @@ type routeLedger struct {
98 99
99 func newRouteLedger() *routeLedger { return &routeLedger{} }100 func newRouteLedger() *routeLedger { return &routeLedger{} }
100 101
101-func cloneCommittedIdentity(identity *ports.CommittedRouteIdentity) *ports.CommittedRouteIdentity {102+func cloneCommittedIdentity(identity *protocol.CommittedRouteIdentity) *protocol.CommittedRouteIdentity {
102 if identity == nil {103 if identity == nil {
103 return nil104 return nil
104 }105 }
@@ -106,7 +107,7 @@ func cloneCommittedIdentity(identity *ports.CommittedRouteIdentity) *ports.Commi
106 return &clone107 return &clone
107 }108 }
108 109
109-func cloneRoutePosition(position *ports.RoutePosition) *ports.RoutePosition {110+func cloneRoutePosition(position *protocol.RoutePosition) *protocol.RoutePosition {
110 if position == nil {111 if position == nil {
111 return nil112 return nil
112 }113 }
@@ -114,13 +115,13 @@ func cloneRoutePosition(position *ports.RoutePosition) *ports.RoutePosition {
114 return &clone115 return &clone
115 }116 }
116 117
117-func routeCandidateForAttach(request AttachRequest, identity ports.CommittedRouteIdentity, dialer ports.Dialer, resumeToken uint64) routeCandidate {118+func routeCandidateForAttach(request AttachRequest, identity protocol.CommittedRouteIdentity, dialer ports.Dialer, resumeToken uint64) routeCandidate {
118 origin := normalizeRouteOrigin(request.Origin, request.Remote)119 origin := normalizeRouteOrigin(request.Origin, request.Remote)
119 originKey := normalizeRouteOriginKey(request.OriginKey, origin)120 originKey := normalizeRouteOriginKey(request.OriginKey, origin)
120- kind := ports.RouteKindLocal121+ kind := protocol.RouteKindLocal
121 hostLabel := ""122 hostLabel := ""
122- if request.Remote || request.RemoteTarget != nil || origin != ports.RouteOriginLocal {123+ if request.Remote || request.RemoteTarget != nil || origin != protocol.RouteOriginLocal {
123- kind = ports.RouteKindRemote124+ kind = protocol.RouteKindRemote
124 if request.RemoteTarget != nil {125 if request.RemoteTarget != nil {
125 hostLabel = domain.RemoteDisplayOrigin(request.RemoteTarget.DisplayOrigin)126 hostLabel = domain.RemoteDisplayOrigin(request.RemoteTarget.DisplayOrigin)
126 }127 }
@@ -150,7 +151,7 @@ func routeCandidateForAttach(request AttachRequest, identity ports.CommittedRout
150 hostLabel: hostLabel,151 hostLabel: hostLabel,
151 kind: kind,152 kind: kind,
152 ephemeral: identity.Ephemeral,153 ephemeral: identity.Ephemeral,
153- reachability: ports.RouteReachabilityReachable,154+ reachability: protocol.RouteReachabilityReachable,
154 },155 },
155 dialer: dialer,156 dialer: dialer,
156 request: request,157 request: request,
@@ -158,7 +159,7 @@ func routeCandidateForAttach(request AttachRequest, identity ports.CommittedRout
158 }159 }
159 }160 }
160 161
161-func routeCandidateForCommittedIdentity(active routeRecord, identity ports.CommittedRouteIdentity) routeCandidate {162+func routeCandidateForCommittedIdentity(active routeRecord, identity protocol.CommittedRouteIdentity) routeCandidate {
162 request := cloneAttachRequest(active.request)163 request := cloneAttachRequest(active.request)
163 request.SessionName = identity.Target.SessionName164 request.SessionName = identity.Target.SessionName
164 request.ExactTarget = &identity.Target165 request.ExactTarget = &identity.Target
@@ -175,7 +176,7 @@ func routeCandidateForCommittedIdentity(active routeRecord, identity ports.Commi
175 hostLabel: active.presentation.hostLabel,176 hostLabel: active.presentation.hostLabel,
176 kind: active.presentation.kind,177 kind: active.presentation.kind,
177 ephemeral: identity.Ephemeral,178 ephemeral: identity.Ephemeral,
178- reachability: ports.RouteReachabilityReachable,179+ reachability: protocol.RouteReachabilityReachable,
179 },180 },
180 dialer: active.dialer,181 dialer: active.dialer,
181 request: request,182 request: request,
@@ -184,38 +185,38 @@ func routeCandidateForCommittedIdentity(active routeRecord, identity ports.Commi
184 }185 }
185 }186 }
186 187
187-func normalizeRouteOrigin(origin ports.RouteOrigin, remote bool) ports.RouteOrigin {188+func normalizeRouteOrigin(origin protocol.RouteOrigin, remote bool) protocol.RouteOrigin {
188 if origin != 0 {189 if origin != 0 {
189 return origin190 return origin
190 }191 }
191 if remote {192 if remote {
192- return ports.RouteOriginRemote193+ return protocol.RouteOriginRemote
193 }194 }
194- return ports.RouteOriginLocal195+ return protocol.RouteOriginLocal
195 }196 }
196 197
197-func routeFailureCode(err error) ports.RouteFailureCode {198+func routeFailureCode(err error) protocol.RouteFailureCode {
198 var protocolErr *ProtocolError199 var protocolErr *ProtocolError
199 if errors.As(err, &protocolErr) {200 if errors.As(err, &protocolErr) {
200 switch protocolErr.Code {201 switch protocolErr.Code {
201 case ports.ErrNoSuchSession, ports.ErrNoSuchTarget:202 case ports.ErrNoSuchSession, ports.ErrNoSuchTarget:
202- return ports.RouteFailureNoSuchRoute203+ return protocol.RouteFailureNoSuchRoute
203 }204 }
204 }205 }
205 if errors.Is(err, errRouteTargetChanged) {206 if errors.Is(err, errRouteTargetChanged) {
206- return ports.RouteFailureTargetChanged207+ return protocol.RouteFailureTargetChanged
207 }208 }
208- return ports.RouteFailureUnavailable209+ return protocol.RouteFailureUnavailable
209 }210 }
210 211
211-func normalizeRouteOriginKey(key string, origin ports.RouteOrigin) string {212+func normalizeRouteOriginKey(key string, origin protocol.RouteOrigin) string {
212 if key != "" {213 if key != "" {
213 return key214 return key
214 }215 }
215 switch origin {216 switch origin {
216- case ports.RouteOriginLocal:217+ case protocol.RouteOriginLocal:
217 return "local"218 return "local"
218- case ports.RouteOriginDiscovery:219+ case protocol.RouteOriginDiscovery:
219 return "discovery"220 return "discovery"
220 default:221 default:
221 return "remote"222 return "remote"
@@ -223,10 +224,10 @@ func normalizeRouteOriginKey(key string, origin ports.RouteOrigin) string {
223 }224 }
224 225
225 func validateRoutePresentation(p routePresentation) error {226 func validateRoutePresentation(p routePresentation) error {
226- if err := ports.ValidateRouteLabel(p.name, false); err != nil {227+ if err := protocol.ValidateRouteLabel(p.name, false); err != nil {
227 return err228 return err
228 }229 }
229- if err := ports.ValidateRouteLabel(p.hostLabel, true); err != nil {230+ if err := protocol.ValidateRouteLabel(p.hostLabel, true); err != nil {
230 return err231 return err
231 }232 }
232 if err := p.kind.Validate(); err != nil {233 if err := p.kind.Validate(); err != nil {
@@ -247,7 +248,7 @@ func (l *routeLedger) nextIdentityLocked() (routeIdentity, error) {
247 return routeIdentity{key: l.nextKey, generation: l.generation}, nil248 return routeIdentity{key: l.nextKey, generation: l.generation}, nil
248 }249 }
249 250
250-func (l *routeLedger) findByTargetLocked(origin ports.RouteOrigin, originKey string, target ports.ExactSessionTarget) int {251+func (l *routeLedger) findByTargetLocked(origin protocol.RouteOrigin, originKey string, target protocol.ExactSessionTarget) int {
251 for i := range l.entries {252 for i := range l.entries {
252 entry := l.entries[i]253 entry := l.entries[i]
253 if entry.origin == origin && entry.originKey == originKey && entry.target == target {254 if entry.origin == origin && entry.originKey == originKey && entry.target == target {
@@ -340,7 +341,7 @@ func (l *routeLedger) commitAttach(candidate routeCandidate) (routeIdentity, err
340 // commitCommittedIdentity snapshots the current active record and commits the341 // commitCommittedIdentity snapshots the current active record and commits the
341 // daemon's local transition under one lock. The active route, origin, dialer,342 // daemon's local transition under one lock. The active route, origin, dialer,
342 // and attach request cannot be read from different ledger generations.343 // and attach request cannot be read from different ledger generations.
343-func (l *routeLedger) commitCommittedIdentity(identity ports.CommittedRouteIdentity) (routeIdentity, error) {344+func (l *routeLedger) commitCommittedIdentity(identity protocol.CommittedRouteIdentity) (routeIdentity, error) {
344 if err := identity.Validate(); err != nil {345 if err := identity.Validate(); err != nil {
345 return routeIdentity{}, err346 return routeIdentity{}, err
346 }347 }
@@ -383,7 +384,7 @@ func (l *routeLedger) commitCommittedIdentity(identity ports.CommittedRouteIdent
383 // updateRoutePosition remembers a cursor only for the currently active exact384 // updateRoutePosition remembers a cursor only for the currently active exact
384 // route. Delayed frames from a prior transition are valid wire messages but do385 // route. Delayed frames from a prior transition are valid wire messages but do
385 // not mutate another route.386 // not mutate another route.
386-func (l *routeLedger) updateRoutePosition(position ports.RoutePosition) error {387+func (l *routeLedger) updateRoutePosition(position protocol.RoutePosition) error {
387 if err := position.Validate(); err != nil {388 if err := position.Validate(); err != nil {
388 return err389 return err
389 }390 }
@@ -459,7 +460,7 @@ func (l *routeLedger) indexByIdentityLocked(identity routeIdentity) int {
459 return -1460 return -1
460 }461 }
461 462
462-func (l *routeLedger) lookup(ref ports.RouteRef) (routeRecord, bool) {463+func (l *routeLedger) lookup(ref protocol.RouteRef) (routeRecord, bool) {
463 identity := identityFromWire(ref)464 identity := identityFromWire(ref)
464 if identity.empty() {465 if identity.empty() {
465 return routeRecord{}, false466 return routeRecord{}, false
@@ -481,21 +482,21 @@ func cloneRouteRecord(entry routeRecord) routeRecord {
481 482
482 // attentionSubscription publishes only routes served by the active route's483 // attentionSubscription publishes only routes served by the active route's
483 // daemon. Other route origins remain private to their owning daemon.484 // daemon. Other route origins remain private to their owning daemon.
484-func (l *routeLedger) attentionSubscription() ports.RouteAttentionSubscription {485+func (l *routeLedger) attentionSubscription() protocol.RouteAttentionSubscription {
485 l.mu.RLock()486 l.mu.RLock()
486 defer l.mu.RUnlock()487 defer l.mu.RUnlock()
487 488
488 activeIndex := l.indexByIdentityLocked(l.active)489 activeIndex := l.indexByIdentityLocked(l.active)
489 if activeIndex < 0 {490 if activeIndex < 0 {
490- return ports.RouteAttentionSubscription{}491+ return protocol.RouteAttentionSubscription{}
491 }492 }
492 active := l.entries[activeIndex]493 active := l.entries[activeIndex]
493- subscription := ports.RouteAttentionSubscription{Targets: make([]ports.RouteAttentionTarget, 0, len(l.entries))}494+ subscription := protocol.RouteAttentionSubscription{Targets: make([]protocol.RouteAttentionTarget, 0, len(l.entries))}
494 for _, entry := range l.entries {495 for _, entry := range l.entries {
495 if entry.identity == active.identity || entry.origin != active.origin || entry.originKey != active.originKey {496 if entry.identity == active.identity || entry.origin != active.origin || entry.originKey != active.originKey {
496 continue497 continue
497 }498 }
498- subscription.Targets = append(subscription.Targets, ports.RouteAttentionTarget{499+ subscription.Targets = append(subscription.Targets, protocol.RouteAttentionTarget{
499 Ref: entry.identity.wire(),500 Ref: entry.identity.wire(),
500 Target: entry.target,501 Target: entry.target,
501 })502 })
@@ -503,8 +504,8 @@ func (l *routeLedger) attentionSubscription() ports.RouteAttentionSubscription {
503 return subscription504 return subscription
504 }505 }
505 506
506-func (r routeRecord) snapshotEntry() ports.RecentRouteEntry {507+func (r routeRecord) snapshotEntry() protocol.RecentRouteEntry {
507- return ports.RecentRouteEntry{508+ return protocol.RecentRouteEntry{
508 Key: uint64(r.identity.key),509 Key: uint64(r.identity.key),
509 Generation: uint64(r.identity.generation),510 Generation: uint64(r.identity.generation),
510 Target: r.target,511 Target: r.target,
@@ -517,11 +518,11 @@ func (r routeRecord) snapshotEntry() ports.RecentRouteEntry {
517 }518 }
518 }519 }
519 520
520-func (l *routeLedger) snapshot() ports.RecentRouteSnapshot {521+func (l *routeLedger) snapshot() protocol.RecentRouteSnapshot {
521 l.mu.RLock()522 l.mu.RLock()
522 defer l.mu.RUnlock()523 defer l.mu.RUnlock()
523 524
524- snapshot := ports.RecentRouteSnapshot{525+ snapshot := protocol.RecentRouteSnapshot{
525 Generation: uint64(l.generation),526 Generation: uint64(l.generation),
526 Active: l.active.wire(),527 Active: l.active.wire(),
527 Previous: l.previous.wire(),528 Previous: l.previous.wire(),
@@ -530,7 +531,7 @@ func (l *routeLedger) snapshot() ports.RecentRouteSnapshot {
530 if len(l.entries) == 0 {531 if len(l.entries) == 0 {
531 return snapshot532 return snapshot
532 }533 }
533- snapshot.Entries = make([]ports.RecentRouteEntry, 0, len(l.entries))534+ snapshot.Entries = make([]protocol.RecentRouteEntry, 0, len(l.entries))
534 for _, entry := range l.entries {535 for _, entry := range l.entries {
535 if entry.identity == l.active {536 if entry.identity == l.active {
536 snapshot.ActiveEntry = entry.snapshotEntry()537 snapshot.ActiveEntry = entry.snapshotEntry()
@@ -548,7 +549,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
548 origin := normalizeRouteOrigin(active.Origin, active.Remote)549 origin := normalizeRouteOrigin(active.Origin, active.Remote)
549 originKey := normalizeRouteOriginKey(active.OriginKey, origin)550 originKey := normalizeRouteOriginKey(active.OriginKey, origin)
550 environmentPolicy := target.EnvironmentPolicy551 environmentPolicy := target.EnvironmentPolicy
551- if origin == ports.RouteOriginLocal {552+ if origin == protocol.RouteOriginLocal {
552 environmentPolicy = ports.EnvironmentPolicyClientOwned553 environmentPolicy = ports.EnvironmentPolicyClientOwned
553 }554 }
554 555
@@ -569,7 +570,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
569 request.RemoteTarget = nil570 request.RemoteTarget = nil
570 if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {571 if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {
571 request.NavigationCapabilities = 0572 request.NavigationCapabilities = 0
572- request.StartupOverlay = ports.StartupOverlayNone573+ request.StartupOverlay = protocol.StartupOverlayNone
573 }574 }
574 request.EnvironmentPolicy = environmentPolicy575 request.EnvironmentPolicy = environmentPolicy
575 request.ExactTarget = target.ExactTarget576 request.ExactTarget = target.ExactTarget
@@ -587,25 +588,25 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
587 request.RemoteTarget = nil588 request.RemoteTarget = nil
588 if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {589 if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {
589 request.NavigationCapabilities = 0590 request.NavigationCapabilities = 0
590- request.StartupOverlay = ports.StartupOverlayNone591+ request.StartupOverlay = protocol.StartupOverlayNone
591 }592 }
592 request.EnvironmentPolicy = environmentPolicy593 request.EnvironmentPolicy = environmentPolicy
593 return request594 return request
594 }595 }
595 596
596-func (l *routeLedger) activeRef() ports.RouteRef {597+func (l *routeLedger) activeRef() protocol.RouteRef {
597 l.mu.RLock()598 l.mu.RLock()
598 defer l.mu.RUnlock()599 defer l.mu.RUnlock()
599 return l.active.wire()600 return l.active.wire()
600 }601 }
601 602
602-func (l *routeLedger) previousRef() ports.RouteRef {603+func (l *routeLedger) previousRef() protocol.RouteRef {
603 l.mu.RLock()604 l.mu.RLock()
604 defer l.mu.RUnlock()605 defer l.mu.RUnlock()
605 return l.previous.wire()606 return l.previous.wire()
606 }607 }
607 608
608-func (l *routeLedger) homeRef() ports.RouteRef {609+func (l *routeLedger) homeRef() protocol.RouteRef {
609 l.mu.RLock()610 l.mu.RLock()
610 defer l.mu.RUnlock()611 defer l.mu.RUnlock()
611 return l.home.wire()612 return l.home.wire()
@@ -619,13 +620,13 @@ type routeTransitionPlan struct {
619 record routeRecord620 record routeRecord
620 snapshotGeneration routeGeneration621 snapshotGeneration routeGeneration
621 active routeIdentity622 active routeIdentity
622- origin ports.RouteOrigin623+ origin protocol.RouteOrigin
623 originKey string624 originKey string
624- target ports.ExactSessionTarget625+ target protocol.ExactSessionTarget
625 }626 }
626 627
627 type routeTransitionCommit struct {628 type routeTransitionCommit struct {
628- identity ports.CommittedRouteIdentity629+ identity protocol.CommittedRouteIdentity
629 presentation routePresentation630 presentation routePresentation
630 dialer ports.Dialer631 dialer ports.Dialer
631 resumeToken uint64632 resumeToken uint64
@@ -650,11 +651,11 @@ type routeNavigationSelection struct {
650 // generation, and active identity under one read lock. The commit phase uses651 // generation, and active identity under one read lock. The commit phase uses
651 // the captured generation and active identity to reject interleavings that652 // the captured generation and active identity to reject interleavings that
652 // changed the ledger while dialing.653 // changed the ledger while dialing.
653-func (l *routeLedger) navigationSelection(action ports.RouteNavigationAction) (routeNavigationSelection, bool) {654+func (l *routeLedger) navigationSelection(action protocol.RouteNavigationAction) (routeNavigationSelection, bool) {
654 if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {655 if action.SnapshotGeneration == 0 || action.Key == 0 || action.Generation == 0 {
655 return routeNavigationSelection{}, false656 return routeNavigationSelection{}, false
656 }657 }
657- identity := identityFromWire(ports.RouteRef{Key: action.Key, Generation: action.Generation})658+ identity := identityFromWire(protocol.RouteRef{Key: action.Key, Generation: action.Generation})
658 l.mu.RLock()659 l.mu.RLock()
659 defer l.mu.RUnlock()660 defer l.mu.RUnlock()
660 if uint64(l.generation) != action.SnapshotGeneration {661 if uint64(l.generation) != action.SnapshotGeneration {
@@ -680,7 +681,7 @@ func (l *routeLedger) navigationSelection(action ports.RouteNavigationAction) (r
680 681
681 // navigationRecord validates that an action belongs to the latest complete682 // navigationRecord validates that an action belongs to the latest complete
682 // snapshot. The active route is metadata-only and selecting it is a no-op.683 // snapshot. The active route is metadata-only and selecting it is a no-op.
683-func (l *routeLedger) navigationRecord(action ports.RouteNavigationAction) (routeRecord, bool, bool) {684+func (l *routeLedger) navigationRecord(action protocol.RouteNavigationAction) (routeRecord, bool, bool) {
684 selection, ok := l.navigationSelection(action)685 selection, ok := l.navigationSelection(action)
685 return selection.selected, ok, selection.noOp686 return selection.selected, ok, selection.noOp
686 }687 }
@@ -688,7 +689,7 @@ func (l *routeLedger) navigationRecord(action ports.RouteNavigationAction) (rout
688 // navigate resolves a captured key/generation, attempts resume before exact689 // navigate resolves a captured key/generation, attempts resume before exact
689 // attach, and commits only after the connector returns a matching identity.690 // attach, and commits only after the connector returns a matching identity.
690 // A failed transition restores the prior live route and leaves history alone.691 // A failed transition restores the prior live route and leaves history alone.
691-func (l *routeLedger) navigate(ctx context.Context, action ports.RouteNavigationAction, connector routeTransitionConnector) error {692+func (l *routeLedger) navigate(ctx context.Context, action protocol.RouteNavigationAction, connector routeTransitionConnector) error {
692 selection, ok := l.navigationSelection(action)693 selection, ok := l.navigationSelection(action)
693 if !ok {694 if !ok {
694 return errRouteStaleSelection695 return errRouteStaleSelection
@@ -704,7 +705,7 @@ func (l *routeLedger) navigate(ctx context.Context, action ports.RouteNavigation
704 if ctx == nil {705 if ctx == nil {
705 return errors.New("route transition context is nil")706 return errors.New("route transition context is nil")
706 }707 }
707- selected := ports.RouteRef{Key: action.Key, Generation: action.Generation}708+ selected := protocol.RouteRef{Key: action.Key, Generation: action.Generation}
708 plan := routeTransitionPlan{709 plan := routeTransitionPlan{
709 identity: identityFromWire(selected),710 identity: identityFromWire(selected),
710 record: record,711 record: record,
M internal/usecase/client/routes_test.go +66 -65
@@ -8,22 +8,23 @@ import (
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 "github.com/stretchr/testify/require"12 "github.com/stretchr/testify/require"
12 )13 )
13 14
14-func routeTestTarget(index byte) ports.ExactSessionTarget {15+func routeTestTarget(index byte) protocol.ExactSessionTarget {
15- return ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{index + 1}, SessionName: "session-" + string(rune('a'+index%26)) + string(rune('0'+index/26))}16+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{index + 1}, SessionName: "session-" + string(rune('a'+index%26)) + string(rune('0'+index/26))}
16 }17 }
17 18
18-func routeTestCandidate(index byte, origin ports.RouteOrigin) routeCandidate {19+func routeTestCandidate(index byte, origin protocol.RouteOrigin) routeCandidate {
19 target := routeTestTarget(index)20 target := routeTestTarget(index)
20 return routeCandidate{21 return routeCandidate{
21 origin: origin,22 origin: origin,
22 target: target,23 target: target,
23 presentation: routePresentation{24 presentation: routePresentation{
24 name: target.SessionName,25 name: target.SessionName,
25- kind: ports.RouteKindLocal,26+ kind: protocol.RouteKindLocal,
26- reachability: ports.RouteReachabilityReachable,27+ reachability: protocol.RouteReachabilityReachable,
27 },28 },
28 request: AttachRequest{29 request: AttachRequest{
29 Intent: ports.IntentAttach,30 Intent: ports.IntentAttach,
@@ -37,8 +38,8 @@ func TestRouteCandidateRetainsHostLabelWithoutDiscoveryTarget(t *testing.T) {
37 target := routeTestTarget(1)38 target := routeTestTarget(1)
38 candidate := routeCandidateForAttach(AttachRequest{39 candidate := routeCandidateForAttach(AttachRequest{
39 Intent: ports.IntentAttach, SessionName: target.SessionName, Remote: true,40 Intent: ports.IntentAttach, SessionName: target.SessionName, Remote: true,
40- Origin: ports.RouteOriginRemote, OriginKey: "user@remote", HostLabel: "remote",41+ Origin: protocol.RouteOriginRemote, OriginKey: "user@remote", HostLabel: "remote",
41- }, ports.CommittedRouteIdentity{Target: target}, nil, 0)42+ }, protocol.CommittedRouteIdentity{Target: target}, nil, 0)
42 43
43 require.Nil(t, candidate.request.RemoteTarget)44 require.Nil(t, candidate.request.RemoteTarget)
44 require.Equal(t, "remote", candidate.request.HostLabel)45 require.Equal(t, "remote", candidate.request.HostLabel)
@@ -49,7 +50,7 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
49 ledger := newRouteLedger()50 ledger := newRouteLedger()
50 var keys []routeKey51 var keys []routeKey
51 for i := byte(0); i < maxRouteLedgerEntries+8; i++ {52 for i := byte(0); i < maxRouteLedgerEntries+8; i++ {
52- candidate := routeTestCandidate(i, ports.RouteOriginLocal)53+ candidate := routeTestCandidate(i, protocol.RouteOriginLocal)
53 candidate.home = i == 054 candidate.home = i == 0
54 identity, err := ledger.commit(candidate)55 identity, err := ledger.commit(candidate)
55 require.NoError(t, err)56 require.NoError(t, err)
@@ -61,11 +62,11 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
61 require.Len(t, snapshot.Entries, maxRouteLedgerEntries-1)62 require.Len(t, snapshot.Entries, maxRouteLedgerEntries-1)
62 require.Equal(t, uint64(keys[len(keys)-1]), snapshot.Active.Key)63 require.Equal(t, uint64(keys[len(keys)-1]), snapshot.Active.Key)
63 require.Equal(t, routeTestTarget(maxRouteLedgerEntries+7), snapshot.ActiveEntry.Target)64 require.Equal(t, routeTestTarget(maxRouteLedgerEntries+7), snapshot.ActiveEntry.Target)
64- require.Equal(t, snapshot.Active, ports.RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation})65+ require.Equal(t, snapshot.Active, protocol.RouteRef{Key: snapshot.ActiveEntry.Key, Generation: snapshot.ActiveEntry.Generation})
65 for _, entry := range snapshot.Entries {66 for _, entry := range snapshot.Entries {
66 require.NoError(t, entry.Target.Validate())67 require.NoError(t, entry.Target.Validate())
67 require.Equal(t, entry.Name, entry.Target.SessionName)68 require.Equal(t, entry.Name, entry.Target.SessionName)
68- require.NotEqual(t, snapshot.Active, ports.RouteRef{Key: entry.Key, Generation: entry.Generation})69+ require.NotEqual(t, snapshot.Active, protocol.RouteRef{Key: entry.Key, Generation: entry.Generation})
69 }70 }
70 require.Equal(t, uint64(keys[len(keys)-2]), snapshot.Previous.Key)71 require.Equal(t, uint64(keys[len(keys)-2]), snapshot.Previous.Key)
71 require.Equal(t, uint64(keys[0]), snapshot.Home.Key, "home is retained when older entries are evicted")72 require.Equal(t, uint64(keys[0]), snapshot.Home.Key, "home is retained when older entries are evicted")
@@ -79,7 +80,7 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
79 require.Equal(t, originalName, fresh.Entries[0].Name)80 require.Equal(t, originalName, fresh.Entries[0].Name)
80 81
81 old := ledger.activeRef()82 old := ledger.activeRef()
82- _, err := ledger.commit(routeTestCandidate(maxRouteLedgerEntries+9, ports.RouteOriginLocal))83+ _, err := ledger.commit(routeTestCandidate(maxRouteLedgerEntries+9, protocol.RouteOriginLocal))
83 require.NoError(t, err)84 require.NoError(t, err)
84 require.NotEqual(t, old, ledger.activeRef())85 require.NotEqual(t, old, ledger.activeRef())
85 require.Equal(t, old, ledger.previousRef(), "the old active route becomes the previous route")86 require.Equal(t, old, ledger.previousRef(), "the old active route becomes the previous route")
@@ -87,21 +88,21 @@ func TestRouteLedgerBoundsAndImmutableSnapshot(t *testing.T) {
87 88
88 func TestRouteLedgerRemembersTabsIndependentlyPerExactRoute(t *testing.T) {89 func TestRouteLedgerRemembersTabsIndependentlyPerExactRoute(t *testing.T) {
89 ledger := newRouteLedger()90 ledger := newRouteLedger()
90- first := routeTestCandidate(1, ports.RouteOriginLocal)91+ first := routeTestCandidate(1, protocol.RouteOriginLocal)
91 firstIdentity, err := ledger.commit(first)92 firstIdentity, err := ledger.commit(first)
92 require.NoError(t, err)93 require.NoError(t, err)
93 firstRef := ledger.activeRef()94 firstRef := ledger.activeRef()
94 firstSnapshotGeneration := ledger.snapshot().Generation95 firstSnapshotGeneration := ledger.snapshot().Generation
95- require.NoError(t, ledger.updateRoutePosition(ports.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))96+ require.NoError(t, ledger.updateRoutePosition(protocol.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))
96 require.Equal(t, firstRef, ledger.activeRef(), "tab memory must not rotate route identity")97 require.Equal(t, firstRef, ledger.activeRef(), "tab memory must not rotate route identity")
97 require.Equal(t, firstSnapshotGeneration, ledger.snapshot().Generation, "tab memory must not stale navigation snapshots")98 require.Equal(t, firstSnapshotGeneration, ledger.snapshot().Generation, "tab memory must not stale navigation snapshots")
98 99
99- second := routeTestCandidate(2, ports.RouteOriginRemote)100+ second := routeTestCandidate(2, protocol.RouteOriginRemote)
100 secondIdentity, err := ledger.commit(second)101 secondIdentity, err := ledger.commit(second)
101 require.NoError(t, err)102 require.NoError(t, err)
102 secondRef := ledger.activeRef()103 secondRef := ledger.activeRef()
103 secondSnapshotGeneration := ledger.snapshot().Generation104 secondSnapshotGeneration := ledger.snapshot().Generation
104- require.NoError(t, ledger.updateRoutePosition(ports.RoutePosition{Target: second.target, ActiveTabID: "tab-second"}))105+ require.NoError(t, ledger.updateRoutePosition(protocol.RoutePosition{Target: second.target, ActiveTabID: "tab-second"}))
105 require.Equal(t, secondRef, ledger.activeRef(), "tab memory must not rotate route identity")106 require.Equal(t, secondRef, ledger.activeRef(), "tab memory must not rotate route identity")
106 require.Equal(t, secondSnapshotGeneration, ledger.snapshot().Generation, "tab memory must not stale navigation snapshots")107 require.Equal(t, secondSnapshotGeneration, ledger.snapshot().Generation, "tab memory must not stale navigation snapshots")
107 108
@@ -115,13 +116,13 @@ func TestRouteLedgerRemembersTabsIndependentlyPerExactRoute(t *testing.T) {
115 116
116 func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {117 func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
117 ledger := newRouteLedger()118 ledger := newRouteLedger()
118- first := routeTestCandidate(1, ports.RouteOriginLocal)119+ first := routeTestCandidate(1, protocol.RouteOriginLocal)
119 first.originKey = "local"120 first.originKey = "local"
120 _, err := ledger.commit(first)121 _, err := ledger.commit(first)
121 require.NoError(t, err)122 require.NoError(t, err)
122- require.NoError(t, ledger.updateRoutePosition(ports.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))123+ require.NoError(t, ledger.updateRoutePosition(protocol.RoutePosition{Target: first.target, ActiveTabID: "tab-first"}))
123 124
124- second := routeTestCandidate(2, ports.RouteOriginLocal)125+ second := routeTestCandidate(2, protocol.RouteOriginLocal)
125 second.originKey = "local"126 second.originKey = "local"
126 _, err = ledger.commit(second)127 _, err = ledger.commit(second)
127 require.NoError(t, err)128 require.NoError(t, err)
@@ -130,8 +131,8 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
130 recreated.LifecycleID[0]++131 recreated.LifecycleID[0]++
131 for _, tt := range []struct {132 for _, tt := range []struct {
132 name string133 name string
133- target ports.ExactSessionTarget134+ target protocol.ExactSessionTarget
134- wantExactTarget *ports.ExactSessionTarget135+ wantExactTarget *protocol.ExactSessionTarget
135 wantTab domain.TabStableID136 wantTab domain.TabStableID
136 }{137 }{
137 {name: "matching lifecycle restores remembered tab", target: first.target, wantExactTarget: &first.target, wantTab: "tab-first"},138 {name: "matching lifecycle restores remembered tab", target: first.target, wantExactTarget: &first.target, wantTab: "tab-first"},
@@ -153,11 +154,11 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
153 }154 }
154 155
155 func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *testing.T) {156 func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *testing.T) {
156- active := routeTestCandidate(2, ports.RouteOriginLocal)157+ active := routeTestCandidate(2, protocol.RouteOriginLocal)
157 active.originKey = "local"158 active.originKey = "local"
158 active.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned159 active.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
159- active.request.NavigationCapabilities = ports.NavigationCapabilityHomePicker160+ active.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
160- active.request.StartupOverlay = ports.StartupOverlaySessionPicker161+ active.request.StartupOverlay = protocol.StartupOverlaySessionPicker
161 162
162 for _, tt := range []struct {163 for _, tt := range []struct {
163 name string164 name string
@@ -170,11 +171,11 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
170 ledger := newRouteLedger()171 ledger := newRouteLedger()
171 target := routeTestTarget(1)172 target := routeTestTarget(1)
172 if tt.matched {173 if tt.matched {
173- stored := routeTestCandidate(1, ports.RouteOriginLocal)174+ stored := routeTestCandidate(1, protocol.RouteOriginLocal)
174 stored.originKey = "local"175 stored.originKey = "local"
175 stored.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned176 stored.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
176- stored.request.NavigationCapabilities = ports.NavigationCapabilityHomePicker177+ stored.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
177- stored.request.StartupOverlay = ports.StartupOverlaySessionPicker178+ stored.request.StartupOverlay = protocol.StartupOverlaySessionPicker
178 _, err := ledger.commit(stored)179 _, err := ledger.commit(stored)
179 require.NoError(t, err)180 require.NoError(t, err)
180 }181 }
@@ -186,7 +187,7 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
186 187
187 require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)188 require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
188 require.Zero(t, request.NavigationCapabilities)189 require.Zero(t, request.NavigationCapabilities)
189- require.Equal(t, ports.StartupOverlayNone, request.StartupOverlay)190+ require.Equal(t, protocol.StartupOverlayNone, request.StartupOverlay)
190 require.NoError(t, validateAttachRequest(request))191 require.NoError(t, validateAttachRequest(request))
191 })192 })
192 }193 }
@@ -194,7 +195,7 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
194 195
195 func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {196 func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
196 ledger := newRouteLedger()197 ledger := newRouteLedger()
197- work := routeTestCandidate(1, ports.RouteOriginRemote)198+ work := routeTestCandidate(1, protocol.RouteOriginRemote)
198 work.originKey = "remote"199 work.originKey = "remote"
199 work.request.Remote = true200 work.request.Remote = true
200 work.request.OriginKey = "remote"201 work.request.OriginKey = "remote"
@@ -206,7 +207,7 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
206 _, err := ledger.commit(work)207 _, err := ledger.commit(work)
207 require.NoError(t, err)208 require.NoError(t, err)
208 209
209- agents := routeTestCandidate(2, ports.RouteOriginRemote)210+ agents := routeTestCandidate(2, protocol.RouteOriginRemote)
210 agents.originKey = "remote"211 agents.originKey = "remote"
211 agents.request.Remote = true212 agents.request.Remote = true
212 agents.request.OriginKey = "remote"213 agents.request.OriginKey = "remote"
@@ -220,32 +221,32 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
220 })221 })
221 222
222 require.True(t, request.Remote)223 require.True(t, request.Remote)
223- require.Equal(t, ports.RouteOriginRemote, request.Origin)224+ require.Equal(t, protocol.RouteOriginRemote, request.Origin)
224 require.Equal(t, "remote", request.OriginKey)225 require.Equal(t, "remote", request.OriginKey)
225 require.Nil(t, request.RemoteTarget)226 require.Nil(t, request.RemoteTarget)
226 }227 }
227 228
228 func TestRouteAttentionSubscriptionIncludesOnlyActiveOriginRoutes(t *testing.T) {229 func TestRouteAttentionSubscriptionIncludesOnlyActiveOriginRoutes(t *testing.T) {
229 ledger := newRouteLedger()230 ledger := newRouteLedger()
230- first := routeTestCandidate(0, ports.RouteOriginRemote)231+ first := routeTestCandidate(0, protocol.RouteOriginRemote)
231 first.originKey = "host-a"232 first.originKey = "host-a"
232 firstIdentity, err := ledger.commit(first)233 firstIdentity, err := ledger.commit(first)
233 require.NoError(t, err)234 require.NoError(t, err)
234- other := routeTestCandidate(1, ports.RouteOriginLocal)235+ other := routeTestCandidate(1, protocol.RouteOriginLocal)
235 _, err = ledger.commit(other)236 _, err = ledger.commit(other)
236 require.NoError(t, err)237 require.NoError(t, err)
237- otherRemote := routeTestCandidate(2, ports.RouteOriginRemote)238+ otherRemote := routeTestCandidate(2, protocol.RouteOriginRemote)
238 otherRemote.originKey = "host-b"239 otherRemote.originKey = "host-b"
239 _, err = ledger.commit(otherRemote)240 _, err = ledger.commit(otherRemote)
240 require.NoError(t, err)241 require.NoError(t, err)
241- active := routeTestCandidate(3, ports.RouteOriginRemote)242+ active := routeTestCandidate(3, protocol.RouteOriginRemote)
242 active.originKey = "host-a"243 active.originKey = "host-a"
243 _, err = ledger.commit(active)244 _, err = ledger.commit(active)
244 require.NoError(t, err)245 require.NoError(t, err)
245 246
246 subscription := ledger.attentionSubscription()247 subscription := ledger.attentionSubscription()
247 248
248- require.Equal(t, []ports.RouteAttentionTarget{{249+ require.Equal(t, []protocol.RouteAttentionTarget{{
249 Ref: firstIdentity.wire(),250 Ref: firstIdentity.wire(),
250 Target: first.target,251 Target: first.target,
251 }}, subscription.Targets)252 }}, subscription.Targets)
@@ -253,16 +254,16 @@ func TestRouteAttentionSubscriptionIncludesOnlyActiveOriginRoutes(t *testing.T)
253 254
254 func TestCommittedIdentityRenamesActiveRouteInPlace(t *testing.T) {255 func TestCommittedIdentityRenamesActiveRouteInPlace(t *testing.T) {
255 ledger := newRouteLedger()256 ledger := newRouteLedger()
256- original := routeTestCandidate(0, ports.RouteOriginLocal)257+ original := routeTestCandidate(0, protocol.RouteOriginLocal)
257- original.target = ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "0"}258+ original.target = protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "0"}
258 original.presentation.name = "0"259 original.presentation.name = "0"
259 original.request.SessionName = "0"260 original.request.SessionName = "0"
260 identity, err := ledger.commit(original)261 identity, err := ledger.commit(original)
261 require.NoError(t, err)262 require.NoError(t, err)
262 before := ledger.snapshot()263 before := ledger.snapshot()
263 264
264- committed, err := ledger.commitCommittedIdentity(ports.CommittedRouteIdentity{265+ committed, err := ledger.commitCommittedIdentity(protocol.CommittedRouteIdentity{
265- Target: ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "vps-infra"},266+ Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "vps-infra"},
266 Ephemeral: false,267 Ephemeral: false,
267 })268 })
268 require.NoError(t, err)269 require.NoError(t, err)
@@ -280,11 +281,11 @@ func TestCommittedIdentityRenamesActiveRouteInPlace(t *testing.T) {
280 281
281 func TestCommittedIdentityDoesNotReassignHomeRoute(t *testing.T) {282 func TestCommittedIdentityDoesNotReassignHomeRoute(t *testing.T) {
282 ledger := newRouteLedger()283 ledger := newRouteLedger()
283- home := routeTestCandidate(1, ports.RouteOriginLocal)284+ home := routeTestCandidate(1, protocol.RouteOriginLocal)
284 home.home = true285 home.home = true
285 homeIdentity, err := ledger.commit(home)286 homeIdentity, err := ledger.commit(home)
286 require.NoError(t, err)287 require.NoError(t, err)
287- committed, err := ledger.commitCommittedIdentity(ports.CommittedRouteIdentity{288+ committed, err := ledger.commitCommittedIdentity(protocol.CommittedRouteIdentity{
288 Target: routeTestTarget(2),289 Target: routeTestTarget(2),
289 Ephemeral: true,290 Ephemeral: true,
290 })291 })
@@ -295,7 +296,7 @@ func TestCommittedIdentityDoesNotReassignHomeRoute(t *testing.T) {
295 require.True(t, ok)296 require.True(t, ok)
296 require.Equal(t, committed, active.identity)297 require.Equal(t, committed, active.identity)
297 298
298- _, err = ledger.commit(routeTestCandidate(1, ports.RouteOriginLocal))299+ _, err = ledger.commit(routeTestCandidate(1, protocol.RouteOriginLocal))
299 require.NoError(t, err)300 require.NoError(t, err)
300 require.True(t, mustRouteRecord(t, ledger, ledger.activeRef()).home, "upserting the home route must retain its marker")301 require.True(t, mustRouteRecord(t, ledger, ledger.activeRef()).home, "upserting the home route must retain its marker")
301 }302 }
@@ -309,7 +310,7 @@ func TestRouteLedgerConcurrentInitialAttachmentsKeepOneHome(t *testing.T) {
309 wg.Add(1)310 wg.Add(1)
310 go func() {311 go func() {
311 defer wg.Done()312 defer wg.Done()
312- _, err := ledger.commitAttach(routeTestCandidate(i, ports.RouteOriginLocal))313+ _, err := ledger.commitAttach(routeTestCandidate(i, protocol.RouteOriginLocal))
313 errs <- err314 errs <- err
314 }()315 }()
315 }316 }
@@ -333,22 +334,22 @@ func TestRouteLedgerConcurrentInitialAttachmentsKeepOneHome(t *testing.T) {
333 334
334 func TestRouteLedgerTransitionRejectsConcurrentCommit(t *testing.T) {335 func TestRouteLedgerTransitionRejectsConcurrentCommit(t *testing.T) {
335 ledger := newRouteLedger()336 ledger := newRouteLedger()
336- selected, err := ledger.commit(routeTestCandidate(1, ports.RouteOriginLocal))337+ selected, err := ledger.commit(routeTestCandidate(1, protocol.RouteOriginLocal))
337 require.NoError(t, err)338 require.NoError(t, err)
338- _, err = ledger.commit(routeTestCandidate(2, ports.RouteOriginLocal))339+ _, err = ledger.commit(routeTestCandidate(2, protocol.RouteOriginLocal))
339 require.NoError(t, err)340 require.NoError(t, err)
340 selection, ok := ledger.navigationSelection(selected.wireNavigationAction(routeGeneration(ledger.snapshot().Generation)))341 selection, ok := ledger.navigationSelection(selected.wireNavigationAction(routeGeneration(ledger.snapshot().Generation)))
341 require.True(t, ok)342 require.True(t, ok)
342 343
343- _, err = ledger.commit(routeTestCandidate(3, ports.RouteOriginLocal))344+ _, err = ledger.commit(routeTestCandidate(3, protocol.RouteOriginLocal))
344 require.NoError(t, err)345 require.NoError(t, err)
345- candidate := routeTestCandidate(4, ports.RouteOriginLocal)346+ candidate := routeTestCandidate(4, protocol.RouteOriginLocal)
346 err = ledger.commitTransition(selected, selection.snapshotGeneration, selection.active, candidate)347 err = ledger.commitTransition(selected, selection.snapshotGeneration, selection.active, candidate)
347 require.ErrorIs(t, err, errRouteStaleSelection)348 require.ErrorIs(t, err, errRouteStaleSelection)
348 require.Equal(t, routeTestTarget(3), mustRouteRecord(t, ledger, ledger.activeRef()).target)349 require.Equal(t, routeTestTarget(3), mustRouteRecord(t, ledger, ledger.activeRef()).target)
349 }350 }
350 351
351-func mustRouteRecord(t *testing.T, ledger *routeLedger, ref ports.RouteRef) routeRecord {352+func mustRouteRecord(t *testing.T, ledger *routeLedger, ref protocol.RouteRef) routeRecord {
352 t.Helper()353 t.Helper()
353 record, ok := ledger.lookup(ref)354 record, ok := ledger.lookup(ref)
354 require.True(t, ok)355 require.True(t, ok)
@@ -357,7 +358,7 @@ func mustRouteRecord(t *testing.T, ledger *routeLedger, ref ports.RouteRef) rout
357 358
358 func TestRouteLedgerUpsertChangesGenerationWithoutChangingOpaqueKey(t *testing.T) {359 func TestRouteLedgerUpsertChangesGenerationWithoutChangingOpaqueKey(t *testing.T) {
359 ledger := newRouteLedger()360 ledger := newRouteLedger()
360- candidate := routeTestCandidate(1, ports.RouteOriginRemote)361+ candidate := routeTestCandidate(1, protocol.RouteOriginRemote)
361 first, err := ledger.commit(candidate)362 first, err := ledger.commit(candidate)
362 require.NoError(t, err)363 require.NoError(t, err)
363 364
@@ -387,7 +388,7 @@ type routeTestConnector struct {
387 attachCalls int388 attachCalls int
388 restoreCalls int389 restoreCalls int
389 restoreErr error390 restoreErr error
390- restoreTarget ports.ExactSessionTarget391+ restoreTarget protocol.ExactSessionTarget
391 }392 }
392 393
393 func (c *routeTestConnector) Resume(context.Context, routeRecord) (routeTransitionCommit, error) {394 func (c *routeTestConnector) Resume(context.Context, routeRecord) (routeTransitionCommit, error) {
@@ -410,11 +411,11 @@ func (c *routeTestConnector) Attach(_ context.Context, record routeRecord) (rout
410 return routeTransitionCommit{}, c.attachErr411 return routeTransitionCommit{}, c.attachErr
411 }412 }
412 return routeTransitionCommit{413 return routeTransitionCommit{
413- identity: ports.CommittedRouteIdentity{Target: record.target},414+ identity: protocol.CommittedRouteIdentity{Target: record.target},
414 presentation: routePresentation{415 presentation: routePresentation{
415 name: record.presentation.name,416 name: record.presentation.name,
416 kind: record.presentation.kind,417 kind: record.presentation.kind,
417- reachability: ports.RouteReachabilityReachable,418+ reachability: protocol.RouteReachabilityReachable,
418 },419 },
419 resumeToken: 123,420 resumeToken: 123,
420 }, nil421 }, nil
@@ -422,12 +423,12 @@ func (c *routeTestConnector) Attach(_ context.Context, record routeRecord) (rout
422 423
423 func TestRouteLedgerNavigateResumesThenFallsBackAndRestoresOrigin(t *testing.T) {424 func TestRouteLedgerNavigateResumesThenFallsBackAndRestoresOrigin(t *testing.T) {
424 ledger := newRouteLedger()425 ledger := newRouteLedger()
425- candidate := routeTestCandidate(2, ports.RouteOriginDiscovery)426+ candidate := routeTestCandidate(2, protocol.RouteOriginDiscovery)
426 candidate.originKey = "daemon-discovered"427 candidate.originKey = "daemon-discovered"
427 candidate.resumeToken = 77428 candidate.resumeToken = 77
428 identity, err := ledger.commit(candidate)429 identity, err := ledger.commit(candidate)
429 require.NoError(t, err)430 require.NoError(t, err)
430- _, err = ledger.commit(routeTestCandidate(4, ports.RouteOriginLocal))431+ _, err = ledger.commit(routeTestCandidate(4, protocol.RouteOriginLocal))
431 require.NoError(t, err)432 require.NoError(t, err)
432 433
433 connector := &routeTestConnector{resumeErr: errRouteResumeUnavailable}434 connector := &routeTestConnector{resumeErr: errRouteResumeUnavailable}
@@ -440,18 +441,18 @@ func TestRouteLedgerNavigateResumesThenFallsBackAndRestoresOrigin(t *testing.T)
440 441
441 active, ok := ledger.lookup(ledger.activeRef())442 active, ok := ledger.lookup(ledger.activeRef())
442 require.True(t, ok)443 require.True(t, ok)
443- require.Equal(t, ports.RouteOriginDiscovery, active.origin)444+ require.Equal(t, protocol.RouteOriginDiscovery, active.origin)
444 require.Equal(t, "daemon-discovered", active.originKey)445 require.Equal(t, "daemon-discovered", active.originKey)
445- require.Equal(t, ports.RouteOriginDiscovery, active.request.Origin)446+ require.Equal(t, protocol.RouteOriginDiscovery, active.request.Origin)
446 require.Equal(t, candidate.target, active.target)447 require.Equal(t, candidate.target, active.target)
447 require.Greater(t, active.identity.generation, identity.generation)448 require.Greater(t, active.identity.generation, identity.generation)
448 }449 }
449 450
450 func TestRouteLedgerNavigateIsTransactionalOnFailureOrTargetChange(t *testing.T) {451 func TestRouteLedgerNavigateIsTransactionalOnFailureOrTargetChange(t *testing.T) {
451 ledger := newRouteLedger()452 ledger := newRouteLedger()
452- identity, err := ledger.commit(routeTestCandidate(3, ports.RouteOriginLocal))453+ identity, err := ledger.commit(routeTestCandidate(3, protocol.RouteOriginLocal))
453 require.NoError(t, err)454 require.NoError(t, err)
454- _, err = ledger.commit(routeTestCandidate(5, ports.RouteOriginLocal))455+ _, err = ledger.commit(routeTestCandidate(5, protocol.RouteOriginLocal))
455 require.NoError(t, err)456 require.NoError(t, err)
456 before := ledger.snapshot()457 before := ledger.snapshot()
457 action := identity.wireNavigationAction(routeGeneration(before.Generation))458 action := identity.wireNavigationAction(routeGeneration(before.Generation))
@@ -475,7 +476,7 @@ func TestRouteLedgerNavigateIsTransactionalOnFailureOrTargetChange(t *testing.T)
475 476
476 func TestRouteLedgerNavigationRejectsStaleActionsAndActiveNoOp(t *testing.T) {477 func TestRouteLedgerNavigationRejectsStaleActionsAndActiveNoOp(t *testing.T) {
477 ledger := newRouteLedger()478 ledger := newRouteLedger()
478- active, err := ledger.commit(routeTestCandidate(6, ports.RouteOriginLocal))479+ active, err := ledger.commit(routeTestCandidate(6, protocol.RouteOriginLocal))
479 require.NoError(t, err)480 require.NoError(t, err)
480 firstSnapshot := ledger.snapshot()481 firstSnapshot := ledger.snapshot()
481 before := ledger.snapshot()482 before := ledger.snapshot()
@@ -483,7 +484,7 @@ func TestRouteLedgerNavigationRejectsStaleActionsAndActiveNoOp(t *testing.T) {
483 require.NoError(t, ledger.navigate(context.Background(), active.wireNavigationAction(routeGeneration(firstSnapshot.Generation)), nil))484 require.NoError(t, ledger.navigate(context.Background(), active.wireNavigationAction(routeGeneration(firstSnapshot.Generation)), nil))
484 require.Equal(t, before, ledger.snapshot())485 require.Equal(t, before, ledger.snapshot())
485 486
486- _, err = ledger.commit(routeTestCandidate(7, ports.RouteOriginLocal))487+ _, err = ledger.commit(routeTestCandidate(7, protocol.RouteOriginLocal))
487 require.NoError(t, err)488 require.NoError(t, err)
488 stale := active.wireNavigationAction(routeGeneration(firstSnapshot.Generation))489 stale := active.wireNavigationAction(routeGeneration(firstSnapshot.Generation))
489 connector := &routeTestConnector{}490 connector := &routeTestConnector{}
@@ -496,13 +497,13 @@ type mismatchingRouteConnector struct{ *routeTestConnector }
496 497
497 func (c mismatchingRouteConnector) Attach(_ context.Context, record routeRecord) (routeTransitionCommit, error) {498 func (c mismatchingRouteConnector) Attach(_ context.Context, record routeRecord) (routeTransitionCommit, error) {
498 return routeTransitionCommit{499 return routeTransitionCommit{
499- identity: ports.CommittedRouteIdentity{Target: routeTestTarget(99)},500+ identity: protocol.CommittedRouteIdentity{Target: routeTestTarget(99)},
500 presentation: routePresentation{name: record.presentation.name, kind: record.presentation.kind},501 presentation: routePresentation{name: record.presentation.name, kind: record.presentation.kind},
501 }, nil502 }, nil
502 }503 }
503 504
504-func (i routeIdentity) wireNavigationAction(snapshotGeneration routeGeneration) ports.RouteNavigationAction {505+func (i routeIdentity) wireNavigationAction(snapshotGeneration routeGeneration) protocol.RouteNavigationAction {
505- return ports.RouteNavigationAction{SnapshotGeneration: uint64(snapshotGeneration), Key: uint64(i.key), Generation: uint64(i.generation)}506+ return protocol.RouteNavigationAction{SnapshotGeneration: uint64(snapshotGeneration), Key: uint64(i.key), Generation: uint64(i.generation)}
506 }507 }
507 508
508 func TestRouteLedgerConcurrentSnapshotsAndCommits(t *testing.T) {509 func TestRouteLedgerConcurrentSnapshotsAndCommits(t *testing.T) {
@@ -513,7 +514,7 @@ func TestRouteLedgerConcurrentSnapshotsAndCommits(t *testing.T) {
513 go func() {514 go func() {
514 defer wg.Done()515 defer wg.Done()
515 for i := byte(0); i < 20; i++ {516 for i := byte(0); i < 20; i++ {
516- _, _ = ledger.commit(routeTestCandidate(worker*20+i, ports.RouteOriginLocal))517+ _, _ = ledger.commit(routeTestCandidate(worker*20+i, protocol.RouteOriginLocal))
517 _ = ledger.snapshot()518 _ = ledger.snapshot()
518 }519 }
519 }()520 }()
@@ -524,7 +525,7 @@ func TestRouteLedgerConcurrentSnapshotsAndCommits(t *testing.T) {
524 525
525 func TestRouteLedgerSeparatesRemoteDaemonOrigins(t *testing.T) {526 func TestRouteLedgerSeparatesRemoteDaemonOrigins(t *testing.T) {
526 ledger := newRouteLedger()527 ledger := newRouteLedger()
527- firstCandidate := routeTestCandidate(40, ports.RouteOriginRemote)528+ firstCandidate := routeTestCandidate(40, protocol.RouteOriginRemote)
528 firstCandidate.originKey = "daemon-a"529 firstCandidate.originKey = "daemon-a"
529 secondCandidate := firstCandidate530 secondCandidate := firstCandidate
530 secondCandidate.originKey = "daemon-b"531 secondCandidate.originKey = "daemon-b"
M internal/usecase/client/same_peer_switch.go +2 -2
@@ -4,7 +4,7 @@ import (
4 "context"4 "context"
5 "sync"5 "sync"
6 6
7- "github.com/bnema/vev/internal/ports"7+ "github.com/bnema/vev/internal/protocol"
8 )8 )
9 9
10 // samePeerInputGate holds raw input behind a requested in-band switch. It does10 // samePeerInputGate holds raw input behind a requested in-band switch. It does
@@ -20,7 +20,7 @@ type samePeerInputGate struct {
20 20
21 type samePeerSwitchPending struct {21 type samePeerSwitchPending struct {
22 requestID uint6422 requestID uint64
23- target ports.ExactSessionTarget23+ target protocol.ExactSessionTarget
24 }24 }
25 25
26 func newSamePeerInputGate() *samePeerInputGate {26 func newSamePeerInputGate() *samePeerInputGate {
M internal/usecase/client/same_peer_switch_test.go +2 -1
@@ -9,6 +9,7 @@ import (
9 9
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 )13 )
13 14
14 func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {15 func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
@@ -33,7 +34,7 @@ func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
33 resultCh := make(chan attachResult, 1)34 resultCh := make(chan attachResult, 1)
34 go func() { resultCh <- attempt.run(ctx) }()35 go func() { resultCh <- attempt.run(ctx) }()
35 36
36- exact := ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{7}, SessionName: "stopped"}37+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{7}, SessionName: "stopped"}
37 target := ports.AttachTarget{38 target := ports.AttachTarget{
38 Session: "stopped", Intent: ports.IntentAttach, ExactTarget: &exact,39 Session: "stopped", Intent: ports.IntentAttach, ExactTarget: &exact,
39 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,40 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
M internal/usecase/client/transport_observability_test.go +3 -2
@@ -14,6 +14,7 @@ import (
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16 portsmocks "github.com/bnema/vev/internal/ports/mocks"16 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
17 "github.com/bnema/vev/internal/usecase/client"18 "github.com/bnema/vev/internal/usecase/client"
18 )19 )
19 20
@@ -50,7 +51,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
50 transport.EXPECT().Send(isType(ports.MsgAck)).Run(func(ports.Frame) { close(acked) }).Return(nil).Once()51 transport.EXPECT().Send(isType(ports.MsgAck)).Run(func(ports.Frame) { close(acked) }).Return(nil).Once()
51 unblock := scriptRecv(transport,52 unblock := scriptRecv(transport,
52 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},53 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
53- recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("flush-before-observe"), New: 3}))},54+ recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("flush-before-observe"), New: 3}))},
54 )55 )
55 defer unblock()56 defer unblock()
56 transport.EXPECT().Close().Return(nil).Once()57 transport.EXPECT().Close().Return(nil).Once()
@@ -90,7 +91,7 @@ func TestTerminalFlushBoundaryTransportObservability(t *testing.T) {
90 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()91 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
91 unblock := scriptRecv(transport,92 unblock := scriptRecv(transport,
92 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},93 recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
93- recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(ports.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("unchanged-by-observer"), New: 3}))},94+ recvItem{f: frameOf(ports.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("unchanged-by-observer"), New: 3}))},
94 recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},95 recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
95 )96 )
96 defer unblock()97 defer unblock()
M internal/usecase/client/wire_external_test_helpers_test.go +5 -2
@@ -1,8 +1,11 @@
1 package client_test1 package client_test
2 2
3-import "github.com/bnema/vev/internal/ports"3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
4 7
5-func mustMarshalOutput(m ports.Output) []byte {8+func mustMarshalOutput(m protocol.Output) []byte {
6 payload, err := ports.MarshalOutput(m)9 payload, err := ports.MarshalOutput(m)
7 if err != nil {10 if err != nil {
8 panic(err)11 panic(err)
M internal/usecase/client/wire_test_helpers_test.go +5 -2
@@ -1,8 +1,11 @@
1 package client1 package client
2 2
3-import "github.com/bnema/vev/internal/ports"3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
4 7
5-func mustMarshalOutput(m ports.Output) []byte {8+func mustMarshalOutput(m protocol.Output) []byte {
6 payload, err := ports.MarshalOutput(m)9 payload, err := ports.MarshalOutput(m)
7 if err != nil {10 if err != nil {
8 panic(err)11 panic(err)
M internal/usecase/daemon/attachment_effect_gate_test.go +14 -13
@@ -12,6 +12,7 @@ import (
12 renderer "github.com/bnema/vev-vt"12 renderer "github.com/bnema/vev-vt"
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
15 "github.com/bnema/vev/internal/usecase/keys"16 "github.com/bnema/vev/internal/usecase/keys"
16 )17 )
17 18
@@ -611,7 +612,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes
611 },612 },
612 {613 {
613 name: "theme",614 name: "theme",
614- frame: ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(ports.Theme{615+ frame: ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(protocol.Theme{
615 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},616 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
616 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},617 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
617 })},618 })},
@@ -624,7 +625,7 @@ func TestAttachmentEffectGateAdmittedActiveEffectsFinishBeforeReplacement(t *tes
624 setup: func(_ *Daemon, _ *session, ac *attachedClient, _ *transactionalResizePTY) {625 setup: func(_ *Daemon, _ *session, ac *attachedClient, _ *transactionalResizePTY) {
625 ac.output.next = 3626 ac.output.next = 3
626 },627 },
627- frame: ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(ports.Ack{Epoch: 1, State: 1})},628+ frame: ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(protocol.Ack{Epoch: 1, State: 1})},
628 assert: func(t *testing.T, ac *attachedClient, _ *transactionalResizePTY) {629 assert: func(t *testing.T, ac *attachedClient, _ *transactionalResizePTY) {
629 ac.sendMu.Lock()630 ac.sendMu.Lock()
630 defer ac.sendMu.Unlock()631 defer ac.sendMu.Unlock()
@@ -687,14 +688,14 @@ func TestAttachedRouteSnapshotIsAcceptedAsAnAttachmentValue(t *testing.T) {
687 token.lease = rc.attachmentLease(ac)688 token.lease = rc.attachmentLease(ac)
688 ac.installTestAttachmentCapability(token)689 ac.installTestAttachmentCapability(token)
689 690
690- activeRef := ports.RouteRef{Key: 8, Generation: 4}691+ activeRef := protocol.RouteRef{Key: 8, Generation: 4}
691 activeTarget := testRouteTarget(source.name, 8)692 activeTarget := testRouteTarget(source.name, 8)
692- snapshot := ports.RecentRouteSnapshot{693+ snapshot := protocol.RecentRouteSnapshot{
693 Generation: 4,694 Generation: 4,
694 Active: activeRef,695 Active: activeRef,
695- ActiveEntry: ports.RecentRouteEntry{Key: 8, Generation: 4, Target: activeTarget, Name: source.name, Kind: ports.RouteKindLocal},696+ ActiveEntry: protocol.RecentRouteEntry{Key: 8, Generation: 4, Target: activeTarget, Name: source.name, Kind: protocol.RouteKindLocal},
696 Home: activeRef,697 Home: activeRef,
697- Entries: []ports.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, ports.RouteKindLocal)},698+ Entries: []protocol.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, protocol.RouteKindLocal)},
698 }699 }
699 payload, err := ports.MarshalRecentRouteSnapshot(snapshot)700 payload, err := ports.MarshalRecentRouteSnapshot(snapshot)
700 require.NoError(t, err)701 require.NoError(t, err)
@@ -711,12 +712,12 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
711 d.mu.Lock()712 d.mu.Lock()
712 d.sessions[target.id] = target713 d.sessions[target.id] = target
713 d.mu.Unlock()714 d.mu.Unlock()
714- ac.setRouteSnapshot(ports.RecentRouteSnapshot{715+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
715 Generation: 2,716 Generation: 2,
716- Active: ports.RouteRef{Key: 2, Generation: 2},717+ Active: protocol.RouteRef{Key: 2, Generation: 2},
717- ActiveEntry: testRouteEntry(2, 2, source.name, 2, ports.RouteKindLocal),718+ ActiveEntry: testRouteEntry(2, 2, source.name, 2, protocol.RouteKindLocal),
718- Previous: ports.RouteRef{Key: 1, Generation: 1},719+ Previous: protocol.RouteRef{Key: 1, Generation: 1},
719- Entries: []ports.RecentRouteEntry{testRouteEntry(1, 1, "target", 1, ports.RouteKindLocal)},720+ Entries: []protocol.RecentRouteEntry{testRouteEntry(1, 1, "target", 1, protocol.RouteKindLocal)},
720 })721 })
721 722
722 transport := &closeTrackingTransport{}723 transport := &closeTrackingTransport{}
@@ -744,7 +745,7 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
744 break745 break
745 }746 }
746 require.True(t, result.OK, result.Text)747 require.True(t, result.OK, result.Text)
747- var action ports.RouteNavigationAction748+ var action protocol.RouteNavigationAction
748 for _, frame := range frames {749 for _, frame := range frames {
749 if frame.Type != ports.MsgNavigateRecentRoute {750 if frame.Type != ports.MsgNavigateRecentRoute {
750 continue751 continue
@@ -752,7 +753,7 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
752 action, err = ports.UnmarshalRouteNavigationAction(frame.Payload)753 action, err = ports.UnmarshalRouteNavigationAction(frame.Payload)
753 require.NoError(t, err)754 require.NoError(t, err)
754 }755 }
755- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}, action)756+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 2, Key: 1, Generation: 1}, action)
756 require.False(t, transport.Closed(), "client navigation must not tear down the attachment")757 require.False(t, transport.Closed(), "client navigation must not tear down the attachment")
757 }758 }
758 759
M internal/usecase/daemon/attachment_output.go +5 -4
@@ -9,6 +9,7 @@ import (
9 renderer "github.com/bnema/vev-vt/ansi"9 renderer "github.com/bnema/vev-vt/ansi"
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 )13 )
13 14
14 // attachmentOutput owns the terminal-output dependency chain and emitted15 // attachmentOutput owns the terminal-output dependency chain and emitted
@@ -32,7 +33,7 @@ type attachmentOutput struct {
32 forceSnapshot bool33 forceSnapshot bool
33 initialized bool34 initialized bool
34 lastCursor cursorOut35 lastCursor cursorOut
35- lastRoutePosition ports.RoutePosition36+ lastRoutePosition protocol.RoutePosition
36 graphicsOutput *graphicsOutputState37 graphicsOutput *graphicsOutputState
37 graphicsUnsupportedWarned atomic.Bool38 graphicsUnsupportedWarned atomic.Bool
38 attachment *attachedClient39 attachment *attachedClient
@@ -139,7 +140,7 @@ func (o *attachmentOutput) prepareFrame(d *Daemon, state *capturedRenderState, f
139 }140 }
140 cursor := o.prepareCursorTail(desired, len(ansi.data) > 0)141 cursor := o.prepareCursorTail(desired, len(ansi.data) > 0)
141 graphicsReset := reset || o.forceSnapshot || !o.initialized142 graphicsReset := reset || o.forceSnapshot || !o.initialized
142- graphicsBudget := max(ports.MaxOutputDataLen-len(ansi.data)-len(cursor.data), 0)143+ graphicsBudget := max(protocol.MaxOutputDataLen-len(ansi.data)-len(cursor.data), 0)
143 preparedGraphics, err := graphicsOutputDataWithDaemonLimit(d, state, o.attachment, graphicsReset, graphicsBudget)144 preparedGraphics, err := graphicsOutputDataWithDaemonLimit(d, state, o.attachment, graphicsReset, graphicsBudget)
144 if err != nil {145 if err != nil {
145 return nil, err146 return nil, err
@@ -379,7 +380,7 @@ func (p *preparedOutput) currentLocked() bool {
379 }380 }
380 381
381 func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (ports.Frame, error) {382 func marshalOutputState(data []byte, epoch, base, next, echoAck, viewRevision uint64, size domain.Size, full bool) (ports.Frame, error) {
382- payload, err := ports.MarshalOutput(ports.Output{383+ payload, err := ports.MarshalOutput(protocol.Output{
383 Epoch: epoch, Base: base, New: next, Echo: echoAck, ViewRevision: viewRevision,384 Epoch: epoch, Base: base, New: next, Echo: echoAck, ViewRevision: viewRevision,
384 Size: size, Full: full, Data: data,385 Size: size, Full: full, Data: data,
385 })386 })
@@ -440,7 +441,7 @@ func (s *attachmentOutput) rebaseAttachment() {
440 return441 return
441 }442 }
442 s.rebase()443 s.rebase()
443- s.lastRoutePosition = ports.RoutePosition{}444+ s.lastRoutePosition = protocol.RoutePosition{}
444 }445 }
445 446
446 // rebaseLocked is called while the attachment view lock is held. Publishing447 // rebaseLocked is called while the attachment view lock is held. Publishing
M internal/usecase/daemon/attachment_output_test.go +3 -2
@@ -10,6 +10,7 @@ import (
10 "github.com/bnema/vev-vt/graphics"10 "github.com/bnema/vev-vt/graphics"
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12 "github.com/bnema/vev/internal/ports"12 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
13 "github.com/bnema/vev/internal/usecase/layout"14 "github.com/bnema/vev/internal/usecase/layout"
14 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
15 )16 )
@@ -468,7 +469,7 @@ func TestAttachmentOutputRebaseRetiresAttachmentState(t *testing.T) {
468 output.next = 3469 output.next = 3
469 output.acked = 2470 output.acked = 2
470 output.lastCursor = cursorOut{valid: true, row: 2, col: 3}471 output.lastCursor = cursorOut{valid: true, row: 2, col: 3}
471- output.lastRoutePosition = ports.RoutePosition{ActiveTabID: "tab"}472+ output.lastRoutePosition = protocol.RoutePosition{ActiveTabID: "tab"}
472 473
473 output.rebaseAttachment()474 output.rebaseAttachment()
474 475
@@ -477,7 +478,7 @@ func TestAttachmentOutputRebaseRetiresAttachmentState(t *testing.T) {
477 require.Zero(t, output.acked)478 require.Zero(t, output.acked)
478 require.True(t, output.forceSnapshot)479 require.True(t, output.forceSnapshot)
479 require.Equal(t, cursorOut{valid: true, row: 2, col: 3}, output.lastCursor, "the forced frame will reassert cursor state")480 require.Equal(t, cursorOut{valid: true, row: 2, col: 3}, output.lastCursor, "the forced frame will reassert cursor state")
480- require.Equal(t, ports.RoutePosition{}, output.lastRoutePosition)481+ require.Equal(t, protocol.RoutePosition{}, output.lastRoutePosition)
481 }482 }
482 483
483 func TestAttachmentOutputDefaultsAndNormalizesWindow(t *testing.T) {484 func TestAttachmentOutputDefaultsAndNormalizesWindow(t *testing.T) {
M internal/usecase/daemon/attachment_parking_test.go +2 -1
@@ -7,6 +7,7 @@ import (
7 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
8 8
9 "github.com/bnema/vev/internal/ports"9 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
10 )11 )
11 12
12 type resumeWelcomeFailureTransport struct {13 type resumeWelcomeFailureTransport struct {
@@ -137,7 +138,7 @@ func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
137 sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)138 sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)
138 require.NoError(t, err)139 require.NoError(t, err)
139 oldToken := ac.resumeToken140 oldToken := ac.resumeToken
140- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})141+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
141 d.clientGone(sess, ac, oldTransport, false)142 d.clientGone(sess, ac, oldTransport, false)
142 143
143 resumedTransport := &closeTrackingTransport{}144 resumedTransport := &closeTrackingTransport{}
M internal/usecase/daemon/attachment_render_test.go +2 -1
@@ -9,6 +9,7 @@ import (
9 9
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
13 )14 )
14 15
@@ -285,7 +286,7 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
285 token := sess.captureAttachmentCapability(ac, tr)286 token := sess.captureAttachmentCapability(ac, tr)
286 d.handleAttachmentClientFrame(token, ports.Frame{287 d.handleAttachmentClientFrame(token, ports.Frame{
287 Type: ports.MsgResize,288 Type: ports.MsgResize,
288- Payload: mustMarshalResize(ports.Resize{Size: size}),289+ Payload: mustMarshalResize(protocol.Resize{Size: size}),
289 })290 })
290 }291 }
291 resize(second, secondTransport, domain.Size{Cols: 120, Rows: 50})292 resize(second, secondTransport, domain.Size{Cols: 120, Rows: 50})
M internal/usecase/daemon/attachment_route_identity_test.go +4 -3
@@ -6,6 +6,7 @@ import (
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
10 )11 )
11 12
@@ -28,7 +29,7 @@ func TestAttachmentTransitionPublishesCommittedRouteIdentity(t *testing.T) {
28 token := source.captureAttachmentCapability(ac, transport)29 token := source.captureAttachmentCapability(ac, transport)
29 token.lease = rc.attachmentLease(ac)30 token.lease = rc.attachmentLease(ac)
30 ac.installTestAttachmentCapability(token)31 ac.installTestAttachmentCapability(token)
31- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})32+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
32 33
33 target := &session{34 target := &session{
34 sessionCore: sessionCore{35 sessionCore: sessionCore{
@@ -54,7 +55,7 @@ func TestAttachmentTransitionPublishesCommittedRouteIdentity(t *testing.T) {
54 require.NoError(t, err)55 require.NoError(t, err)
55 d.deferAttachmentTransitionCleanups(transition)56 d.deferAttachmentTransitionCleanups(transition)
56 57
57- var identity ports.CommittedRouteIdentity58+ var identity protocol.CommittedRouteIdentity
58 found := false59 found := false
59 for _, frame := range transport.Sends() {60 for _, frame := range transport.Sends() {
60 if frame.Type != ports.MsgCommittedRouteIdentity {61 if frame.Type != ports.MsgCommittedRouteIdentity {
@@ -78,7 +79,7 @@ func TestAttachmentTransitionTearsDownAfterCommittedIdentitySendFailure(t *testi
78 token := source.captureAttachmentCapability(ac, transport)79 token := source.captureAttachmentCapability(ac, transport)
79 token.lease = rc.attachmentLease(ac)80 token.lease = rc.attachmentLease(ac)
80 ac.installTestAttachmentCapability(token)81 ac.installTestAttachmentCapability(token)
81- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})82+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
82 83
83 target := &session{84 target := &session{
84 sessionCore: sessionCore{85 sessionCore: sessionCore{
M internal/usecase/daemon/client.go +19 -18
@@ -13,6 +13,7 @@ import (
13 13
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
16 "github.com/bnema/vev/internal/usecase/keys"17 "github.com/bnema/vev/internal/usecase/keys"
17 "github.com/bnema/vev/internal/usecase/mouse"18 "github.com/bnema/vev/internal/usecase/mouse"
18 themeui "github.com/bnema/vev/internal/usecase/theme"19 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -40,8 +41,8 @@ type attachedClient struct {
40 overlayOnce sync.Once41 overlayOnce sync.Once
41 clientID [16]byte42 clientID [16]byte
42 terminalCapabilities ports.TerminalCapabilities43 terminalCapabilities ports.TerminalCapabilities
43- navigationCapabilities ports.NavigationCapabilities44+ navigationCapabilities protocol.NavigationCapabilities
44- startupOverlay ports.StartupOverlay45+ startupOverlay protocol.StartupOverlay
45 // lifecycle is the sole authority for attachment capability publication,46 // lifecycle is the sole authority for attachment capability publication,
46 // effect admission, transition freeze/drain, and connection generation.47 // effect admission, transition freeze/drain, and connection generation.
47 lifecycle attachmentLifecycle48 lifecycle attachmentLifecycle
@@ -86,15 +87,15 @@ type attachedClient struct {
86 linkMu sync.Mutex87 linkMu sync.Mutex
87 sendMu sync.Mutex88 sendMu sync.Mutex
88 routeMu sync.RWMutex89 routeMu sync.RWMutex
89- routeSnapshot ports.RecentRouteSnapshot90+ routeSnapshot protocol.RecentRouteSnapshot
90 pendingRouteIdentity bool91 pendingRouteIdentity bool
91 samePeerOfferMu sync.Mutex92 samePeerOfferMu sync.Mutex
92- samePeerOffer *ports.ExactSessionTarget93+ samePeerOffer *protocol.ExactSessionTarget
93 parkedRouteMu sync.Mutex94 parkedRouteMu sync.Mutex
94 parkedRoute *parkedRouteLease95 parkedRoute *parkedRouteLease
95 parkedRouteOutput atomic.Bool96 parkedRouteOutput atomic.Bool
96 parkedRouteFullPending atomic.Bool97 parkedRouteFullPending atomic.Bool
97- routeAttentionSubscription ports.RouteAttentionSubscription98+ routeAttentionSubscription protocol.RouteAttentionSubscription
98 // routeCreatedSession marks a session created by this attachment's route.99 // routeCreatedSession marks a session created by this attachment's route.
99 // A handshake that never commits Welcome must tear down that exact empty100 // A handshake that never commits Welcome must tear down that exact empty
100 // session, while an attachment routed to an existing session must not.101 // session, while an attachment routed to an existing session must not.
@@ -336,8 +337,8 @@ func (ac *attachedClient) currentTransportIs(tr ports.Transport) bool {
336 337
337 // setRouteSnapshot records a client-owned route view and reports whether a338 // setRouteSnapshot records a client-owned route view and reports whether a
338 // rename identity was deferred until that first usable publication.339 // rename identity was deferred until that first usable publication.
339-func (ac *attachedClient) setRouteSnapshot(snapshot ports.RecentRouteSnapshot) bool {340+func (ac *attachedClient) setRouteSnapshot(snapshot protocol.RecentRouteSnapshot) bool {
340- snapshot.Entries = append([]ports.RecentRouteEntry(nil), snapshot.Entries...)341+ snapshot.Entries = append([]protocol.RecentRouteEntry(nil), snapshot.Entries...)
341 ac.routeMu.Lock()342 ac.routeMu.Lock()
342 ac.routeSnapshot = snapshot343 ac.routeSnapshot = snapshot
343 replayPendingIdentity := snapshot.Generation != 0 && ac.pendingRouteIdentity344 replayPendingIdentity := snapshot.Generation != 0 && ac.pendingRouteIdentity
@@ -361,22 +362,22 @@ func (ac *attachedClient) deferRouteIdentityUntilSnapshot() bool {
361 return true362 return true
362 }363 }
363 364
364-func (ac *attachedClient) routeSnapshotCopy() ports.RecentRouteSnapshot {365+func (ac *attachedClient) routeSnapshotCopy() protocol.RecentRouteSnapshot {
365 ac.routeMu.RLock()366 ac.routeMu.RLock()
366 defer ac.routeMu.RUnlock()367 defer ac.routeMu.RUnlock()
367 snapshot := ac.routeSnapshot368 snapshot := ac.routeSnapshot
368- snapshot.Entries = append([]ports.RecentRouteEntry(nil), snapshot.Entries...)369+ snapshot.Entries = append([]protocol.RecentRouteEntry(nil), snapshot.Entries...)
369 return snapshot370 return snapshot
370 }371 }
371 372
372-func (ac *attachedClient) setRouteAttentionSubscription(subscription ports.RouteAttentionSubscription) {373+func (ac *attachedClient) setRouteAttentionSubscription(subscription protocol.RouteAttentionSubscription) {
373- subscription.Targets = append([]ports.RouteAttentionTarget(nil), subscription.Targets...)374+ subscription.Targets = append([]protocol.RouteAttentionTarget(nil), subscription.Targets...)
374 ac.routeMu.Lock()375 ac.routeMu.Lock()
375 ac.routeAttentionSubscription = subscription376 ac.routeAttentionSubscription = subscription
376 ac.routeMu.Unlock()377 ac.routeMu.Unlock()
377 }378 }
378 379
379-func (ac *attachedClient) routeAttentionTarget(ref ports.RouteRef) (ports.ExactSessionTarget, bool) {380+func (ac *attachedClient) routeAttentionTarget(ref protocol.RouteRef) (protocol.ExactSessionTarget, bool) {
380 ac.routeMu.RLock()381 ac.routeMu.RLock()
381 defer ac.routeMu.RUnlock()382 defer ac.routeMu.RUnlock()
382 for _, target := range ac.routeAttentionSubscription.Targets {383 for _, target := range ac.routeAttentionSubscription.Targets {
@@ -384,7 +385,7 @@ func (ac *attachedClient) routeAttentionTarget(ref ports.RouteRef) (ports.ExactS
384 return target.Target, true385 return target.Target, true
385 }386 }
386 }387 }
387- return ports.ExactSessionTarget{}, false388+ return protocol.ExactSessionTarget{}, false
388 }389 }
389 390
390 func (ac *attachedClient) ackOutputState(epoch, state uint64) {391 func (ac *attachedClient) ackOutputState(epoch, state uint64) {
@@ -613,8 +614,8 @@ type attachClientOptions struct {
613 maxOutputInFlight uint8614 maxOutputInFlight uint8
614 terminalCapabilities ports.TerminalCapabilities615 terminalCapabilities ports.TerminalCapabilities
615 capabilitiesSet bool616 capabilitiesSet bool
616- navigationCapabilities ports.NavigationCapabilities617+ navigationCapabilities protocol.NavigationCapabilities
617- startupOverlay ports.StartupOverlay618+ startupOverlay protocol.StartupOverlay
618 }619 }
619 620
620 func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) {621 func (d *Daemon) attachClient(sess *session, tr ports.Transport, sz domain.Size, opts attachClientOptions) (*attachedClient, error) {
@@ -882,7 +883,7 @@ func (d *Daemon) firstPaintWithLease(sess *session, ac *attachedClient, lease *a
882 883
883 // detachIfCurrent clears the client iff ac is the current one, reporting884 // detachIfCurrent clears the client iff ac is the current one, reporting
884 885
885-func themeFromMessage(msg ports.Theme) themeui.Theme {886+func themeFromMessage(msg protocol.Theme) themeui.Theme {
886 return themeui.Theme{887 return themeui.Theme{
887 Foreground: msg.Foreground,888 Foreground: msg.Foreground,
888 Background: msg.Background,889 Background: msg.Background,
@@ -897,7 +898,7 @@ func themeFromMessage(msg ports.Theme) themeui.Theme {
897 }898 }
898 }899 }
899 900
900-func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg ports.Theme) {901+func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg protocol.Theme) {
901 clientTheme := themeFromMessage(msg)902 clientTheme := themeFromMessage(msg)
902 ac.setClientTheme(clientTheme)903 ac.setClientTheme(clientTheme)
903 904
@@ -907,7 +908,7 @@ func (d *Daemon) applyTheme(sess *session, ac *attachedClient, msg ports.Theme)
907 d.invalidateRender(sess, ac, true, "client.go")908 d.invalidateRender(sess, ac, true, "client.go")
908 }909 }
909 910
910-func (d *Daemon) applyThemeForAttachment(effect *attachmentEffect, msg ports.Theme) {911+func (d *Daemon) applyThemeForAttachment(effect *attachmentEffect, msg protocol.Theme) {
911 if !effect.current() {912 if !effect.current() {
912 return913 return
913 }914 }
M internal/usecase/daemon/client_frame_routing.go +6 -5
@@ -6,6 +6,7 @@ import (
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
9 "github.com/bnema/vev/internal/usecase/command"10 "github.com/bnema/vev/internal/usecase/command"
10 )11 )
11 12
@@ -169,15 +170,15 @@ func (d *Daemon) handleAttachmentClientFrame(capability attachmentCapability, f
169 }170 }
170 message := "route navigation failed"171 message := "route navigation failed"
171 switch failure.Code {172 switch failure.Code {
172- case ports.RouteFailureStaleSelection:173+ case protocol.RouteFailureStaleSelection:
173 message = "that recent route is no longer available"174 message = "that recent route is no longer available"
174- case ports.RouteFailureTargetChanged:175+ case protocol.RouteFailureTargetChanged:
175 message = "that recent route changed before attach"176 message = "that recent route changed before attach"
176- case ports.RouteFailureOriginUnavailable:177+ case protocol.RouteFailureOriginUnavailable:
177 message = "the original route is no longer available"178 message = "the original route is no longer available"
178- case ports.RouteFailureUnavailable:179+ case protocol.RouteFailureUnavailable:
179 message = "the selected route is unavailable"180 message = "the selected route is unavailable"
180- case ports.RouteFailureNoSuchRoute:181+ case protocol.RouteFailureNoSuchRoute:
181 message = "that recent route no longer exists"182 message = "that recent route no longer exists"
182 }183 }
183 d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, message, nil)184 d.notify(effect.sess, domain.NoticeWarn, domain.NoticeSessionUnavailable, message, nil)
M internal/usecase/daemon/client_transport_errors.go +3 -1
@@ -1,6 +1,8 @@
1 package daemon1 package daemon
2 2
3-import "github.com/bnema/vev/internal/ports"3+import (
4+ "github.com/bnema/vev/internal/ports"
5+)
4 6
5 // clientGone detaches ac if it is still the session's current client. The7 // clientGone detaches ac if it is still the session's current client. The
6 // session remains registered and headless after the client is gone.8 // session remains registered and headless after the client is gone.
M internal/usecase/daemon/clipboard.go +7 -6
@@ -8,6 +8,7 @@ import (
8 "os"8 "os"
9 9
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 scopy "github.com/bnema/vev/internal/usecase/copy"12 scopy "github.com/bnema/vev/internal/usecase/copy"
12 )13 )
13 14
@@ -26,14 +27,14 @@ var (
26 clipPasteCloseMarker = []byte("\x1b[201~")27 clipPasteCloseMarker = []byte("\x1b[201~")
27 )28 )
28 29
29-func (d *Daemon) handleSequencedImagePushForAttachment(effect *attachmentEffect, _ uint64, ip ports.ImagePush) {30+func (d *Daemon) handleSequencedImagePushForAttachment(effect *attachmentEffect, _ uint64, ip protocol.ImagePush) {
30 if !effect.current() {31 if !effect.current() {
31 return32 return
32 }33 }
33 d.handleImagePushForAttachment(effect, ip)34 d.handleImagePushForAttachment(effect, ip)
34 }35 }
35 36
36-func (d *Daemon) handleImagePushForAttachment(effect *attachmentEffect, ip ports.ImagePush) {37+func (d *Daemon) handleImagePushForAttachment(effect *attachmentEffect, ip protocol.ImagePush) {
37 if len(ip.Data) == 0 || len(ip.Data) > maxImagePushSize || !effect.current() {38 if len(ip.Data) == 0 || len(ip.Data) > maxImagePushSize || !effect.current() {
38 return39 return
39 }40 }
@@ -55,7 +56,7 @@ func (d *Daemon) handleImagePushForAttachment(effect *attachmentEffect, ip ports
55 // failure is logged and the push is dropped: the client has already56 // failure is logged and the push is dropped: the client has already
56 // consumed the Ctrl+V keystroke that triggered it, so there is nothing left57 // consumed the Ctrl+V keystroke that triggered it, so there is nothing left
57 // to re-trigger from here.58 // to re-trigger from here.
58-func (d *Daemon) handleImagePush(sess *session, ip ports.ImagePush) {59+func (d *Daemon) handleImagePush(sess *session, ip protocol.ImagePush) {
59 if len(ip.Data) == 0 {60 if len(ip.Data) == 0 {
60 return61 return
61 }62 }
@@ -73,7 +74,7 @@ func (d *Daemon) handleImagePush(sess *session, ip ports.ImagePush) {
73 74
74 // writeClipboardImage writes ip's bytes to a new 0600 temp file and records75 // writeClipboardImage writes ip's bytes to a new 0600 temp file and records
75 // the path on sess for best-effort cleanup at session end (killSession).76 // the path on sess for best-effort cleanup at session end (killSession).
76-func (d *Daemon) writeClipboardImage(sess *session, ip ports.ImagePush) (string, error) {77+func (d *Daemon) writeClipboardImage(sess *session, ip protocol.ImagePush) (string, error) {
77 path, err := d.createClipboardImage(ip)78 path, err := d.createClipboardImage(ip)
78 if err != nil {79 if err != nil {
79 return "", err80 return "", err
@@ -84,7 +85,7 @@ func (d *Daemon) writeClipboardImage(sess *session, ip ports.ImagePush) (string,
84 return path, nil85 return path, nil
85 }86 }
86 87
87-func (d *Daemon) writeClipboardImageForAttachment(effect *attachmentEffect, ip ports.ImagePush) (string, error) {88+func (d *Daemon) writeClipboardImageForAttachment(effect *attachmentEffect, ip protocol.ImagePush) (string, error) {
88 path, err := d.createClipboardImage(ip)89 path, err := d.createClipboardImage(ip)
89 if err != nil {90 if err != nil {
90 return "", err91 return "", err
@@ -105,7 +106,7 @@ func (d *Daemon) writeClipboardImageForAttachment(effect *attachmentEffect, ip p
105 return path, nil106 return path, nil
106 }107 }
107 108
108-func (d *Daemon) createClipboardImage(ip ports.ImagePush) (string, error) {109+func (d *Daemon) createClipboardImage(ip protocol.ImagePush) (string, error) {
109 dir := d.tempDir110 dir := d.tempDir
110 if dir == "" {111 if dir == "" {
111 dir = os.TempDir()112 dir = os.TempDir()
M internal/usecase/daemon/clipboard_test.go +7 -6
@@ -18,6 +18,7 @@ import (
18 "github.com/bnema/vev/internal/domain"18 "github.com/bnema/vev/internal/domain"
19 "github.com/bnema/vev/internal/ports"19 "github.com/bnema/vev/internal/ports"
20 portsmocks "github.com/bnema/vev/internal/ports/mocks"20 portsmocks "github.com/bnema/vev/internal/ports/mocks"
21+ "github.com/bnema/vev/internal/protocol"
21 scopy "github.com/bnema/vev/internal/usecase/copy"22 scopy "github.com/bnema/vev/internal/usecase/copy"
22 )23 )
23 24
@@ -47,7 +48,7 @@ func TestHandleImagePushWritesFileWithExactBytesAndMode0600(t *testing.T) {
47 d.tempDir = t.TempDir()48 d.tempDir = t.TempDir()
48 49
49 data := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0x01, 0x02, 0x03}50 data := []byte{0x89, 'P', 'N', 'G', 0x0d, 0x0a, 0x1a, 0x0a, 0x01, 0x02, 0x03}
50- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: data})51+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: data})
51 52
52 injected := <-writes53 injected := <-writes
53 path := string(injected)54 path := string(injected)
@@ -73,7 +74,7 @@ func TestHandleImagePushInjectsPathWithoutBracketedPasteWrapping(t *testing.T) {
73 pane := sess.tabs[0].focusedPane()74 pane := sess.tabs[0].focusedPane()
74 require.False(t, pane.screen.BracketedPasteMode())75 require.False(t, pane.screen.BracketedPasteMode())
75 76
76- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: []byte("x")})77+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: []byte("x")})
77 78
78 injected := <-writes79 injected := <-writes
79 require.False(t, hasBracketedPasteMarkers(injected), "path must not be wrapped when the pane is not in bracketed-paste mode")80 require.False(t, hasBracketedPasteMarkers(injected), "path must not be wrapped when the pane is not in bracketed-paste mode")
@@ -90,7 +91,7 @@ func TestHandleImagePushInjectsPathWithBracketedPasteWrapping(t *testing.T) {
90 pane.screen.Write([]byte("\x1b[?2004h"))91 pane.screen.Write([]byte("\x1b[?2004h"))
91 require.True(t, pane.screen.BracketedPasteMode())92 require.True(t, pane.screen.BracketedPasteMode())
92 93
93- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: []byte("x")})94+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: []byte("x")})
94 95
95 injected := <-writes96 injected := <-writes
96 require.True(t, len(injected) > len(clipPasteOpenMarker)+len(clipPasteCloseMarker))97 require.True(t, len(injected) > len(clipPasteOpenMarker)+len(clipPasteCloseMarker))
@@ -114,7 +115,7 @@ func TestHandleImagePushRejectsOversizedPayloadWithoutWritingOrInjecting(t *test
114 d.tempDir = dir115 d.tempDir = dir
115 116
116 huge := make([]byte, maxImagePushSize+1)117 huge := make([]byte, maxImagePushSize+1)
117- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: huge})118+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: huge})
118 119
119 select {120 select {
120 case w := <-writes:121 case w := <-writes:
@@ -134,7 +135,7 @@ func TestHandleImagePushIgnoresEmptyPayload(t *testing.T) {
134 dir := t.TempDir()135 dir := t.TempDir()
135 d.tempDir = dir136 d.tempDir = dir
136 137
137- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: nil})138+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: nil})
138 139
139 select {140 select {
140 case w := <-writes:141 case w := <-writes:
@@ -170,7 +171,7 @@ func TestKillSessionRemovesClipboardTempFiles(t *testing.T) {
170 // killSession requires the session to be registered in the daemon's171 // killSession requires the session to be registered in the daemon's
171 // registry (newManualSessionWithPTYs already does this via d.sessions).172 // registry (newManualSessionWithPTYs already does this via d.sessions).
172 173
173- d.handleImagePush(sess, ports.ImagePush{Mime: "image/png", Data: []byte("x")})174+ d.handleImagePush(sess, protocol.ImagePush{Mime: "image/png", Data: []byte("x")})
174 injected := <-writes175 injected := <-writes
175 path := string(injected)176 path := string(injected)
176 require.FileExists(t, path)177 require.FileExists(t, path)
M internal/usecase/daemon/coderabbit_regression_test.go +6 -6
@@ -350,9 +350,9 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
350 d.mu.Unlock()350 d.mu.Unlock()
351 close(catalogue.release)351 close(catalogue.release)
352 352
353- var protocol *protoErr353+ var protocolErr *protoErr
354- require.ErrorAs(t, <-result, &protocol)354+ require.ErrorAs(t, <-result, &protocolErr)
355- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)355+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
356 require.Empty(t, catalogue.MetadataUpdates())356 require.Empty(t, catalogue.MetadataUpdates())
357 d.mu.Lock()357 d.mu.Lock()
358 defer d.mu.Unlock()358 defer d.mu.Unlock()
@@ -443,9 +443,9 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
443 }443 }
444 close(catalogue.release)444 close(catalogue.release)
445 445
446- var protocol *protoErr446+ var protocolErr *protoErr
447- require.ErrorAs(t, <-result, &protocol)447+ require.ErrorAs(t, <-result, &protocolErr)
448- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)448+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
449 require.Empty(t, catalogue.MetadataUpdates())449 require.Empty(t, catalogue.MetadataUpdates())
450 d.mu.Lock()450 d.mu.Lock()
451 defer d.mu.Unlock()451 defer d.mu.Unlock()
M internal/usecase/daemon/config_test.go +2 -1
@@ -9,6 +9,7 @@ import (
9 renderer "github.com/bnema/vev-vt"9 renderer "github.com/bnema/vev-vt"
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 "github.com/bnema/vev/internal/usecase/keys"13 "github.com/bnema/vev/internal/usecase/keys"
13 themeui "github.com/bnema/vev/internal/usecase/theme"14 themeui "github.com/bnema/vev/internal/usecase/theme"
14 )15 )
@@ -63,7 +64,7 @@ func TestApplyThemeCopiesTerminalPaletteAndHotReloadGatesIt(t *testing.T) {
63 clientPalette := [16]renderer.RGB{}64 clientPalette := [16]renderer.RGB{}
64 clientPalette[1] = renderer.RGB{R: 1, G: 2, B: 3}65 clientPalette[1] = renderer.RGB{R: 1, G: 2, B: 3}
65 clientPalette[12] = renderer.RGB{R: 4, G: 5, B: 6}66 clientPalette[12] = renderer.RGB{R: 4, G: 5, B: 6}
66- d.applyTheme(sess, ac, ports.Theme{67+ d.applyTheme(sess, ac, protocol.Theme{
67 HasForeground: true,68 HasForeground: true,
68 Foreground: renderer.RGB{R: 7, G: 8, B: 9},69 Foreground: renderer.RGB{R: 7, G: 8, B: 9},
69 HasBackground: true,70 HasBackground: true,
M internal/usecase/daemon/daemon.go +4 -3
@@ -17,6 +17,7 @@ import (
17 17
18 "github.com/bnema/vev/internal/domain"18 "github.com/bnema/vev/internal/domain"
19 "github.com/bnema/vev/internal/ports"19 "github.com/bnema/vev/internal/ports"
20+ "github.com/bnema/vev/internal/protocol"
20 "github.com/bnema/vev/internal/usecase/keys"21 "github.com/bnema/vev/internal/usecase/keys"
21 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"22 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
22 )23 )
@@ -1286,7 +1287,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
1286 failAttachment()1287 failAttachment()
1287 return1288 return
1288 }1289 }
1289- if ac.startupOverlay == ports.StartupOverlaySessionPicker {1290+ if ac.startupOverlay == protocol.StartupOverlaySessionPicker {
1290 d.enterPicker(sess, ac)1291 d.enterPicker(sess, ac)
1291 }1292 }
1292 postWelcomeTicket.End()1293 postWelcomeTicket.End()
@@ -1449,7 +1450,7 @@ func (d *Daemon) waitForTargetRestore(ctx context.Context, name string) error {
1449 // route resolves a Hello to a session and a freshly attached client, creating1450 // route resolves a Hello to a session and a freshly attached client, creating
1450 // the session for ephemeral/new intents. Direct package callers retain the1451 // the session for ephemeral/new intents. Direct package callers retain the
1451 // daemon lifetime context; inbound handshakes use routeWithContext.1452 // daemon lifetime context; inbound handshakes use routeWithContext.
1452-func (d *Daemon) validateExactSessionTargetLocked(target ports.ExactSessionTarget) error {1453+func (d *Daemon) validateExactSessionTargetLocked(target protocol.ExactSessionTarget) error {
1453 if sess := d.findByNameLocked(target.SessionName); sess != nil {1454 if sess := d.findByNameLocked(target.SessionName); sess != nil {
1454 if sess.incarnation != target.LifecycleID {1455 if sess.incarnation != target.LifecycleID {
1455 return &protoErr{ports.ErrNoSuchSession, "session lifecycle has changed: " + target.SessionName}1456 return &protoErr{ports.ErrNoSuchSession, "session lifecycle has changed: " + target.SessionName}
@@ -1463,7 +1464,7 @@ func (d *Daemon) validateExactSessionTargetLocked(target ports.ExactSessionTarge
1463 return nil1464 return nil
1464 }1465 }
1465 1466
1466-func (d *Daemon) validateExactSessionTarget(ctx context.Context, target ports.ExactSessionTarget) error {1467+func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol.ExactSessionTarget) error {
1467 if err := target.Validate(); err != nil {1468 if err := target.Validate(); err != nil {
1468 return &protoErr{ports.ErrNoSuchSession, "invalid exact session target"}1469 return &protoErr{ports.ErrNoSuchSession, "invalid exact session target"}
1469 }1470 }
M internal/usecase/daemon/exact_route_test.go +14 -13
@@ -6,6 +6,7 @@ import (
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
9 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
10 )11 )
11 12
@@ -15,7 +16,7 @@ func TestRouteExactSessionTargetSelectsLifecycle(t *testing.T) {
15 sess.ephemeral = false16 sess.ephemeral = false
16 sess.incarnation = remoteLifecycleForTest()17 sess.incarnation = remoteLifecycleForTest()
17 tr, _ := newCapturingTransport(t)18 tr, _ := newCapturingTransport(t)
18- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}19+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
19 hello := ports.Hello{20 hello := ports.Hello{
20 Version: ports.ProtocolVersion,21 Version: ports.ProtocolVersion,
21 Intent: ports.IntentAttach,22 Intent: ports.IntentAttach,
@@ -42,13 +43,13 @@ func TestRouteExactSessionTargetRejectsNameMismatch(t *testing.T) {
42 Intent: ports.IntentAttach,43 Intent: ports.IntentAttach,
43 Name: "other",44 Name: "other",
44 Size: domain.Size{Cols: 80, Rows: 24},45 Size: domain.Size{Cols: 80, Rows: 24},
45- ExactTarget: &ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},46+ ExactTarget: &protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},
46 }47 }
47 48
48 _, _, err := d.routeWithContext(context.Background(), hello, tr)49 _, _, err := d.routeWithContext(context.Background(), hello, tr)
49- var protocol *protoErr50+ var protocolErr *protoErr
50- require.ErrorAs(t, err, &protocol)51+ require.ErrorAs(t, err, &protocolErr)
51- require.Equal(t, ports.ErrNoSuchSession, protocol.code)52+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
52 }53 }
53 54
54 func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {55 func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
@@ -56,7 +57,7 @@ func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
56 sess := addControlSession(d, "work", "tab-1", "pane-1")57 sess := addControlSession(d, "work", "tab-1", "pane-1")
57 sess.ephemeral = false58 sess.ephemeral = false
58 sess.incarnation = remoteLifecycleForTest()59 sess.incarnation = remoteLifecycleForTest()
59- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}60+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
60 replacement := sess.incarnation61 replacement := sess.incarnation
61 replacement[0]++62 replacement[0]++
62 sess.incarnation = replacement63 sess.incarnation = replacement
@@ -65,9 +66,9 @@ func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
65 err := d.validateExactSessionTargetLocked(target)66 err := d.validateExactSessionTargetLocked(target)
66 d.mu.Unlock()67 d.mu.Unlock()
67 68
68- var protocol *protoErr69+ var protocolErr *protoErr
69- require.ErrorAs(t, err, &protocol)70+ require.ErrorAs(t, err, &protocolErr)
70- require.Equal(t, ports.ErrNoSuchSession, protocol.code)71+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
71 }72 }
72 73
73 func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {74 func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
@@ -83,11 +84,11 @@ func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
83 Intent: ports.IntentAttach,84 Intent: ports.IntentAttach,
84 Name: sess.name,85 Name: sess.name,
85 Size: domain.Size{Cols: 80, Rows: 24},86 Size: domain.Size{Cols: 80, Rows: 24},
86- ExactTarget: &ports.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},87+ ExactTarget: &protocol.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},
87 }88 }
88 89
89 _, _, err := d.routeWithContext(context.Background(), hello, tr)90 _, _, err := d.routeWithContext(context.Background(), hello, tr)
90- var protocol *protoErr91+ var protocolErr *protoErr
91- require.ErrorAs(t, err, &protocol)92+ require.ErrorAs(t, err, &protocolErr)
92- require.Equal(t, ports.ErrNoSuchSession, protocol.code)93+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
93 }94 }
M internal/usecase/daemon/graphics_output.go +3 -2
@@ -16,6 +16,7 @@ import (
16 "github.com/bnema/vev-vt/graphics"16 "github.com/bnema/vev-vt/graphics"
17 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
18 "github.com/bnema/vev/internal/ports"18 "github.com/bnema/vev/internal/ports"
19+ "github.com/bnema/vev/internal/protocol"
19 "github.com/bnema/vev/internal/usecase/layout"20 "github.com/bnema/vev/internal/usecase/layout"
20 themeui "github.com/bnema/vev/internal/usecase/theme"21 themeui "github.com/bnema/vev/internal/usecase/theme"
21 )22 )
@@ -25,8 +26,8 @@ import (
25 // attachment-local state is also the ownership boundary for Kitty IDs: raw26 // attachment-local state is also the ownership boundary for Kitty IDs: raw
26 // scene IDs are never sent to the outer terminal.27 // scene IDs are never sent to the outer terminal.
27 const (28 const (
28- maxGraphicsRecordBytes = ports.MaxOutputDataLen29+ maxGraphicsRecordBytes = protocol.MaxOutputDataLen
29- maxGraphicsOutputBytes = ports.MaxOutputDataLen30+ maxGraphicsOutputBytes = protocol.MaxOutputDataLen
30 graphicsUploadChunkBytes = 128 << 1031 graphicsUploadChunkBytes = 128 << 10
31 )32 )
32 33
M internal/usecase/daemon/input_test.go +7 -6
@@ -17,6 +17,7 @@ import (
17 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
18 "github.com/bnema/vev/internal/ports"18 "github.com/bnema/vev/internal/ports"
19 portsmocks "github.com/bnema/vev/internal/ports/mocks"19 portsmocks "github.com/bnema/vev/internal/ports/mocks"
20+ "github.com/bnema/vev/internal/protocol"
20 scopy "github.com/bnema/vev/internal/usecase/copy"21 scopy "github.com/bnema/vev/internal/usecase/copy"
21 "github.com/bnema/vev/internal/usecase/keys"22 "github.com/bnema/vev/internal/usecase/keys"
22 "github.com/bnema/vev/internal/usecase/layout"23 "github.com/bnema/vev/internal/usecase/layout"
@@ -789,18 +790,18 @@ func TestPaletteBackSessionSendsClientPreviousRouteAction(t *testing.T) {
789 effect, admitted := ac.beginAttachmentEffect(token)790 effect, admitted := ac.beginAttachmentEffect(token)
790 require.True(t, admitted)791 require.True(t, admitted)
791 defer effect.End()792 defer effect.End()
792- ac.setRouteSnapshot(ports.RecentRouteSnapshot{793+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
793 Generation: 3,794 Generation: 3,
794- Active: ports.RouteRef{Key: 3, Generation: 3},795+ Active: protocol.RouteRef{Key: 3, Generation: 3},
795- Previous: ports.RouteRef{Key: 2, Generation: 2},796+ Previous: protocol.RouteRef{Key: 2, Generation: 2},
796- Entries: []ports.RecentRouteEntry{testRouteEntry(2, 2, "recent", 2, ports.RouteKindLocal)},797+ Entries: []protocol.RecentRouteEntry{testRouteEntry(2, 2, "recent", 2, protocol.RouteKindLocal)},
797 })798 })
798 799
799 require.NoError(t, d.backSessionForAttachment(effect))800 require.NoError(t, d.backSessionForAttachment(effect))
800 frame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)801 frame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
801 action, err := ports.UnmarshalRouteNavigationAction(frame.Payload)802 action, err := ports.UnmarshalRouteNavigationAction(frame.Payload)
802 require.NoError(t, err)803 require.NoError(t, err)
803- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 3, Key: 2, Generation: 2}, action)804+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 3, Key: 2, Generation: 2}, action)
804 }805 }
805 806
806 func TestBackSessionWithoutClientHistoryIsNoop(t *testing.T) {807 func TestBackSessionWithoutClientHistoryIsNoop(t *testing.T) {
@@ -1088,7 +1089,7 @@ func TestCopyModeMouseHorizontalReverseDragUsesExactOSC52(t *testing.T) {
1088 mustOutputData(t, sends)1089 mustOutputData(t, sends)
1089 d.handleInput(sess, ac, []byte("\x1b[<0;2;2m"))1090 d.handleInput(sess, ac, []byte("\x1b[<0;2;2m"))
1090 d.handleInput(sess, ac, []byte("y"))1091 d.handleInput(sess, ac, []byte("y"))
1091- var msg ports.Output1092+ var msg protocol.Output
1092 require.Eventually(t, func() bool {1093 require.Eventually(t, func() bool {
1093 frame := awaitFrame(t, sends, ports.MsgOutput)1094 frame := awaitFrame(t, sends, ports.MsgOutput)
1094 var err error1095 var err error
M internal/usecase/daemon/move_lifecycle_test.go +1 -2
@@ -4,9 +4,8 @@ import (
4 "testing"4 "testing"
5 "time"5 "time"
6 6
7- "github.com/stretchr/testify/require"
8-
9 "github.com/bnema/vev/internal/ports"7 "github.com/bnema/vev/internal/ports"
8+ "github.com/stretchr/testify/require"
10 )9 )
11 10
12 func awaitMoveTeardownState(t *testing.T, sess *session, reservations, waiters uint) {11 func awaitMoveTeardownState(t *testing.T, sess *session, reservations, waiters uint) {
M internal/usecase/daemon/output_state_sync_test.go +3 -2
@@ -12,6 +12,7 @@ import (
12 renderer "github.com/bnema/vev-vt/ansi"12 renderer "github.com/bnema/vev-vt/ansi"
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
15 "github.com/stretchr/testify/require"16 "github.com/stretchr/testify/require"
16 )17 )
17 18
@@ -102,7 +103,7 @@ func TestDatagramAttachPipelinesRendererBeforeAck(t *testing.T) {
102 require.Equal(t, uint64(2), out.New)103 require.Equal(t, uint64(2), out.New)
103 // Both states were emitted before the MsgAck; the ordered dependency chain104 // Both states were emitted before the MsgAck; the ordered dependency chain
104 // must therefore remain valid without waiting for renderer acknowledgement.105 // must therefore remain valid without waiting for renderer acknowledgement.
105- tr.recv <- ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(ports.Ack{Epoch: out.Epoch, State: out.New})}106+ tr.recv <- ports.Frame{Type: ports.MsgAck, Payload: mustMarshalAck(protocol.Ack{Epoch: out.Epoch, State: out.New})}
106 require.NoError(t, tr.Close())107 require.NoError(t, tr.Close())
107 d.runConnLoop(ac)108 d.runConnLoop(ac)
108 109
@@ -197,7 +198,7 @@ func outputStateRow(row []renderer.Cell) string {
197 return string(runes)198 return string(runes)
198 }199 }
199 200
200-func mustApplyOutput(t *testing.T, screen *vt.Screen, frame ports.Frame) ports.Output {201+func mustApplyOutput(t *testing.T, screen *vt.Screen, frame ports.Frame) protocol.Output {
201 t.Helper()202 t.Helper()
202 out, err := ports.UnmarshalOutput(frame.Payload)203 out, err := ports.UnmarshalOutput(frame.Payload)
203 require.NoError(t, err)204 require.NoError(t, err)
M internal/usecase/daemon/overlay_runtime.go +4 -4
@@ -6,7 +6,7 @@ import (
6 "time"6 "time"
7 7
8 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"9+ "github.com/bnema/vev/internal/protocol"
10 scopy "github.com/bnema/vev/internal/usecase/copy"10 scopy "github.com/bnema/vev/internal/usecase/copy"
11 "github.com/bnema/vev/internal/usecase/notices"11 "github.com/bnema/vev/internal/usecase/notices"
12 "github.com/bnema/vev/internal/usecase/palette"12 "github.com/bnema/vev/internal/usecase/palette"
@@ -43,7 +43,7 @@ type overlayRuntime struct {
43 43
44 paletteMu sync.Mutex44 paletteMu sync.Mutex
45 palette *palette.Model45 palette *palette.Model
46- paletteRouteSnapshot ports.RecentRouteSnapshot46+ paletteRouteSnapshot protocol.RecentRouteSnapshot
47 paletteGeneration uint6447 paletteGeneration uint64
48 paletteHints palette.ContextualHints48 paletteHints palette.ContextualHints
49 palettePreview string49 palettePreview string
@@ -309,7 +309,7 @@ type overlayRenderSnapshot struct {
309 paletteHints *palette.ContextualHints309 paletteHints *palette.ContextualHints
310 palettePreview string310 palettePreview string
311 paletteFeedback string311 paletteFeedback string
312- paletteRouteSnapshot ports.RecentRouteSnapshot312+ paletteRouteSnapshot protocol.RecentRouteSnapshot
313 paletteLocked bool313 paletteLocked bool
314 314
315 promptActive bool315 promptActive bool
@@ -383,7 +383,7 @@ func (rt *overlayRuntime) SnapshotForRender() *overlayRenderSnapshot {
383 snap.palettePreview = rt.palettePreview383 snap.palettePreview = rt.palettePreview
384 snap.paletteFeedback = rt.paletteFeedback384 snap.paletteFeedback = rt.paletteFeedback
385 snap.paletteRouteSnapshot = rt.paletteRouteSnapshot385 snap.paletteRouteSnapshot = rt.paletteRouteSnapshot
386- snap.paletteRouteSnapshot.Entries = append([]ports.RecentRouteEntry(nil), rt.paletteRouteSnapshot.Entries...)386+ snap.paletteRouteSnapshot.Entries = append([]protocol.RecentRouteEntry(nil), rt.paletteRouteSnapshot.Entries...)
387 snap.paletteLocked = true387 snap.paletteLocked = true
388 } else {388 } else {
389 rt.paletteMu.Unlock()389 rt.paletteMu.Unlock()
M internal/usecase/daemon/palette.go +25 -24
@@ -8,6 +8,7 @@ import (
8 renderer "github.com/bnema/vev-vt"8 renderer "github.com/bnema/vev-vt"
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 "github.com/bnema/vev/internal/usecase/command"12 "github.com/bnema/vev/internal/usecase/command"
12 "github.com/bnema/vev/internal/usecase/layout"13 "github.com/bnema/vev/internal/usecase/layout"
13 "github.com/bnema/vev/internal/usecase/palette"14 "github.com/bnema/vev/internal/usecase/palette"
@@ -61,7 +62,7 @@ func (d *Daemon) enterPalette(sess *session, ac *attachedClient) {
61 }62 }
62 63
63 type paletteSessionIdentity struct {64 type paletteSessionIdentity struct {
64- kind ports.RouteKind65+ kind protocol.RouteKind
65 endpoint string66 endpoint string
66 lifecycle domain.SessionLifecycleID67 lifecycle domain.SessionLifecycleID
67 }68 }
@@ -73,11 +74,11 @@ type remotePalettePresentationIdentity struct {
73 }74 }
74 75
75 func localPaletteSessionIdentity(lifecycle domain.SessionLifecycleID) paletteSessionIdentity {76 func localPaletteSessionIdentity(lifecycle domain.SessionLifecycleID) paletteSessionIdentity {
76- return paletteSessionIdentity{kind: ports.RouteKindLocal, endpoint: "local", lifecycle: lifecycle}77+ return paletteSessionIdentity{kind: protocol.RouteKindLocal, endpoint: "local", lifecycle: lifecycle}
77 }78 }
78 79
79 func remotePaletteSessionIdentity(endpoint string, lifecycle domain.SessionLifecycleID) paletteSessionIdentity {80 func remotePaletteSessionIdentity(endpoint string, lifecycle domain.SessionLifecycleID) paletteSessionIdentity {
80- return paletteSessionIdentity{kind: ports.RouteKindRemote, endpoint: endpoint, lifecycle: lifecycle}81+ return paletteSessionIdentity{kind: protocol.RouteKindRemote, endpoint: endpoint, lifecycle: lifecycle}
81 }82 }
82 83
83 func paletteRemoteDisplayOrigin(hostLabel string) string {84 func paletteRemoteDisplayOrigin(hostLabel string) string {
@@ -88,19 +89,19 @@ func paletteRemoteDisplayOrigin(hostLabel string) string {
88 return origin89 return origin
89 }90 }
90 91
91-func paletteDaemonDisplayOrigin(snapshot ports.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) string {92+func paletteDaemonDisplayOrigin(snapshot protocol.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) string {
92 active, ok := activeRouteEntryForLifecycle(snapshot, lifecycle)93 active, ok := activeRouteEntryForLifecycle(snapshot, lifecycle)
93- if !ok || active.Kind != ports.RouteKindRemote {94+ if !ok || active.Kind != protocol.RouteKindRemote {
94 return ""95 return ""
95 }96 }
96 return paletteRemoteDisplayOrigin(active.HostLabel)97 return paletteRemoteDisplayOrigin(active.HostLabel)
97 }98 }
98 99
99-func paletteRouteRepresentsDaemonSession(entry ports.RecentRouteEntry, daemonDisplayOrigin string) bool {100+func paletteRouteRepresentsDaemonSession(entry protocol.RecentRouteEntry, daemonDisplayOrigin string) bool {
100- if entry.Kind == ports.RouteKindLocal {101+ if entry.Kind == protocol.RouteKindLocal {
101 return true102 return true
102 }103 }
103- return entry.Kind == ports.RouteKindRemote && daemonDisplayOrigin != "" &&104+ return entry.Kind == protocol.RouteKindRemote && daemonDisplayOrigin != "" &&
104 paletteRemoteDisplayOrigin(entry.HostLabel) == daemonDisplayOrigin105 paletteRemoteDisplayOrigin(entry.HostLabel) == daemonDisplayOrigin
105 }106 }
106 107
@@ -113,7 +114,7 @@ func remotePaletteUnavailableReason(status remoteHostStatus, session ports.Remot
113 114
114 // paletteResults captures eligible named sessions before paletteMu so the115 // paletteResults captures eligible named sessions before paletteMu so the
115 // palette has an immutable lifecycle target without violating lock ordering.116 // palette has an immutable lifecycle target without violating lock ordering.
116-func (d *Daemon) paletteResults(current *session, commands []command.Command, routeSnapshot ports.RecentRouteSnapshot) []palette.Result {117+func (d *Daemon) paletteResults(current *session, commands []command.Command, routeSnapshot protocol.RecentRouteSnapshot) []palette.Result {
117 d.mu.Lock()118 d.mu.Lock()
118 sessions := d.sessionsSnapshotLocked()119 sessions := d.sessionsSnapshotLocked()
119 stopped := make([]inactiveSession, 0, len(d.inactive))120 stopped := make([]inactiveSession, 0, len(d.inactive))
@@ -154,7 +155,7 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro
154 if candidate == current {155 if candidate == current {
155 continue156 continue
156 }157 }
157- target := ports.ExactSessionTarget{LifecycleID: snap.incarnation, SessionName: snap.name}158+ target := protocol.ExactSessionTarget{LifecycleID: snap.incarnation, SessionName: snap.name}
158 active = append(active, palette.NewActiveSessionResultWithDisplayOrigin(target, time.Unix(0, snap.createdAt), daemonDisplayOrigin))159 active = append(active, palette.NewActiveSessionResultWithDisplayOrigin(target, time.Unix(0, snap.createdAt), daemonDisplayOrigin))
159 }160 }
160 sort.Slice(active, func(i, j int) bool {161 sort.Slice(active, func(i, j int) bool {
@@ -165,7 +166,7 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro
165 sort.Slice(stopped, func(i, j int) bool { return stopped[i].name < stopped[j].name })166 sort.Slice(stopped, func(i, j int) bool { return stopped[i].name < stopped[j].name })
166 results = append(results, active...)167 results = append(results, active...)
167 for _, candidate := range stopped {168 for _, candidate := range stopped {
168- target := ports.ExactSessionTarget{LifecycleID: candidate.incarnation, SessionName: candidate.name}169+ target := protocol.ExactSessionTarget{LifecycleID: candidate.incarnation, SessionName: candidate.name}
169 results = append(results, palette.NewStoppedSessionResultWithDisplayOrigin(target, time.Unix(0, candidate.createdAt), daemonDisplayOrigin))170 results = append(results, palette.NewStoppedSessionResultWithDisplayOrigin(target, time.Unix(0, candidate.createdAt), daemonDisplayOrigin))
170 represented[localPaletteSessionIdentity(target.LifecycleID)] = struct{}{}171 represented[localPaletteSessionIdentity(target.LifecycleID)] = struct{}{}
171 }172 }
@@ -210,12 +211,12 @@ func (d *Daemon) paletteResults(current *session, commands []command.Command, ro
210 if _, exists := represented[identity]; exists && paletteRouteRepresentsDaemonSession(entry, daemonDisplayOrigin) {211 if _, exists := represented[identity]; exists && paletteRouteRepresentsDaemonSession(entry, daemonDisplayOrigin) {
211 continue212 continue
212 }213 }
213- results = append(results, palette.NewRecentRouteResult(label, ports.RouteNavigationAction{214+ results = append(results, palette.NewRecentRouteResult(label, protocol.RouteNavigationAction{
214 SnapshotGeneration: routeSnapshot.Generation,215 SnapshotGeneration: routeSnapshot.Generation,
215 Key: entry.Key,216 Key: entry.Key,
216 Generation: entry.Generation,217 Generation: entry.Generation,
217 }))218 }))
218- if entry.Kind == ports.RouteKindLocal {219+ if entry.Kind == protocol.RouteKindLocal {
219 represented[identity] = struct{}{}220 represented[identity] = struct{}{}
220 continue221 continue
221 }222 }
@@ -251,7 +252,7 @@ func (d *Daemon) refreshPalette(ac *attachedClient) {
251 model := rt.palette252 model := rt.palette
252 generation := rt.paletteGeneration253 generation := rt.paletteGeneration
253 routeSnapshot := rt.paletteRouteSnapshot254 routeSnapshot := rt.paletteRouteSnapshot
254- routeSnapshot.Entries = append([]ports.RecentRouteEntry(nil), routeSnapshot.Entries...)255+ routeSnapshot.Entries = append([]protocol.RecentRouteEntry(nil), routeSnapshot.Entries...)
255 rt.paletteMu.Unlock()256 rt.paletteMu.Unlock()
256 if model == nil {257 if model == nil {
257 return258 return
@@ -266,7 +267,7 @@ func (d *Daemon) refreshPalette(ac *attachedClient) {
266 model.ReplaceResults(results)267 model.ReplaceResults(results)
267 }268 }
268 269
269-func recentRouteHints(snapshot ports.RecentRouteSnapshot, args []string) palette.ContextualHints {270+func recentRouteHints(snapshot protocol.RecentRouteSnapshot, args []string) palette.ContextualHints {
270 formatted := formatRecentRouteSnapshot(snapshot)271 formatted := formatRecentRouteSnapshot(snapshot)
271 names := make([]string, len(formatted))272 names := make([]string, len(formatted))
272 for i, entry := range formatted {273 for i, entry := range formatted {
@@ -342,10 +343,10 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ...
342 var remoteKey domain.RemoteSessionKey343 var remoteKey domain.RemoteSessionKey
343 var remoteUnavailableReason string344 var remoteUnavailableReason string
344 var hasRemoteTarget bool345 var hasRemoteTarget bool
345- var routeTarget ports.RouteNavigationAction346+ var routeTarget protocol.RouteNavigationAction
346 var hasRouteTarget bool347 var hasRouteTarget bool
347 var args []string348 var args []string
348- var routeSnapshot ports.RecentRouteSnapshot349+ var routeSnapshot protocol.RecentRouteSnapshot
349 var generation uint64350 var generation uint64
350 var rawQuery string351 var rawQuery string
351 changed, cancel, execute := false, false, false352 changed, cancel, execute := false, false, false
@@ -419,7 +420,7 @@ func (d *Daemon) handlePaletteInput(ac *attachedClient, data []byte, effects ...
419 }420 }
420 if cmd.ContextHint == command.ContextHintRecentSessions {421 if cmd.ContextHint == command.ContextHintRecentSessions {
421 routeSnapshot = ac.overlays.paletteRouteSnapshot422 routeSnapshot = ac.overlays.paletteRouteSnapshot
422- routeSnapshot.Entries = append([]ports.RecentRouteEntry(nil), routeSnapshot.Entries...)423+ routeSnapshot.Entries = append([]protocol.RecentRouteEntry(nil), routeSnapshot.Entries...)
423 }424 }
424 } else if target, ok := selected.RemoteSessionTarget(); ok {425 } else if target, ok := selected.RemoteSessionTarget(); ok {
425 key, keyOK := selected.RemoteSessionKey()426 key, keyOK := selected.RemoteSessionKey()
@@ -624,7 +625,7 @@ func (ac *attachedClient) clearPaletteLocked() remoteDiscoveryInstance {
624 model := ac.overlays.palette625 model := ac.overlays.palette
625 ac.overlays.paletteGeneration++626 ac.overlays.paletteGeneration++
626 ac.overlays.palette = nil627 ac.overlays.palette = nil
627- ac.overlays.paletteRouteSnapshot = ports.RecentRouteSnapshot{}628+ ac.overlays.paletteRouteSnapshot = protocol.RecentRouteSnapshot{}
628 ac.overlays.paletteHints = palette.ContextualHints{}629 ac.overlays.paletteHints = palette.ContextualHints{}
629 ac.overlays.palettePreview = ""630 ac.overlays.palettePreview = ""
630 ac.overlays.paletteFeedback = ""631 ac.overlays.paletteFeedback = ""
@@ -679,7 +680,7 @@ type paletteExec struct {
679 sess *session680 sess *session
680 attachment *session681 attachment *session
681 ac *attachedClient682 ac *attachedClient
682- routeSnapshot ports.RecentRouteSnapshot683+ routeSnapshot protocol.RecentRouteSnapshot
683 actions daemonActionRunner684 actions daemonActionRunner
684 effect *attachmentEffect685 effect *attachmentEffect
685 redrawClosedPalette bool686 redrawClosedPalette bool
@@ -911,8 +912,8 @@ func (e paletteExec) RenameTabTo(name string) error {
911 }912 }
912 913
913 func (e paletteExec) OpenSessionPicker() error {914 func (e paletteExec) OpenSessionPicker() error {
914- if e.ac != nil && e.ac.navigationCapabilities&ports.NavigationCapabilityHomePicker != 0 && e.effect != nil {915+ if e.ac != nil && e.ac.navigationCapabilities&protocol.NavigationCapabilityHomePicker != 0 && e.effect != nil {
915- return e.d.sendNavigationActionForAttachment(e.effect, ports.NavigationOpenHomePicker)916+ return e.d.sendNavigationActionForAttachment(e.effect, protocol.NavigationOpenHomePicker)
916 }917 }
917 e.d.enterPicker(e.sess, e.ac)918 e.d.enterPicker(e.sess, e.ac)
918 return nil919 return nil
@@ -945,14 +946,14 @@ func (e paletteExec) JumpRecentSession(rank int) error {
945 return command.ErrInvalidArguments946 return command.ErrInvalidArguments
946 }947 }
947 entry := e.routeSnapshot.Entries[rank-1]948 entry := e.routeSnapshot.Entries[rank-1]
948- return e.NavigateRecentRoute(ports.RouteNavigationAction{949+ return e.NavigateRecentRoute(protocol.RouteNavigationAction{
949 SnapshotGeneration: e.routeSnapshot.Generation,950 SnapshotGeneration: e.routeSnapshot.Generation,
950 Key: entry.Key,951 Key: entry.Key,
951 Generation: entry.Generation,952 Generation: entry.Generation,
952 })953 })
953 }954 }
954 955
955-func (e paletteExec) NavigateRecentRoute(action ports.RouteNavigationAction) error {956+func (e paletteExec) NavigateRecentRoute(action protocol.RouteNavigationAction) error {
956 if e.d.beforeRecentSessionHandoff != nil {957 if e.d.beforeRecentSessionHandoff != nil {
957 e.d.beforeRecentSessionHandoff()958 e.d.beforeRecentSessionHandoff()
958 }959 }
M internal/usecase/daemon/palette_test.go +54 -53
@@ -13,6 +13,7 @@ import (
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15 portsmocks "github.com/bnema/vev/internal/ports/mocks"15 portsmocks "github.com/bnema/vev/internal/ports/mocks"
16+ "github.com/bnema/vev/internal/protocol"
16 "github.com/bnema/vev/internal/usecase/command"17 "github.com/bnema/vev/internal/usecase/command"
17 "github.com/bnema/vev/internal/usecase/palette"18 "github.com/bnema/vev/internal/usecase/palette"
18 "github.com/bnema/vev/internal/usecase/picker"19 "github.com/bnema/vev/internal/usecase/picker"
@@ -20,14 +21,14 @@ import (
20 "github.com/bnema/vev/internal/usecase/ui"21 "github.com/bnema/vev/internal/usecase/ui"
21 )22 )
22 23
23-func testRecentRouteSnapshot() ports.RecentRouteSnapshot {24+func testRecentRouteSnapshot() protocol.RecentRouteSnapshot {
24- return ports.RecentRouteSnapshot{25+ return protocol.RecentRouteSnapshot{
25 Generation: 1,26 Generation: 1,
26- Active: ports.RouteRef{Key: 1, Generation: 1},27+ Active: protocol.RouteRef{Key: 1, Generation: 1},
27- ActiveEntry: testRouteEntry(1, 1, "current", 1, ports.RouteKindLocal),28+ ActiveEntry: testRouteEntry(1, 1, "current", 1, protocol.RouteKindLocal),
28- Entries: []ports.RecentRouteEntry{29+ Entries: []protocol.RecentRouteEntry{
29- testRouteEntry(2, 1, "recent", 2, ports.RouteKindLocal),30+ testRouteEntry(2, 1, "recent", 2, protocol.RouteKindLocal),
30- testRouteEntry(3, 1, "older", 3, ports.RouteKindLocal),31+ testRouteEntry(3, 1, "older", 3, protocol.RouteKindLocal),
31 },32 },
32 }33 }
33 }34 }
@@ -232,10 +233,10 @@ func TestPaletteEntryPublishesEligibleNamedSessionResults(t *testing.T) {
232 d.inactive["purging"] = inactiveSession{name: "purging", createdAt: 13, incarnation: domain.IncarnationID{4}, state: ports.SessionDown, purging: true}233 d.inactive["purging"] = inactiveSession{name: "purging", createdAt: 13, incarnation: domain.IncarnationID{4}, state: ports.SessionDown, purging: true}
233 d.inactive["broken"] = inactiveSession{name: "broken", createdAt: 14, incarnation: domain.IncarnationID{5}, state: ports.SessionBroken}234 d.inactive["broken"] = inactiveSession{name: "broken", createdAt: 14, incarnation: domain.IncarnationID{5}, state: ports.SessionBroken}
234 d.inactive["degraded"] = inactiveSession{name: "degraded", createdAt: 15, incarnation: domain.IncarnationID{6}, state: ports.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}235 d.inactive["degraded"] = inactiveSession{name: "degraded", createdAt: 15, incarnation: domain.IncarnationID{6}, state: ports.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}
235- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1, Entries: []ports.RecentRouteEntry{236+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1, Entries: []protocol.RecentRouteEntry{
236- testRouteEntry(1, 1, "purging", 4, ports.RouteKindLocal),237+ testRouteEntry(1, 1, "purging", 4, protocol.RouteKindLocal),
237- testRouteEntry(2, 1, "broken", 5, ports.RouteKindLocal),238+ testRouteEntry(2, 1, "broken", 5, protocol.RouteKindLocal),
238- testRouteEntry(3, 1, "degraded", 6, ports.RouteKindLocal),239+ testRouteEntry(3, 1, "degraded", 6, protocol.RouteKindLocal),
239 }})240 }})
240 241
241 d.enterPalette(current, ac)242 d.enterPalette(current, ac)
@@ -374,7 +375,7 @@ func TestPaletteJRSActivatesOnlyExactContextualHint(t *testing.T) {
374 p, release := newBlockingPTY(t)375 p, release := newBlockingPTY(t)
375 defer release()376 defer release()
376 d, sess, ac, sends := newManualSessionWithPTYs(t, p)377 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
377- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1, Active: ports.RouteRef{Key: 1, Generation: 1}})378+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1, Active: protocol.RouteRef{Key: 1, Generation: 1}})
378 379
379 d.handleInput(sess, ac, []byte("\x1b "))380 d.handleInput(sess, ac, []byte("\x1b "))
380 awaitFrame(t, sends, ports.MsgOutput)381 awaitFrame(t, sends, ports.MsgOutput)
@@ -412,7 +413,7 @@ func TestPaletteJRSUsesEffectiveOverrideOnly(t *testing.T) {
412 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)413 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
413 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)414 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
414 require.NoError(t, err)415 require.NoError(t, err)
415- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)416+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)
416 require.Same(t, current, ac.currentSession())417 require.Same(t, current, ac.currentSession())
417 require.False(t, ac.overlays.paletteActive())418 require.False(t, ac.overlays.paletteActive())
418 419
@@ -447,7 +448,7 @@ func TestPaletteIncludesExactRemoteCatalogTargetBesideSameNameLocalSession(t *te
447 d.remoteCatalog.status["user@arch"] = remoteHostFresh448 d.remoteCatalog.status["user@arch"] = remoteHostFresh
448 d.remoteCatalog.mu.Unlock()449 d.remoteCatalog.mu.Unlock()
449 450
450- results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{})451+ results := d.paletteResults(current, nil, protocol.RecentRouteSnapshot{})
451 var matching []palette.Result452 var matching []palette.Result
452 for _, result := range results {453 for _, result := range results {
453 if result.DisplayText() == "Switch to session vev" || result.DisplayText() == "Switch to session vev@arch" {454 if result.DisplayText() == "Switch to session vev" || result.DisplayText() == "Switch to session vev@arch" {
@@ -472,18 +473,18 @@ func TestPaletteQualifiesDaemonSessionForRemoteAttachment(t *testing.T) {
472 target.ephemeral = false473 target.ephemeral = false
473 target.incarnation = domain.SessionLifecycleID{42}474 target.incarnation = domain.SessionLifecycleID{42}
474 475
475- results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{476+ results := d.paletteResults(current, nil, protocol.RecentRouteSnapshot{
476 Generation: 1,477 Generation: 1,
477- Active: ports.RouteRef{Key: 1, Generation: 1},478+ Active: protocol.RouteRef{Key: 1, Generation: 1},
478- ActiveEntry: ports.RecentRouteEntry{479+ ActiveEntry: protocol.RecentRouteEntry{
479 Key: 1, Generation: 1,480 Key: 1, Generation: 1,
480- Target: ports.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "current"},481+ Target: protocol.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "current"},
481- Name: "current", HostLabel: "user@remote-host", Kind: ports.RouteKindRemote,482+ Name: "current", HostLabel: "user@remote-host", Kind: protocol.RouteKindRemote,
482 },483 },
483- Entries: []ports.RecentRouteEntry{{484+ Entries: []protocol.RecentRouteEntry{{
484 Key: 2, Generation: 1,485 Key: 2, Generation: 1,
485- Target: ports.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"},486+ Target: protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"},
486- Name: "target", HostLabel: "user@remote-host", Kind: ports.RouteKindRemote,487+ Name: "target", HostLabel: "user@remote-host", Kind: protocol.RouteKindRemote,
487 }},488 }},
488 })489 })
489 490
@@ -496,7 +497,7 @@ func TestPaletteQualifiesDaemonSessionForRemoteAttachment(t *testing.T) {
496 require.Len(t, matching, 1)497 require.Len(t, matching, 1)
497 got, ok := matching[0].SessionTarget()498 got, ok := matching[0].SessionTarget()
498 require.True(t, ok)499 require.True(t, ok)
499- require.Equal(t, ports.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"}, got)500+ require.Equal(t, protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: "target"}, got)
500 }501 }
501 502
502 func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) {503 func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) {
@@ -536,15 +537,15 @@ func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) {
536 }537 }
537 d.remoteCatalog.mu.Unlock()538 d.remoteCatalog.mu.Unlock()
538 539
539- results := d.paletteResults(current, nil, ports.RecentRouteSnapshot{540+ results := d.paletteResults(current, nil, protocol.RecentRouteSnapshot{
540 Generation: 2,541 Generation: 2,
541- Entries: []ports.RecentRouteEntry{{542+ Entries: []protocol.RecentRouteEntry{{
542 Key: 3, Generation: 1,543 Key: 3, Generation: 1,
543- Target: ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"},544+ Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "vev"},
544- Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote,545+ Name: "vev", HostLabel: "arch", Kind: protocol.RouteKindRemote,
545 }},546 }},
546 })547 })
547- var actions []ports.RouteNavigationAction548+ var actions []protocol.RouteNavigationAction
548 var catalogEndpoints []string549 var catalogEndpoints []string
549 for _, result := range results {550 for _, result := range results {
550 if result.DisplayText() != "Switch to session vev@arch" {551 if result.DisplayText() != "Switch to session vev@arch" {
@@ -558,7 +559,7 @@ func TestPaletteMatchesRecentRemoteRouteToCatalog(t *testing.T) {
558 require.True(t, ok)559 require.True(t, ok)
559 catalogEndpoints = append(catalogEndpoints, target.Endpoint)560 catalogEndpoints = append(catalogEndpoints, target.Endpoint)
560 }561 }
561- require.Equal(t, []ports.RouteNavigationAction{{562+ require.Equal(t, []protocol.RouteNavigationAction{{
562 SnapshotGeneration: 2, Key: 3, Generation: 1,563 SnapshotGeneration: 2, Key: 3, Generation: 1,
563 }}, actions)564 }}, actions)
564 require.ElementsMatch(t, test.wantCatalogEndpoints, catalogEndpoints)565 require.ElementsMatch(t, test.wantCatalogEndpoints, catalogEndpoints)
@@ -720,21 +721,21 @@ func TestPaletteResultsDeduplicateByLifecycleAndKeepEqualLabels(t *testing.T) {
720 local := addControlSession(d, "vev", "tab-2", "pane-2")721 local := addControlSession(d, "vev", "tab-2", "pane-2")
721 local.ephemeral = false722 local.ephemeral = false
722 local.incarnation = domain.SessionLifecycleID{2}723 local.incarnation = domain.SessionLifecycleID{2}
723- remoteOne := ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{21}, SessionName: "vev"}724+ remoteOne := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{21}, SessionName: "vev"}
724- remoteTwo := ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{22}, SessionName: "vev"}725+ remoteTwo := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{22}, SessionName: "vev"}
725- snapshot := ports.RecentRouteSnapshot{726+ snapshot := protocol.RecentRouteSnapshot{
726 Generation: 2,727 Generation: 2,
727- Entries: []ports.RecentRouteEntry{728+ Entries: []protocol.RecentRouteEntry{
728- {Key: 1, Generation: 1, Target: ports.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "hi"}, Name: "hi", Kind: ports.RouteKindLocal},729+ {Key: 1, Generation: 1, Target: protocol.ExactSessionTarget{LifecycleID: current.incarnation, SessionName: "hi"}, Name: "hi", Kind: protocol.RouteKindLocal},
729- {Key: 2, Generation: 1, Target: ports.ExactSessionTarget{LifecycleID: local.incarnation, SessionName: "vev"}, Name: "vev", Kind: ports.RouteKindLocal},730+ {Key: 2, Generation: 1, Target: protocol.ExactSessionTarget{LifecycleID: local.incarnation, SessionName: "vev"}, Name: "vev", Kind: protocol.RouteKindLocal},
730- {Key: 3, Generation: 1, Target: remoteOne, Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote},731+ {Key: 3, Generation: 1, Target: remoteOne, Name: "vev", HostLabel: "arch", Kind: protocol.RouteKindRemote},
731- {Key: 4, Generation: 1, Target: remoteTwo, Name: "vev", HostLabel: "arch", Kind: ports.RouteKindRemote},732+ {Key: 4, Generation: 1, Target: remoteTwo, Name: "vev", HostLabel: "arch", Kind: protocol.RouteKindRemote},
732 },733 },
733 }734 }
734 735
735 results := d.paletteResults(current, nil, snapshot)736 results := d.paletteResults(current, nil, snapshot)
736- var sessionTargets []ports.ExactSessionTarget737+ var sessionTargets []protocol.ExactSessionTarget
737- var routeActions []ports.RouteNavigationAction738+ var routeActions []protocol.RouteNavigationAction
738 for _, result := range results {739 for _, result := range results {
739 if target, ok := result.SessionTarget(); ok {740 if target, ok := result.SessionTarget(); ok {
740 sessionTargets = append(sessionTargets, target)741 sessionTargets = append(sessionTargets, target)
@@ -743,8 +744,8 @@ func TestPaletteResultsDeduplicateByLifecycleAndKeepEqualLabels(t *testing.T) {
743 routeActions = append(routeActions, action)744 routeActions = append(routeActions, action)
744 }745 }
745 }746 }
746- require.Equal(t, []ports.ExactSessionTarget{{LifecycleID: local.incarnation, SessionName: "vev"}}, sessionTargets)747+ require.Equal(t, []protocol.ExactSessionTarget{{LifecycleID: local.incarnation, SessionName: "vev"}}, sessionTargets)
747- require.Equal(t, []ports.RouteNavigationAction{748+ require.Equal(t, []protocol.RouteNavigationAction{
748 {SnapshotGeneration: 2, Key: 3, Generation: 1},749 {SnapshotGeneration: 2, Key: 3, Generation: 1},
749 {SnapshotGeneration: 2, Key: 4, Generation: 1},750 {SnapshotGeneration: 2, Key: 4, Generation: 1},
750 }, routeActions)751 }, routeActions)
@@ -781,12 +782,12 @@ func TestPaletteLifecycleTargetRejectsSameNameReplacement(t *testing.T) {
781 func TestPaletteFuzzyRemoteRecentRouteSendsExactNavigationAction(t *testing.T) {782 func TestPaletteFuzzyRemoteRecentRouteSendsExactNavigationAction(t *testing.T) {
782 d, current, ac, sends, releases := newRecentNavigationTestSessions(t)783 d, current, ac, sends, releases := newRecentNavigationTestSessions(t)
783 defer releaseAll(releases)784 defer releaseAll(releases)
784- ac.setRouteSnapshot(ports.RecentRouteSnapshot{785+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
785 Generation: 9,786 Generation: 9,
786- Active: ports.RouteRef{Key: 1, Generation: 1},787+ Active: protocol.RouteRef{Key: 1, Generation: 1},
787- ActiveEntry: testRouteEntry(1, 1, current.name, 1, ports.RouteKindLocal),788+ ActiveEntry: testRouteEntry(1, 1, current.name, 1, protocol.RouteKindLocal),
788- Entries: []ports.RecentRouteEntry{{789+ Entries: []protocol.RecentRouteEntry{{
789- Key: 8, Generation: 4, Target: testRouteTarget("logs", 8), Name: "logs", HostLabel: "edge", Kind: ports.RouteKindRemote,790+ Key: 8, Generation: 4, Target: testRouteTarget("logs", 8), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote,
790 }},791 }},
791 })792 })
792 token := beginRecentRoutePaletteEffect(t, d, current, ac)793 token := beginRecentRoutePaletteEffect(t, d, current, ac)
@@ -798,7 +799,7 @@ func TestPaletteFuzzyRemoteRecentRouteSendsExactNavigationAction(t *testing.T) {
798 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)799 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
799 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)800 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
800 require.NoError(t, err)801 require.NoError(t, err)
801- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 9, Key: 8, Generation: 4}, action)802+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 9, Key: 8, Generation: 4}, action)
802 require.False(t, ac.overlays.paletteActive())803 require.False(t, ac.overlays.paletteActive())
803 }804 }
804 805
@@ -830,7 +831,7 @@ func TestPaletteJRSUsesCapturedRankAfterMRUChanges(t *testing.T) {
830 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)831 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
831 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)832 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
832 require.NoError(t, err)833 require.NoError(t, err)
833- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)834+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 2, Generation: 1}, action)
834 require.Same(t, current, ac.currentSession())835 require.Same(t, current, ac.currentSession())
835 require.False(t, ac.overlays.paletteActive())836 require.False(t, ac.overlays.paletteActive())
836 }837 }
@@ -861,11 +862,11 @@ func TestPaletteJRSDoesNotRevalidateTargetInDaemon(t *testing.T) {
861 p, release := newBlockingPTY(t)862 p, release := newBlockingPTY(t)
862 defer release()863 defer release()
863 d, sess, ac, sends := newManualSessionWithPTYs(t, p)864 d, sess, ac, sends := newManualSessionWithPTYs(t, p)
864- ac.setRouteSnapshot(ports.RecentRouteSnapshot{865+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
865 Generation: 7,866 Generation: 7,
866- Active: ports.RouteRef{Key: 1, Generation: 1},867+ Active: protocol.RouteRef{Key: 1, Generation: 1},
867- ActiveEntry: testRouteEntry(1, 1, sess.name, 1, ports.RouteKindLocal),868+ ActiveEntry: testRouteEntry(1, 1, sess.name, 1, protocol.RouteKindLocal),
868- Entries: []ports.RecentRouteEntry{testRouteEntry(9, 4, "captured", 9, ports.RouteKindLocal)},869+ Entries: []protocol.RecentRouteEntry{testRouteEntry(9, 4, "captured", 9, protocol.RouteKindLocal)},
869 })870 })
870 871
871 validated := make(chan struct{})872 validated := make(chan struct{})
@@ -891,7 +892,7 @@ func TestPaletteJRSDoesNotRevalidateTargetInDaemon(t *testing.T) {
891 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)892 actionFrame := awaitFrame(t, sends, ports.MsgNavigateRecentRoute)
892 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)893 action, err := ports.UnmarshalRouteNavigationAction(actionFrame.Payload)
893 require.NoError(t, err)894 require.NoError(t, err)
894- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 7, Key: 9, Generation: 4}, action)895+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 7, Key: 9, Generation: 4}, action)
895 require.False(t, ac.overlays.paletteActive())896 require.False(t, ac.overlays.paletteActive())
896 }897 }
897 898
M internal/usecase/daemon/parked_route.go +52 -51
@@ -8,12 +8,13 @@ import (
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 )12 )
12 13
13 const parkedRouteLeaseTTL = 15 * time.Minute14 const parkedRouteLeaseTTL = 15 * time.Minute
14 15
15 type parkedRouteLease struct {16 type parkedRouteLease struct {
16- id ports.ParkedRouteLeaseID17+ id protocol.ParkedRouteLeaseID
17 generation uint6418 generation uint64
18 transport transportSnapshot19 transport transportSnapshot
19 expiresAt time.Time20 expiresAt time.Time
@@ -24,14 +25,14 @@ type parkedRouteLease struct {
24 expired bool25 expired bool
25 }26 }
26 27
27-func (lease *parkedRouteLease) matches(id ports.ParkedRouteLeaseID, generation uint64, transport transportSnapshot) bool {28+func (lease *parkedRouteLease) matches(id protocol.ParkedRouteLeaseID, generation uint64, transport transportSnapshot) bool {
28 return lease != nil && lease.id == id && lease.generation == generation && lease.transport == transport29 return lease != nil && lease.id == id && lease.generation == generation && lease.transport == transport
29 }30 }
30 31
31-func newParkedRouteLeaseID() (ports.ParkedRouteLeaseID, error) {32+func newParkedRouteLeaseID() (protocol.ParkedRouteLeaseID, error) {
32- var id ports.ParkedRouteLeaseID33+ var id protocol.ParkedRouteLeaseID
33 if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {34 if _, err := io.ReadFull(rand.Reader, id[:]); err != nil {
34- return ports.ParkedRouteLeaseID{}, err35+ return protocol.ParkedRouteLeaseID{}, err
35 }36 }
36 return id, nil37 return id, nil
37 }38 }
@@ -50,17 +51,17 @@ func stopParkedRouteLeaseLocked(lease *parkedRouteLease) {
50 }51 }
51 }52 }
52 53
53-func (d *Daemon) armParkedRoute(effect *attachmentEffect) (ports.ParkedRouteLeaseID, error) {54+func (d *Daemon) armParkedRoute(effect *attachmentEffect) (protocol.ParkedRouteLeaseID, error) {
54 if !effect.current() {55 if !effect.current() {
55- return ports.ParkedRouteLeaseID{}, errAttachmentTransition56+ return protocol.ParkedRouteLeaseID{}, errAttachmentTransition
56 }57 }
57 ac := effect.ac58 ac := effect.ac
58- if ac == nil || ac.navigationCapabilities&ports.NavigationCapabilityHomePicker == 0 {59+ if ac == nil || ac.navigationCapabilities&protocol.NavigationCapabilityHomePicker == 0 {
59- return ports.ParkedRouteLeaseID{}, errAttachmentTransition60+ return protocol.ParkedRouteLeaseID{}, errAttachmentTransition
60 }61 }
61 id, err := newParkedRouteLeaseID()62 id, err := newParkedRouteLeaseID()
62 if err != nil {63 if err != nil {
63- return ports.ParkedRouteLeaseID{}, err64+ return protocol.ParkedRouteLeaseID{}, err
64 }65 }
65 lease := &parkedRouteLease{66 lease := &parkedRouteLease{
66 id: id, generation: effect.generation, transport: effect.transport,67 id: id, generation: effect.generation, transport: effect.transport,
@@ -123,30 +124,30 @@ func (ac *attachedClient) clearParkedRoute() {
123 ac.parkedRouteMu.Unlock()124 ac.parkedRouteMu.Unlock()
124 }125 }
125 126
126-func (ac *attachedClient) parkedRouteStatus(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time, suspended bool) ports.ParkedRouteStatus {127+func (ac *attachedClient) parkedRouteStatus(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time, suspended bool) protocol.ParkedRouteStatus {
127 if ac == nil || !token.current() {128 if ac == nil || !token.current() {
128- return ports.ParkedRouteRejected129+ return protocol.ParkedRouteRejected
129 }130 }
130 ac.parkedRouteMu.Lock()131 ac.parkedRouteMu.Lock()
131 defer ac.parkedRouteMu.Unlock()132 defer ac.parkedRouteMu.Unlock()
132 lease := ac.parkedRoute133 lease := ac.parkedRoute
133 if !lease.matches(id, token.generation, token.transport) {134 if !lease.matches(id, token.generation, token.transport) {
134- return ports.ParkedRouteRejected135+ return protocol.ParkedRouteRejected
135 }136 }
136 if !now.Before(lease.expiresAt) {137 if !now.Before(lease.expiresAt) {
137 stopParkedRouteLeaseLocked(lease)138 stopParkedRouteLeaseLocked(lease)
138 ac.parkedRoute = nil139 ac.parkedRoute = nil
139 ac.parkedRouteOutput.Store(false)140 ac.parkedRouteOutput.Store(false)
140 ac.parkedRouteFullPending.Store(false)141 ac.parkedRouteFullPending.Store(false)
141- return ports.ParkedRouteExpired142+ return protocol.ParkedRouteExpired
142 }143 }
143 if lease.suspended != suspended {144 if lease.suspended != suspended {
144- return ports.ParkedRouteRejected145+ return protocol.ParkedRouteRejected
145 }146 }
146 return 0147 return 0
147 }148 }
148 149
149-func (ac *attachedClient) prepareParkedRoute(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {150+func (ac *attachedClient) prepareParkedRoute(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
150 if status := ac.parkedRouteStatus(token, id, now, false); status != 0 {151 if status := ac.parkedRouteStatus(token, id, now, false); status != 0 {
151 return status152 return status
152 }153 }
@@ -154,14 +155,14 @@ func (ac *attachedClient) prepareParkedRoute(token attachmentCapability, id port
154 defer ac.parkedRouteMu.Unlock()155 defer ac.parkedRouteMu.Unlock()
155 lease := ac.parkedRoute156 lease := ac.parkedRoute
156 if !lease.matches(id, token.generation, token.transport) || lease.suspended {157 if !lease.matches(id, token.generation, token.transport) || lease.suspended {
157- return ports.ParkedRouteRejected158+ return protocol.ParkedRouteRejected
158 }159 }
159 lease.suspended = true160 lease.suspended = true
160 ac.parkedRouteOutput.Store(true)161 ac.parkedRouteOutput.Store(true)
161 return 0162 return 0
162 }163 }
163 164
164-func (ac *attachedClient) consumeParkedRoute(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {165+func (ac *attachedClient) consumeParkedRoute(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
165 if status := ac.parkedRouteStatus(token, id, now, true); status != 0 {166 if status := ac.parkedRouteStatus(token, id, now, true); status != 0 {
166 return status167 return status
167 }168 }
@@ -169,43 +170,43 @@ func (ac *attachedClient) consumeParkedRoute(token attachmentCapability, id port
169 defer ac.parkedRouteMu.Unlock()170 defer ac.parkedRouteMu.Unlock()
170 lease := ac.parkedRoute171 lease := ac.parkedRoute
171 if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {172 if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
172- return ports.ParkedRouteRejected173+ return protocol.ParkedRouteRejected
173 }174 }
174 stopParkedRouteLeaseLocked(lease)175 stopParkedRouteLeaseLocked(lease)
175 ac.parkedRoute = nil176 ac.parkedRoute = nil
176 return 0177 return 0
177 }178 }
178 179
179-func (ac *attachedClient) beginParkedRouteSwitch(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {180+func (ac *attachedClient) beginParkedRouteSwitch(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
180 if ac == nil || !token.current() {181 if ac == nil || !token.current() {
181- return ports.ParkedRouteRejected182+ return protocol.ParkedRouteRejected
182 }183 }
183 ac.parkedRouteMu.Lock()184 ac.parkedRouteMu.Lock()
184 defer ac.parkedRouteMu.Unlock()185 defer ac.parkedRouteMu.Unlock()
185 lease := ac.parkedRoute186 lease := ac.parkedRoute
186 if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {187 if !lease.matches(id, token.generation, token.transport) || !lease.suspended || lease.inFlight {
187- return ports.ParkedRouteRejected188+ return protocol.ParkedRouteRejected
188 }189 }
189 if lease.expired || !now.Before(lease.expiresAt) {190 if lease.expired || !now.Before(lease.expiresAt) {
190 stopParkedRouteLeaseLocked(lease)191 stopParkedRouteLeaseLocked(lease)
191 ac.parkedRoute = nil192 ac.parkedRoute = nil
192 ac.parkedRouteOutput.Store(false)193 ac.parkedRouteOutput.Store(false)
193 ac.parkedRouteFullPending.Store(false)194 ac.parkedRouteFullPending.Store(false)
194- return ports.ParkedRouteExpired195+ return protocol.ParkedRouteExpired
195 }196 }
196 lease.inFlight = true197 lease.inFlight = true
197 return 0198 return 0
198 }199 }
199 200
200-func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentCapability, id ports.ParkedRouteLeaseID, now time.Time) ports.ParkedRouteStatus {201+func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentCapability, id protocol.ParkedRouteLeaseID, now time.Time) protocol.ParkedRouteStatus {
201 if ac == nil {202 if ac == nil {
202- return ports.ParkedRouteRejected203+ return protocol.ParkedRouteRejected
203 }204 }
204 ac.parkedRouteMu.Lock()205 ac.parkedRouteMu.Lock()
205 defer ac.parkedRouteMu.Unlock()206 defer ac.parkedRouteMu.Unlock()
206 lease := ac.parkedRoute207 lease := ac.parkedRoute
207 if !lease.matches(id, token.generation, token.transport) || !lease.inFlight {208 if !lease.matches(id, token.generation, token.transport) || !lease.inFlight {
208- return ports.ParkedRouteRejected209+ return protocol.ParkedRouteRejected
209 }210 }
210 lease.inFlight = false211 lease.inFlight = false
211 if lease.expired || !now.Before(lease.expiresAt) {212 if lease.expired || !now.Before(lease.expiresAt) {
@@ -213,20 +214,20 @@ func (ac *attachedClient) rejectParkedRouteSwitch(token attachmentCapability, id
213 ac.parkedRoute = nil214 ac.parkedRoute = nil
214 ac.parkedRouteOutput.Store(false)215 ac.parkedRouteOutput.Store(false)
215 ac.parkedRouteFullPending.Store(false)216 ac.parkedRouteFullPending.Store(false)
216- return ports.ParkedRouteExpired217+ return protocol.ParkedRouteExpired
217 }218 }
218 return 0219 return 0
219 }220 }
220 221
221-func (ac *attachedClient) consumePublishedParkedRoute(previous, published attachmentCapability, id ports.ParkedRouteLeaseID) ports.ParkedRouteStatus {222+func (ac *attachedClient) consumePublishedParkedRoute(previous, published attachmentCapability, id protocol.ParkedRouteLeaseID) protocol.ParkedRouteStatus {
222 if ac == nil || published.ac != ac || !published.current() {223 if ac == nil || published.ac != ac || !published.current() {
223- return ports.ParkedRouteRejected224+ return protocol.ParkedRouteRejected
224 }225 }
225 ac.parkedRouteMu.Lock()226 ac.parkedRouteMu.Lock()
226 defer ac.parkedRouteMu.Unlock()227 defer ac.parkedRouteMu.Unlock()
227 lease := ac.parkedRoute228 lease := ac.parkedRoute
228 if !lease.matches(id, previous.generation, previous.transport) || !lease.suspended || !lease.inFlight {229 if !lease.matches(id, previous.generation, previous.transport) || !lease.suspended || !lease.inFlight {
229- return ports.ParkedRouteRejected230+ return protocol.ParkedRouteRejected
230 }231 }
231 // Expiry is checked when the request is admitted. Once publication starts,232 // Expiry is checked when the request is admitted. Once publication starts,
232 // the accepted request must commit atomically instead of turning a completed233 // the accepted request must commit atomically instead of turning a completed
@@ -236,7 +237,7 @@ func (ac *attachedClient) consumePublishedParkedRoute(previous, published attach
236 return 0237 return 0
237 }238 }
238 239
239-func parkedRouteResponseFrame(response ports.ParkedRouteResponse) (ports.Frame, error) {240+func parkedRouteResponseFrame(response protocol.ParkedRouteResponse) (ports.Frame, error) {
240 payload := ports.MarshalParkedRouteResponse(response)241 payload := ports.MarshalParkedRouteResponse(response)
241 if payload == nil {242 if payload == nil {
242 return ports.Frame{}, errAttachmentTransition243 return ports.Frame{}, errAttachmentTransition
@@ -244,7 +245,7 @@ func parkedRouteResponseFrame(response ports.ParkedRouteResponse) (ports.Frame,
244 return ports.Frame{Type: ports.MsgParkedRouteResponse, Payload: payload}, nil245 return ports.Frame{Type: ports.MsgParkedRouteResponse, Payload: payload}, nil
245 }246 }
246 247
247-func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response ports.ParkedRouteResponse) error {248+func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response protocol.ParkedRouteResponse) error {
248 frame, err := parkedRouteResponseFrame(response)249 frame, err := parkedRouteResponseFrame(response)
249 if err != nil {250 if err != nil {
250 return err251 return err
@@ -254,7 +255,7 @@ func (d *Daemon) sendParkedRouteResponse(effect *attachmentEffect, response port
254 255
255 // sendParkedRouteResponseLocked preserves response -> full-output ordering.256 // sendParkedRouteResponseLocked preserves response -> full-output ordering.
256 // Caller holds ac.sendMu and keeps parkedRouteOutput set through this send.257 // Caller holds ac.sendMu and keeps parkedRouteOutput set through this send.
257-func (d *Daemon) sendParkedRouteResponseLocked(ac *attachedClient, expected transportSnapshot, response ports.ParkedRouteResponse) error {258+func (d *Daemon) sendParkedRouteResponseLocked(ac *attachedClient, expected transportSnapshot, response protocol.ParkedRouteResponse) error {
258 frame, err := parkedRouteResponseFrame(response)259 frame, err := parkedRouteResponseFrame(response)
259 if err != nil {260 if err != nil {
260 return err261 return err
@@ -283,7 +284,7 @@ func (d *Daemon) parkedRouteResponseFailed(effect *attachmentEffect) {
283 launch()284 launch()
284 }285 }
285 286
286-func (d *Daemon) respondParkedRoute(effect *attachmentEffect, response ports.ParkedRouteResponse) bool {287+func (d *Daemon) respondParkedRoute(effect *attachmentEffect, response protocol.ParkedRouteResponse) bool {
287 if effect == nil || effect.ac == nil {288 if effect == nil || effect.ac == nil {
288 return false289 return false
289 }290 }
@@ -307,35 +308,35 @@ func (d *Daemon) respondParkedRoute(effect *attachmentEffect, response ports.Par
307 return true308 return true
308 }309 }
309 310
310-func (d *Daemon) handleParkedRouteRequest(effect *attachmentEffect, request ports.ParkedRouteRequest) {311+func (d *Daemon) handleParkedRouteRequest(effect *attachmentEffect, request protocol.ParkedRouteRequest) {
311- if ports.ValidateParkedRouteRequest(request) != nil || !effect.current() || effect.ac == nil {312+ if protocol.ValidateParkedRouteRequest(request) != nil || !effect.current() || effect.ac == nil {
312 return313 return
313 }314 }
314 capability := effect.capability()315 capability := effect.capability()
315 switch request.Action {316 switch request.Action {
316- case ports.ParkedRoutePrepare:317+ case protocol.ParkedRoutePrepare:
317 status := effect.ac.prepareParkedRoute(capability, request.LeaseID, d.clock.Now())318 status := effect.ac.prepareParkedRoute(capability, request.LeaseID, d.clock.Now())
318 if status == 0 {319 if status == 0 {
319- status = ports.ParkedRouteReady320+ status = protocol.ParkedRouteReady
320 }321 }
321- d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status})322+ d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status})
322- case ports.ParkedRouteResume:323+ case protocol.ParkedRouteResume:
323 d.resumeParkedRoute(effect, request)324 d.resumeParkedRoute(effect, request)
324- case ports.ParkedRouteSwitch:325+ case protocol.ParkedRouteSwitch:
325 d.switchParkedRoute(effect, request)326 d.switchParkedRoute(effect, request)
326 }327 }
327 }328 }
328 329
329-func (d *Daemon) resumeParkedRoute(effect *attachmentEffect, request ports.ParkedRouteRequest) {330+func (d *Daemon) resumeParkedRoute(effect *attachmentEffect, request protocol.ParkedRouteRequest) {
330 capability := effect.capability()331 capability := effect.capability()
331 status := effect.ac.consumeParkedRoute(capability, request.LeaseID, d.clock.Now())332 status := effect.ac.consumeParkedRoute(capability, request.LeaseID, d.clock.Now())
332 if status != 0 {333 if status != 0 {
333- d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status})334+ d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status})
334 return335 return
335 }336 }
336 ac := effect.ac337 ac := effect.ac
337 ac.sendMu.Lock()338 ac.sendMu.Lock()
338- err := d.sendParkedRouteResponseLocked(ac, effect.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteResumed})339+ err := d.sendParkedRouteResponseLocked(ac, effect.transport, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: protocol.ParkedRouteResumed})
339 if err == nil {340 if err == nil {
340 ac.releaseParkedOutputLocked()341 ac.releaseParkedOutputLocked()
341 }342 }
@@ -349,21 +350,21 @@ func (d *Daemon) resumeParkedRoute(effect *attachmentEffect, request ports.Parke
349 }350 }
350 }351 }
351 352
352-func (d *Daemon) rejectParkedRouteSwitch(effect *attachmentEffect, request ports.ParkedRouteRequest, fallback ports.ParkedRouteStatus) {353+func (d *Daemon) rejectParkedRouteSwitch(effect *attachmentEffect, request protocol.ParkedRouteRequest, fallback protocol.ParkedRouteStatus) {
353 capability := effect.capability()354 capability := effect.capability()
354 status := effect.ac.rejectParkedRouteSwitch(capability, request.LeaseID, d.clock.Now())355 status := effect.ac.rejectParkedRouteSwitch(capability, request.LeaseID, d.clock.Now())
355 if status == 0 {356 if status == 0 {
356 status = fallback357 status = fallback
357 }358 }
358- if d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired {359+ if d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == protocol.ParkedRouteExpired {
359 _ = effect.ac.closeCapturedTransport(effect.transport.transport)360 _ = effect.ac.closeCapturedTransport(effect.transport.transport)
360 }361 }
361 }362 }
362 363
363-func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request ports.ParkedRouteRequest) {364+func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request protocol.ParkedRouteRequest) {
364 capability := effect.capability()365 capability := effect.capability()
365 if status := effect.ac.beginParkedRouteSwitch(capability, request.LeaseID, d.clock.Now()); status != 0 {366 if status := effect.ac.beginParkedRouteSwitch(capability, request.LeaseID, d.clock.Now()); status != 0 {
366- if d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == ports.ParkedRouteExpired {367+ if d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: status}) && status == protocol.ParkedRouteExpired {
367 _ = effect.ac.closeCapturedTransport(effect.transport.transport)368 _ = effect.ac.closeCapturedTransport(effect.transport.transport)
368 }369 }
369 return370 return
@@ -374,13 +375,13 @@ func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request ports.Parke
374 ctx = context.Background()375 ctx = context.Background()
375 }376 }
376 if err := d.waitForParkedTargetRestore(ctx, target.SessionName); err != nil {377 if err := d.waitForParkedTargetRestore(ctx, target.SessionName); err != nil {
377- d.rejectParkedRouteSwitch(effect, request, ports.ParkedRouteStaleTarget)378+ d.rejectParkedRouteSwitch(effect, request, protocol.ParkedRouteStaleTarget)
378 return379 return
379 }380 }
380 381
381 transition, targetSession, err := d.transitionParkedRouteTarget(effect, target)382 transition, targetSession, err := d.transitionParkedRouteTarget(effect, target)
382 if err != nil {383 if err != nil {
383- d.rejectParkedRouteSwitch(effect, request, ports.ParkedRouteStaleTarget)384+ d.rejectParkedRouteSwitch(effect, request, protocol.ParkedRouteStaleTarget)
384 return385 return
385 }386 }
386 if status := effect.ac.consumePublishedParkedRoute(capability, transition.published, request.LeaseID); status != 0 {387 if status := effect.ac.consumePublishedParkedRoute(capability, transition.published, request.LeaseID); status != 0 {
@@ -395,7 +396,7 @@ func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request ports.Parke
395 freshCapability := fresh.capability()396 freshCapability := fresh.capability()
396 ac := effect.ac397 ac := effect.ac
397 ac.sendMu.Lock()398 ac.sendMu.Lock()
398- responseErr := d.sendParkedRouteResponseLocked(ac, freshCapability.transport, ports.ParkedRouteResponse{RequestID: request.RequestID, Status: ports.ParkedRouteSwitched})399+ responseErr := d.sendParkedRouteResponseLocked(ac, freshCapability.transport, protocol.ParkedRouteResponse{RequestID: request.RequestID, Status: protocol.ParkedRouteSwitched})
399 if responseErr == nil {400 if responseErr == nil {
400 ac.releaseParkedOutputLocked()401 ac.releaseParkedOutputLocked()
401 }402 }
M internal/usecase/daemon/parked_route_test.go +36 -35
@@ -13,6 +13,7 @@ import (
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15 portsmocks "github.com/bnema/vev/internal/ports/mocks"15 portsmocks "github.com/bnema/vev/internal/ports/mocks"
16+ "github.com/bnema/vev/internal/protocol"
16 )17 )
17 18
18 type parkedRouteFailTransport struct{}19 type parkedRouteFailTransport struct{}
@@ -61,31 +62,31 @@ func (c *parkedRouteClock) setNow(now time.Time) {
61 c.mu.Unlock()62 c.mu.Unlock()
62 }63 }
63 64
64-func armAndPrepareParkedRoute(t *testing.T, d *Daemon, source *session, ac *attachedClient, sends chan ports.Frame) ports.ParkedRouteLeaseID {65+func armAndPrepareParkedRoute(t *testing.T, d *Daemon, source *session, ac *attachedClient, sends chan ports.Frame) protocol.ParkedRouteLeaseID {
65 t.Helper()66 t.Helper()
66- ac.navigationCapabilities |= ports.NavigationCapabilityHomePicker67+ ac.navigationCapabilities |= protocol.NavigationCapabilityHomePicker
67 token := source.captureAttachmentCapability(ac, ac.transport())68 token := source.captureAttachmentCapability(ac, ac.transport())
68 effect, admitted := ac.beginAttachmentEffect(token)69 effect, admitted := ac.beginAttachmentEffect(token)
69 require.True(t, admitted)70 require.True(t, admitted)
70- require.NoError(t, d.sendNavigationActionForAttachment(effect, ports.NavigationOpenHomePicker))71+ require.NoError(t, d.sendNavigationActionForAttachment(effect, protocol.NavigationOpenHomePicker))
71 effect.End()72 effect.End()
72 directiveFrame := awaitFrame(t, sends, ports.MsgNavigationAction)73 directiveFrame := awaitFrame(t, sends, ports.MsgNavigationAction)
73 directive, err := ports.UnmarshalNavigationDirective(directiveFrame.Payload)74 directive, err := ports.UnmarshalNavigationDirective(directiveFrame.Payload)
74 require.NoError(t, err)75 require.NoError(t, err)
75 76
76- prepare := ports.ParkedRouteRequest{RequestID: 1, LeaseID: directive.LeaseID, Action: ports.ParkedRoutePrepare}77+ prepare := protocol.ParkedRouteRequest{RequestID: 1, LeaseID: directive.LeaseID, Action: protocol.ParkedRoutePrepare}
77 require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(prepare)}))78 require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(prepare)}))
78 readyFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)79 readyFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
79 ready, err := ports.UnmarshalParkedRouteResponse(readyFrame.Payload)80 ready, err := ports.UnmarshalParkedRouteResponse(readyFrame.Payload)
80 require.NoError(t, err)81 require.NoError(t, err)
81- require.Equal(t, ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady}, ready)82+ require.Equal(t, protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}, ready)
82 return directive.LeaseID83 return directive.LeaseID
83 }84 }
84 85
85 func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {86 func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
86 d, source, ac, sends, releases := newManualTabSession(t, 1)87 d, source, ac, sends, releases := newManualTabSession(t, 1)
87 defer releaseAll(releases)88 defer releaseAll(releases)
88- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker89+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
89 90
90 targetLifecycle := domain.SessionLifecycleID{7}91 targetLifecycle := domain.SessionLifecycleID{7}
91 target := &session{92 target := &session{
@@ -97,7 +98,7 @@ func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
97 d.mu.Lock()98 d.mu.Lock()
98 d.sessions[target.id] = target99 d.sessions[target.id] = target
99 d.mu.Unlock()100 d.mu.Unlock()
100- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})101+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
101 102
102 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)103 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
103 require.True(t, ac.parkedRouteOutput.Load())104 require.True(t, ac.parkedRouteOutput.Load())
@@ -106,7 +107,7 @@ func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
106 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: targetLifecycle,107 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: targetLifecycle,
107 SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID),108 SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID),
108 }109 }
109- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest}110+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &targetRequest}
110 require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}))111 require.False(t, d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)}))
111 112
112 require.Same(t, target, ac.currentAttachmentSession())113 require.Same(t, target, ac.currentAttachmentSession())
@@ -114,7 +115,7 @@ func TestParkedRouteSwitchesExactLiveTargetOnRetainedTransport(t *testing.T) {
114 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)115 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
115 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)116 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)
116 require.NoError(t, err)117 require.NoError(t, err)
117- require.Equal(t, ports.ParkedRouteResponse{RequestID: 2, Status: ports.ParkedRouteSwitched}, response)118+ require.Equal(t, protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteSwitched}, response)
118 }119 }
119 120
120 func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.T) {121 func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.T) {
@@ -122,7 +123,7 @@ func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.
122 defer releaseAll(releases)123 defer releaseAll(releases)
123 clock := &parkedRouteClock{}124 clock := &parkedRouteClock{}
124 d.clock = clock125 d.clock = clock
125- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker126+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
126 127
127 targetLifecycle := domain.SessionLifecycleID{7}128 targetLifecycle := domain.SessionLifecycleID{7}
128 target := &session{129 target := &session{
@@ -134,7 +135,7 @@ func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.
134 d.mu.Lock()135 d.mu.Lock()
135 d.sessions[target.id] = target136 d.sessions[target.id] = target
136 d.mu.Unlock()137 d.mu.Unlock()
137- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})138+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
138 139
139 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)140 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
140 d.afterAttachmentEffectsFrozen = func() {141 d.afterAttachmentEffectsFrozen = func() {
@@ -145,21 +146,21 @@ func TestParkedRouteSwitchAcceptedBeforeLeaseExpiryCommitsAtomically(t *testing.
145 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: targetLifecycle,146 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: targetLifecycle,
146 SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID),147 SessionName: "target", LiveTabID: domain.TabStableID(target.tabs[0].stableID),
147 }148 }
148- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &targetRequest}149+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &targetRequest}
149 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})150 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})
150 151
151 require.Same(t, target, ac.currentAttachmentSession())152 require.Same(t, target, ac.currentAttachmentSession())
152 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)153 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
153 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)154 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)
154 require.NoError(t, err)155 require.NoError(t, err)
155- require.Equal(t, ports.ParkedRouteSwitched, response.Status)156+ require.Equal(t, protocol.ParkedRouteSwitched, response.Status)
156 }157 }
157 158
158 func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T) {159 func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T) {
159 d := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})160 d := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
160 d.baseEnv = []string{"OWNER=daemon"}161 d.baseEnv = []string{"OWNER=daemon"}
161 source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source")162 source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source")
162- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker163+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
163 source.mu.Lock()164 source.mu.Lock()
164 source.env = []string{"OWNER=source"}165 source.env = []string{"OWNER=source"}
165 source.mu.Unlock()166 source.mu.Unlock()
@@ -175,7 +176,7 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
175 },176 },
176 }177 }
177 d.mu.Unlock()178 d.mu.Unlock()
178- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})179+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
179 180
180 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)181 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
181 182
@@ -183,7 +184,7 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
183 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: lifecycle,184 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: lifecycle,
184 SessionName: "stopped", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-b"),185 SessionName: "stopped", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-b"),
185 }186 }
186- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &target}187+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &target}
187 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})188 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})
188 189
189 restored := ac.currentAttachmentSession()190 restored := ac.currentAttachmentSession()
@@ -199,8 +200,8 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
199 func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) {200 func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) {
200 d, source, ac, sends, releases := newManualTabSession(t, 1)201 d, source, ac, sends, releases := newManualTabSession(t, 1)
201 defer releaseAll(releases)202 defer releaseAll(releases)
202- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker203+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
203- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})204+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
204 205
205 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)206 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
206 207
@@ -208,21 +209,21 @@ func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) {
208 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{9},209 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{9},
209 SessionName: "missing", LiveTabID: "tab-1",210 SessionName: "missing", LiveTabID: "tab-1",
210 }211 }
211- switchRequest := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteSwitch, Target: &missing}212+ switchRequest := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch, Target: &missing}
212 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})213 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(switchRequest)})
213 staleFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)214 staleFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
214 stale, err := ports.UnmarshalParkedRouteResponse(staleFrame.Payload)215 stale, err := ports.UnmarshalParkedRouteResponse(staleFrame.Payload)
215 require.NoError(t, err)216 require.NoError(t, err)
216- require.Equal(t, ports.ParkedRouteStaleTarget, stale.Status)217+ require.Equal(t, protocol.ParkedRouteStaleTarget, stale.Status)
217 require.Same(t, source, ac.currentAttachmentSession())218 require.Same(t, source, ac.currentAttachmentSession())
218 require.True(t, ac.parkedRouteOutput.Load())219 require.True(t, ac.parkedRouteOutput.Load())
219 220
220- resume := ports.ParkedRouteRequest{RequestID: 3, LeaseID: leaseID, Action: ports.ParkedRouteResume}221+ resume := protocol.ParkedRouteRequest{RequestID: 3, LeaseID: leaseID, Action: protocol.ParkedRouteResume}
221 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})222 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})
222 resumedFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)223 resumedFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
223 resumed, err := ports.UnmarshalParkedRouteResponse(resumedFrame.Payload)224 resumed, err := ports.UnmarshalParkedRouteResponse(resumedFrame.Payload)
224 require.NoError(t, err)225 require.NoError(t, err)
225- require.Equal(t, ports.ParkedRouteResumed, resumed.Status)226+ require.Equal(t, protocol.ParkedRouteResumed, resumed.Status)
226 require.False(t, ac.parkedRouteOutput.Load())227 require.False(t, ac.parkedRouteOutput.Load())
227 outputFrame := awaitFrame(t, sends, ports.MsgOutput)228 outputFrame := awaitFrame(t, sends, ports.MsgOutput)
228 output, err := ports.UnmarshalOutput(outputFrame.Payload)229 output, err := ports.UnmarshalOutput(outputFrame.Payload)
@@ -233,7 +234,7 @@ func TestParkedRouteStaleTargetLeavesSourceResumable(t *testing.T) {
233 func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) {234 func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) {
234 d, source, ac, sends, releases := newManualTabSession(t, 1)235 d, source, ac, sends, releases := newManualTabSession(t, 1)
235 defer releaseAll(releases)236 defer releaseAll(releases)
236- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker237+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
237 238
238 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)239 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
239 require.NoError(t, d.boundedSendOutputErr(ac, []byte("one-shot effect")))240 require.NoError(t, d.boundedSendOutputErr(ac, []byte("one-shot effect")))
@@ -243,7 +244,7 @@ func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) {
243 default:244 default:
244 }245 }
245 246
246- resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume}247+ resume := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteResume}
247 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})248 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})
248 _ = awaitFrame(t, sends, ports.MsgParkedRouteResponse)249 _ = awaitFrame(t, sends, ports.MsgParkedRouteResponse)
249 outputFrame := awaitFrame(t, sends, ports.MsgOutput)250 outputFrame := awaitFrame(t, sends, ports.MsgOutput)
@@ -261,19 +262,19 @@ func TestParkedRouteDoesNotReplayOneShotEffectsAfterResume(t *testing.T) {
261 func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) {262 func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) {
262 d, source, ac, sends, releases := newManualTabSession(t, 1)263 d, source, ac, sends, releases := newManualTabSession(t, 1)
263 defer releaseAll(releases)264 defer releaseAll(releases)
264- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker265+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
265 266
266 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)267 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
267 268
268 ac.parkedRouteMu.Lock()269 ac.parkedRouteMu.Lock()
269 ac.parkedRoute.expiresAt = d.clock.Now()270 ac.parkedRoute.expiresAt = d.clock.Now()
270 ac.parkedRouteMu.Unlock()271 ac.parkedRouteMu.Unlock()
271- resume := ports.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: ports.ParkedRouteResume}272+ resume := protocol.ParkedRouteRequest{RequestID: 2, LeaseID: leaseID, Action: protocol.ParkedRouteResume}
272 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})273 d.handleAttachmentClientFrame(source.captureAttachmentCapability(ac, ac.transport()), ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: ports.MarshalParkedRouteRequest(resume)})
273 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)274 responseFrame := awaitFrame(t, sends, ports.MsgParkedRouteResponse)
274 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)275 response, err := ports.UnmarshalParkedRouteResponse(responseFrame.Payload)
275 require.NoError(t, err)276 require.NoError(t, err)
276- require.Equal(t, ports.ParkedRouteExpired, response.Status)277+ require.Equal(t, protocol.ParkedRouteExpired, response.Status)
277 require.Same(t, source, ac.currentAttachmentSession())278 require.Same(t, source, ac.currentAttachmentSession())
278 require.False(t, ac.parkedRouteOutput.Load())279 require.False(t, ac.parkedRouteOutput.Load())
279 }280 }
@@ -281,7 +282,7 @@ func TestParkedRouteExpiredLeaseFailsClosed(t *testing.T) {
281 func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) {282 func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) {
282 d, source, ac, sends, releases := newManualTabSession(t, 1)283 d, source, ac, sends, releases := newManualTabSession(t, 1)
283 defer releaseAll(releases)284 defer releaseAll(releases)
284- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker285+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
285 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)286 leaseID := armAndPrepareParkedRoute(t, d, source, ac, sends)
286 287
287 ac.parkedRouteMu.Lock()288 ac.parkedRouteMu.Lock()
@@ -289,7 +290,7 @@ func TestParkedRouteResumeRejectsSwitchInFlight(t *testing.T) {
289 ac.parkedRouteMu.Unlock()290 ac.parkedRouteMu.Unlock()
290 status := ac.consumeParkedRoute(source.captureAttachmentCapability(ac, ac.transport()), leaseID, d.clock.Now())291 status := ac.consumeParkedRoute(source.captureAttachmentCapability(ac, ac.transport()), leaseID, d.clock.Now())
291 292
292- require.Equal(t, ports.ParkedRouteRejected, status)293+ require.Equal(t, protocol.ParkedRouteRejected, status)
293 ac.parkedRouteMu.Lock()294 ac.parkedRouteMu.Lock()
294 require.NotNil(t, ac.parkedRoute)295 require.NotNil(t, ac.parkedRoute)
295 ac.parkedRouteMu.Unlock()296 ac.parkedRouteMu.Unlock()
@@ -328,10 +329,10 @@ func TestParkedRouteLeaseExpiryClosesExactRetainedTransport(t *testing.T) {
328 func TestParkedRouteMalformedSwitchRequestsFailClosed(t *testing.T) {329 func TestParkedRouteMalformedSwitchRequestsFailClosed(t *testing.T) {
329 d, source, ac, sends, releases := newManualTabSession(t, 1)330 d, source, ac, sends, releases := newManualTabSession(t, 1)
330 defer releaseAll(releases)331 defer releaseAll(releases)
331- leaseID := ports.ParkedRouteLeaseID{1}332+ leaseID := protocol.ParkedRouteLeaseID{1}
332- for _, request := range []ports.ParkedRouteRequest{333+ for _, request := range []protocol.ParkedRouteRequest{
333- {RequestID: 1, LeaseID: leaseID, Action: ports.ParkedRouteSwitch},334+ {RequestID: 1, LeaseID: leaseID, Action: protocol.ParkedRouteSwitch},
334- {RequestID: 2, Action: ports.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}},335+ {RequestID: 2, Action: protocol.ParkedRouteSwitch, Target: &domain.RemoteSessionTarget{}},
335 {RequestID: 3, LeaseID: leaseID, Action: 99},336 {RequestID: 3, LeaseID: leaseID, Action: 99},
336 } {337 } {
337 capability := source.captureAttachmentCapability(ac, ac.transport())338 capability := source.captureAttachmentCapability(ac, ac.transport())
@@ -358,7 +359,7 @@ func TestParkedRouteResponseFailureDoesNotDrainItsOwnFrameEffect(t *testing.T) {
358 359
359 result := make(chan bool, 1)360 result := make(chan bool, 1)
360 go func() {361 go func() {
361- result <- d.respondParkedRoute(effect, ports.ParkedRouteResponse{RequestID: 1, Status: ports.ParkedRouteReady})362+ result <- d.respondParkedRoute(effect, protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})
362 }()363 }()
363 require.False(t, awaitTestValue(t, result, "parked-route send failure cleanup"))364 require.False(t, awaitTestValue(t, result, "parked-route send failure cleanup"))
364 d.attachmentCleanupWg.Wait()365 d.attachmentCleanupWg.Wait()
@@ -367,7 +368,7 @@ func TestParkedRouteResponseFailureDoesNotDrainItsOwnFrameEffect(t *testing.T) {
367 func TestParkedRouteDetachClearsLeaseAndSuspension(t *testing.T) {368 func TestParkedRouteDetachClearsLeaseAndSuspension(t *testing.T) {
368 d, source, ac, sends, releases := newManualTabSession(t, 1)369 d, source, ac, sends, releases := newManualTabSession(t, 1)
369 defer releaseAll(releases)370 defer releaseAll(releases)
370- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker371+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
371 372
372 _ = armAndPrepareParkedRoute(t, d, source, ac, sends)373 _ = armAndPrepareParkedRoute(t, d, source, ac, sends)
373 require.True(t, ac.parkedRouteOutput.Load())374 require.True(t, ac.parkedRouteOutput.Load())
M internal/usecase/daemon/picker.go +7 -6
@@ -10,6 +10,7 @@ import (
10 renderer "github.com/bnema/vev-vt"10 renderer "github.com/bnema/vev-vt"
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12 "github.com/bnema/vev/internal/ports"12 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
13 "github.com/bnema/vev/internal/usecase/layout"14 "github.com/bnema/vev/internal/usecase/layout"
14 "github.com/bnema/vev/internal/usecase/picker"15 "github.com/bnema/vev/internal/usecase/picker"
15 "github.com/bnema/vev/internal/usecase/ui"16 "github.com/bnema/vev/internal/usecase/ui"
@@ -377,14 +378,14 @@ func (d *Daemon) handlePickerInput(ac *attachedClient, data []byte, effects ...*
377 committing := (result.action == '\r' || result.action == '\n') && ok378 committing := (result.action == '\r' || result.action == '\n') && ok
378 backNavigationSent := false379 backNavigationSent := false
379 if result.exit && !committing {380 if result.exit && !committing {
380- back := ac.startupOverlay == ports.StartupOverlaySessionPicker && ac.navigationCapabilities&ports.NavigationCapabilityBack != 0381+ back := ac.startupOverlay == protocol.StartupOverlaySessionPicker && ac.navigationCapabilities&protocol.NavigationCapabilityBack != 0
381 if back {382 if back {
382 if len(effects) == 0 || effects[0] == nil {383 if len(effects) == 0 || effects[0] == nil {
383 d.invalidateRender(sess, ac, true, "picker.go")384 d.invalidateRender(sess, ac, true, "picker.go")
384 return385 return
385 }386 }
386 effect := effects[0]387 effect := effects[0]
387- if err := d.sendNavigationActionForAttachment(effect, ports.NavigationBack); err != nil {388+ if err := d.sendNavigationActionForAttachment(effect, protocol.NavigationBack); err != nil {
388 d.reportAttachmentError(sess, err)389 d.reportAttachmentError(sess, err)
389 d.invalidateRender(sess, ac, true, "picker.go")390 d.invalidateRender(sess, ac, true, "picker.go")
390 return391 return
@@ -599,7 +600,7 @@ func remotePickerPreviewSize(size domain.Size) (uint16, uint16) {
599 if width <= 0 || height <= 0 {600 if width <= 0 || height <= 0 {
600 return 0, 0601 return 0, 0
601 }602 }
602- return uint16(min(width, ports.RemotePreviewMaxWidth)), uint16(min(height, ports.RemotePreviewMaxHeight))603+ return uint16(min(width, protocol.RemotePreviewMaxWidth)), uint16(min(height, protocol.RemotePreviewMaxHeight))
603 }604 }
604 605
605 func staticRemotePickerPreview(width, height uint16, message string) picker.Preview {606 func staticRemotePickerPreview(width, height uint16, message string) picker.Preview {
@@ -887,7 +888,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
887 d.mu.Lock()888 d.mu.Lock()
888 var targetSess *session889 var targetSess *session
889 var sessionName string890 var sessionName string
890- var exactTarget *ports.ExactSessionTarget891+ var exactTarget *protocol.ExactSessionTarget
891 var matches bool892 var matches bool
892 if target.Name != "" {893 if target.Name != "" {
893 var stopped inactiveSession894 var stopped inactiveSession
@@ -895,7 +896,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
895 targetSess, stopped, stoppedTarget, matches = d.resolveNamedLifecycleTargetLocked(target)896 targetSess, stopped, stoppedTarget, matches = d.resolveNamedLifecycleTargetLocked(target)
896 if stoppedTarget && matches {897 if stoppedTarget && matches {
897 sessionName = stopped.name898 sessionName = stopped.name
898- exactTarget = &ports.ExactSessionTarget{LifecycleID: stopped.incarnation, SessionName: sessionName}899+ exactTarget = &protocol.ExactSessionTarget{LifecycleID: stopped.incarnation, SessionName: sessionName}
899 }900 }
900 } else {901 } else {
901 targetSess = d.sessions[target.Session]902 targetSess = d.sessions[target.Session]
@@ -904,7 +905,7 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
904 if matches && targetSess != nil {905 if matches && targetSess != nil {
905 targetSess.mu.Lock()906 targetSess.mu.Lock()
906 sessionName = targetSess.name907 sessionName = targetSess.name
907- exactTarget = &ports.ExactSessionTarget{LifecycleID: targetSess.incarnation, SessionName: sessionName}908+ exactTarget = &protocol.ExactSessionTarget{LifecycleID: targetSess.incarnation, SessionName: sessionName}
908 targetSess.mu.Unlock()909 targetSess.mu.Unlock()
909 }910 }
910 d.mu.Unlock()911 d.mu.Unlock()
M internal/usecase/daemon/picker_test.go +5 -4
@@ -16,6 +16,7 @@ import (
16 "github.com/bnema/vev/internal/persist"16 "github.com/bnema/vev/internal/persist"
17 "github.com/bnema/vev/internal/ports"17 "github.com/bnema/vev/internal/ports"
18 portsmocks "github.com/bnema/vev/internal/ports/mocks"18 portsmocks "github.com/bnema/vev/internal/ports/mocks"
19+ "github.com/bnema/vev/internal/protocol"
19 "github.com/bnema/vev/internal/usecase/layout"20 "github.com/bnema/vev/internal/usecase/layout"
20 "github.com/bnema/vev/internal/usecase/picker"21 "github.com/bnema/vev/internal/usecase/picker"
21 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"22 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
@@ -892,8 +893,8 @@ func TestPickerNavigationBackSendFailureKeepsPickerOpen(t *testing.T) {
892 const sendErr = "navigation back send failed"893 const sendErr = "navigation back send failed"
893 tr := &remotePickerSendErrorTransport{err: errors.New(sendErr)}894 tr := &remotePickerSendErrorTransport{err: errors.New(sendErr)}
894 ac.replaceTransport(tr)895 ac.replaceTransport(tr)
895- ac.startupOverlay = ports.StartupOverlaySessionPicker896+ ac.startupOverlay = protocol.StartupOverlaySessionPicker
896- ac.navigationCapabilities = ports.NavigationCapabilityBack897+ ac.navigationCapabilities = protocol.NavigationCapabilityBack
897 ac.overlays.pickerMu.Lock()898 ac.overlays.pickerMu.Lock()
898 ac.overlays.picker = picker.New(nil, picker.SelectionConfig{})899 ac.overlays.picker = picker.New(nil, picker.SelectionConfig{})
899 ac.overlays.pickerGeneration++900 ac.overlays.pickerGeneration++
@@ -926,8 +927,8 @@ func TestPickerNavigationBackWithoutCallerTicketKeepsPickerOpen(t *testing.T) {
926 t.Run(tc.name, func(t *testing.T) {927 t.Run(tc.name, func(t *testing.T) {
927 d, sess, ac, _, releases := newManualTabSession(t, 1)928 d, sess, ac, _, releases := newManualTabSession(t, 1)
928 defer releases[0]()929 defer releases[0]()
929- ac.startupOverlay = ports.StartupOverlaySessionPicker930+ ac.startupOverlay = protocol.StartupOverlaySessionPicker
930- ac.navigationCapabilities = ports.NavigationCapabilityBack931+ ac.navigationCapabilities = protocol.NavigationCapabilityBack
931 ac.overlays.pickerMu.Lock()932 ac.overlays.pickerMu.Lock()
932 ac.overlays.picker = picker.New([]picker.SessionView{{ID: sess.id, Name: sess.name, Tabs: []picker.TabEntry{{Name: "tab"}}, Active: 0}}, picker.SelectionConfig{})933 ac.overlays.picker = picker.New([]picker.SessionView{{ID: sess.id, Name: sess.name, Tabs: []picker.TabEntry{{Name: "tab"}}, Active: 0}}, picker.SelectionConfig{})
933 ac.overlays.pickerGeneration++934 ac.overlays.pickerGeneration++
M internal/usecase/daemon/recent_route_presentation_test.go +5 -5
@@ -3,7 +3,7 @@ package daemon
3 import (3 import (
4 "testing"4 "testing"
5 5
6- "github.com/bnema/vev/internal/ports"6+ "github.com/bnema/vev/internal/protocol"
7 "github.com/bnema/vev/internal/usecase/command"7 "github.com/bnema/vev/internal/usecase/command"
8 "github.com/bnema/vev/internal/usecase/palette"8 "github.com/bnema/vev/internal/usecase/palette"
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
@@ -76,11 +76,11 @@ func TestFormatRecentRoutePresentations(t *testing.T) {
76 }76 }
77 77
78 func TestRecentSessionHintsAndRankedRenderingShareCanonicalLabels(t *testing.T) {78 func TestRecentSessionHintsAndRankedRenderingShareCanonicalLabels(t *testing.T) {
79- snapshot := ports.RecentRouteSnapshot{79+ snapshot := protocol.RecentRouteSnapshot{
80 Generation: 1,80 Generation: 1,
81- Entries: []ports.RecentRouteEntry{81+ Entries: []protocol.RecentRouteEntry{
82- {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: ports.RouteKindLocal, Attention: true},82+ {Key: 1, Generation: 1, Target: testRouteTarget("logs", 1), Name: "logs", Kind: protocol.RouteKindLocal, Attention: true},
83- testRouteEntry(2, 1, "logs", 2, ports.RouteKindLocal),83+ testRouteEntry(2, 1, "logs", 2, protocol.RouteKindLocal),
84 },84 },
85 }85 }
86 86
M internal/usecase/daemon/recent_route_snapshot.go +10 -10
@@ -2,26 +2,26 @@ package daemon
2 2
3 import (3 import (
4 "github.com/bnema/vev/internal/domain"4 "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/ports"5+ "github.com/bnema/vev/internal/protocol"
6 )6 )
7 7
8-func activeRouteEntryForLifecycle(snapshot ports.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) (ports.RecentRouteEntry, bool) {8+func activeRouteEntryForLifecycle(snapshot protocol.RecentRouteSnapshot, lifecycle domain.SessionLifecycleID) (protocol.RecentRouteEntry, bool) {
9 active := snapshot.ActiveEntry9 active := snapshot.ActiveEntry
10- activeRef := ports.RouteRef{Key: active.Key, Generation: active.Generation}10+ activeRef := protocol.RouteRef{Key: active.Key, Generation: active.Generation}
11 if lifecycle == (domain.SessionLifecycleID{}) || active.Target.LifecycleID != lifecycle || snapshot.Active != activeRef {11 if lifecycle == (domain.SessionLifecycleID{}) || active.Target.LifecycleID != lifecycle || snapshot.Active != activeRef {
12- return ports.RecentRouteEntry{}, false12+ return protocol.RecentRouteEntry{}, false
13 }13 }
14 return active, true14 return active, true
15 }15 }
16 16
17-func recentRoutePresentationsFromSnapshot(snapshot ports.RecentRouteSnapshot) []recentRoutePresentation {17+func recentRoutePresentationsFromSnapshot(snapshot protocol.RecentRouteSnapshot) []recentRoutePresentation {
18 if snapshot.Entries == nil {18 if snapshot.Entries == nil {
19 return nil19 return nil
20 }20 }
21 out := make([]recentRoutePresentation, len(snapshot.Entries))21 out := make([]recentRoutePresentation, len(snapshot.Entries))
22 for i, entry := range snapshot.Entries {22 for i, entry := range snapshot.Entries {
23 kind := recentRouteLocal23 kind := recentRouteLocal
24- if entry.Kind == ports.RouteKindRemote {24+ if entry.Kind == protocol.RouteKindRemote {
25 kind = recentRouteRemote25 kind = recentRouteRemote
26 }26 }
27 out[i] = recentRoutePresentation{27 out[i] = recentRoutePresentation{
@@ -35,20 +35,20 @@ func recentRoutePresentationsFromSnapshot(snapshot ports.RecentRouteSnapshot) []
35 return out35 return out
36 }36 }
37 37
38-func formatRecentRouteSnapshot(snapshot ports.RecentRouteSnapshot) []recentRouteDisplay {38+func formatRecentRouteSnapshot(snapshot protocol.RecentRouteSnapshot) []recentRouteDisplay {
39 return formatRecentRoutePresentations(recentRoutePresentationsFromSnapshot(snapshot))39 return formatRecentRoutePresentations(recentRoutePresentationsFromSnapshot(snapshot))
40 }40 }
41 41
42 // formatRecentRouteSnapshotForAttachment resolves attention against this42 // formatRecentRouteSnapshotForAttachment resolves attention against this
43 // daemon's live sessions. The client retains route ordering and presentation;43 // daemon's live sessions. The client retains route ordering and presentation;
44 // it subscribes only the exact routes this daemon serves.44 // it subscribes only the exact routes this daemon serves.
45-func (d *Daemon) formatRecentRouteSnapshotForAttachment(ac *attachedClient, snapshot ports.RecentRouteSnapshot) []recentRouteDisplay {45+func (d *Daemon) formatRecentRouteSnapshotForAttachment(ac *attachedClient, snapshot protocol.RecentRouteSnapshot) []recentRouteDisplay {
46 presentations := recentRoutePresentationsFromSnapshot(snapshot)46 presentations := recentRoutePresentationsFromSnapshot(snapshot)
47 if d == nil || ac == nil {47 if d == nil || ac == nil {
48 return formatRecentRoutePresentations(presentations)48 return formatRecentRoutePresentations(presentations)
49 }49 }
50 for i, entry := range snapshot.Entries {50 for i, entry := range snapshot.Entries {
51- target, ok := ac.routeAttentionTarget(ports.RouteRef{Key: entry.Key, Generation: entry.Generation})51+ target, ok := ac.routeAttentionTarget(protocol.RouteRef{Key: entry.Key, Generation: entry.Generation})
52 if ok {52 if ok {
53 presentations[i].attention = d.routeHasAttention(target)53 presentations[i].attention = d.routeHasAttention(target)
54 }54 }
@@ -56,7 +56,7 @@ func (d *Daemon) formatRecentRouteSnapshotForAttachment(ac *attachedClient, snap
56 return formatRecentRoutePresentations(presentations)56 return formatRecentRoutePresentations(presentations)
57 }57 }
58 58
59-func (d *Daemon) routeHasAttention(target ports.ExactSessionTarget) bool {59+func (d *Daemon) routeHasAttention(target protocol.ExactSessionTarget) bool {
60 d.mu.Lock()60 d.mu.Lock()
61 sess := d.findByNameLocked(target.SessionName)61 sess := d.findByNameLocked(target.SessionName)
62 d.mu.Unlock()62 d.mu.Unlock()
M internal/usecase/daemon/recent_route_snapshot_test.go +25 -24
@@ -5,21 +5,22 @@ import (
5 5
6 "github.com/bnema/vev/internal/domain"6 "github.com/bnema/vev/internal/domain"
7 "github.com/bnema/vev/internal/ports"7 "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
8 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
9 )10 )
10 11
11 func TestAttachmentStatusUsesClientRouteSnapshot(t *testing.T) {12 func TestAttachmentStatusUsesClientRouteSnapshot(t *testing.T) {
12 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)13 d, sess, ac, _ := newManualSessionWithPTYs(t, nil)
13- ac.setRouteSnapshot(ports.RecentRouteSnapshot{14+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
14 Generation: 2,15 Generation: 2,
15- Active: ports.RouteRef{Key: 3, Generation: 2},16+ Active: protocol.RouteRef{Key: 3, Generation: 2},
16- Entries: []ports.RecentRouteEntry{17+ Entries: []protocol.RecentRouteEntry{
17- {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: ports.RouteKindRemote},18+ {Key: 2, Generation: 1, Target: testRouteTarget("logs", 2), Name: "logs", HostLabel: "user@edge", Kind: protocol.RouteKindRemote},
18- testRouteEntry(1, 1, "work", 1, ports.RouteKindLocal),19+ testRouteEntry(1, 1, "work", 1, protocol.RouteKindLocal),
19 },20 },
20 })21 })
21 22
22- state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, ports.RecentRouteSnapshot{})23+ state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
23 24
24 require.Len(t, state.mru, 2)25 require.Len(t, state.mru, 2)
25 require.Equal(t, []string{"logs@edge", "work"}, []string{state.mru[0].name, state.mru[1].name})26 require.Equal(t, []string{"logs@edge", "work"}, []string{state.mru[0].name, state.mru[1].name})
@@ -40,21 +41,21 @@ func TestAttachmentStatusResolvesSubscribedRouteAttention(t *testing.T) {
40 sess.incarnation = domain.SessionLifecycleID{9}41 sess.incarnation = domain.SessionLifecycleID{9}
41 sess.tabs[0].attention = true42 sess.tabs[0].attention = true
42 sess.mu.Unlock()43 sess.mu.Unlock()
43- ref := ports.RouteRef{Key: 2, Generation: 1}44+ ref := protocol.RouteRef{Key: 2, Generation: 1}
44- ac.setRouteSnapshot(ports.RecentRouteSnapshot{45+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
45 Generation: 2,46 Generation: 2,
46- Active: ports.RouteRef{Key: 3, Generation: 2},47+ Active: protocol.RouteRef{Key: 3, Generation: 2},
47- Entries: []ports.RecentRouteEntry{{Key: ref.Key, Generation: ref.Generation, Target: ports.ExactSessionTarget{LifecycleID: tt.targetID, SessionName: sess.name}, Name: sess.name, Kind: ports.RouteKindLocal}},48+ Entries: []protocol.RecentRouteEntry{{Key: ref.Key, Generation: ref.Generation, Target: protocol.ExactSessionTarget{LifecycleID: tt.targetID, SessionName: sess.name}, Name: sess.name, Kind: protocol.RouteKindLocal}},
48 })49 })
49- ac.setRouteAttentionSubscription(ports.RouteAttentionSubscription{Targets: []ports.RouteAttentionTarget{{50+ ac.setRouteAttentionSubscription(protocol.RouteAttentionSubscription{Targets: []protocol.RouteAttentionTarget{{
50 Ref: ref,51 Ref: ref,
51- Target: ports.ExactSessionTarget{52+ Target: protocol.ExactSessionTarget{
52 LifecycleID: tt.targetID,53 LifecycleID: tt.targetID,
53 SessionName: sess.name,54 SessionName: sess.name,
54 },55 },
55 }}})56 }}})
56 57
57- state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, ports.RecentRouteSnapshot{})58+ state := d.barStateForAttachmentPaletteHintsFor(sess, ac, "", nil, protocol.RecentRouteSnapshot{})
58 59
59 require.Len(t, state.mru, 1)60 require.Len(t, state.mru, 1)
60 require.Equal(t, tt.attention, state.mru[0].attention)61 require.Equal(t, tt.attention, state.mru[0].attention)
@@ -66,9 +67,9 @@ func TestRecentRouteSnapshotRepaintsWithoutDeferredIdentity(t *testing.T) {
66 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)67 d, sess, ac, sends := newManualSessionWithPTYs(t, nil)
67 token := sess.captureAttachmentCapability(ac, ac.transport())68 token := sess.captureAttachmentCapability(ac, ac.transport())
68 ac.installTestAttachmentCapability(token)69 ac.installTestAttachmentCapability(token)
69- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})70+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
70 71
71- payload, err := ports.MarshalRecentRouteSnapshot(ports.RecentRouteSnapshot{Generation: 2})72+ payload, err := ports.MarshalRecentRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 2})
72 require.NoError(t, err)73 require.NoError(t, err)
73 require.False(t, d.handleAttachmentClientFrame(token, ports.Frame{Type: ports.MsgRecentRouteSnapshot, Payload: payload}))74 require.False(t, d.handleAttachmentClientFrame(token, ports.Frame{Type: ports.MsgRecentRouteSnapshot, Payload: payload}))
74 75
@@ -77,8 +78,8 @@ func TestRecentRouteSnapshotRepaintsWithoutDeferredIdentity(t *testing.T) {
77 78
78 func TestAttachmentRouteSnapshotCopiesPublishedEntries(t *testing.T) {79 func TestAttachmentRouteSnapshotCopiesPublishedEntries(t *testing.T) {
79 _, _, ac, _ := newManualSessionWithPTYs(t, nil)80 _, _, ac, _ := newManualSessionWithPTYs(t, nil)
80- entries := []ports.RecentRouteEntry{testRouteEntry(1, 2, "before", 1, ports.RouteKindLocal)}81+ entries := []protocol.RecentRouteEntry{testRouteEntry(1, 2, "before", 1, protocol.RouteKindLocal)}
81- snapshot := ports.RecentRouteSnapshot{Generation: 3, Entries: entries}82+ snapshot := protocol.RecentRouteSnapshot{Generation: 3, Entries: entries}
82 ac.setRouteSnapshot(snapshot)83 ac.setRouteSnapshot(snapshot)
83 84
84 entries[0].Name = "after"85 entries[0].Name = "after"
@@ -102,16 +103,16 @@ func TestPaletteRecentRouteSelectionSendsTypedClientAction(t *testing.T) {
102 103
103 exec := paletteExec{104 exec := paletteExec{
104 d: d, sess: source, attachment: source, ac: ac,105 d: d, sess: source, attachment: source, ac: ac,
105- routeSnapshot: ports.RecentRouteSnapshot{106+ routeSnapshot: protocol.RecentRouteSnapshot{
106 Generation: 4,107 Generation: 4,
107- Entries: []ports.RecentRouteEntry{testRouteEntry(2, 3, "remote", 2, ports.RouteKindRemote)},108+ Entries: []protocol.RecentRouteEntry{testRouteEntry(2, 3, "remote", 2, protocol.RouteKindRemote)},
108 },109 },
109 effect: effect,110 effect: effect,
110 }111 }
111 require.NoError(t, exec.JumpRecentSession(1))112 require.NoError(t, exec.JumpRecentSession(1))
112 113
113 frames := transport.Sends()114 frames := transport.Sends()
114- var action ports.RouteNavigationAction115+ var action protocol.RouteNavigationAction
115 found := false116 found := false
116 for _, frame := range frames {117 for _, frame := range frames {
117 if frame.Type != ports.MsgNavigateRecentRoute {118 if frame.Type != ports.MsgNavigateRecentRoute {
@@ -123,14 +124,14 @@ func TestPaletteRecentRouteSelectionSendsTypedClientAction(t *testing.T) {
123 found = true124 found = true
124 }125 }
125 require.True(t, found)126 require.True(t, found)
126- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 3}, action)127+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 4, Key: 2, Generation: 3}, action)
127 }128 }
128 129
129 func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {130 func TestRecentRouteHintsRetainSnapshotSelectionIdentity(t *testing.T) {
130- snapshot := ports.RecentRouteSnapshot{131+ snapshot := protocol.RecentRouteSnapshot{
131 Generation: 8,132 Generation: 8,
132- Entries: []ports.RecentRouteEntry{{133+ Entries: []protocol.RecentRouteEntry{{
133- Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: ports.RouteKindRemote,134+ Key: 11, Generation: 7, Target: testRouteTarget("logs", 11), Name: "logs", HostLabel: "edge", Kind: protocol.RouteKindRemote,
134 }},135 }},
135 }136 }
136 137
M internal/usecase/daemon/remote_attach.go +12 -11
@@ -6,6 +6,7 @@ import (
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
9 )10 )
10 11
11 // routeRemoteTargetWithContext is the exact-target branch for picker handoffs.12 // routeRemoteTargetWithContext is the exact-target branch for picker handoffs.
@@ -27,8 +28,8 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello
27 return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote target requires attach or resume"}28 return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote target requires attach or resume"}
28 }29 }
29 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {30 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {
30- var protocol *protoErr31+ var protocolErr *protoErr
31- if errors.As(err, &protocol) && protocol.code == ports.ErrInternal {32+ if errors.As(err, &protocolErr) && protocolErr.code == ports.ErrInternal {
32 return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session is unavailable"}33 return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session is unavailable"}
33 }34 }
34 return nil, nil, remoteTargetError(err)35 return nil, nil, remoteTargetError(err)
@@ -102,10 +103,10 @@ func (d *Daemon) resumeRemoteInactiveSessionLocked(target domain.RemoteSessionTa
102 return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, geometry, env, &expected, validate, expected.tabNames)103 return d.createSessionLockedWithModeAndInactiveFence(target.SessionName, false, cwd, geometry, env, &expected, validate, expected.tabNames)
103 }104 }
104 105
105-func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, action ports.NavigationAction) error {106+func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, action protocol.NavigationAction) error {
106- directive := ports.NavigationDirective{Action: action}107+ directive := protocol.NavigationDirective{Action: action}
107 armed := false108 armed := false
108- if action == ports.NavigationOpenHomePicker {109+ if action == protocol.NavigationOpenHomePicker {
109 leaseID, err := d.armParkedRoute(effect)110 leaseID, err := d.armParkedRoute(effect)
110 if err != nil {111 if err != nil {
111 return err112 return err
@@ -130,7 +131,7 @@ func (d *Daemon) sendNavigationActionForAttachment(effect *attachmentEffect, act
130 return nil131 return nil
131 }132 }
132 133
133-func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action ports.RouteNavigationAction) error {134+func (d *Daemon) sendRecentRouteNavigationActionForAttachment(effect *attachmentEffect, action protocol.RouteNavigationAction) error {
134 payload, err := ports.MarshalRouteNavigationAction(action)135 payload, err := ports.MarshalRouteNavigationAction(action)
135 if err != nil {136 if err != nil {
136 return errAttachmentTransition137 return errAttachmentTransition
@@ -143,8 +144,8 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffec
143 return errAttachmentTransition144 return errAttachmentTransition
144 }145 }
145 effect.sess.mu.Lock()146 effect.sess.mu.Lock()
146- identity := ports.CommittedRouteIdentity{147+ identity := protocol.CommittedRouteIdentity{
147- Target: ports.ExactSessionTarget{LifecycleID: effect.sess.incarnation, SessionName: effect.sess.name},148+ Target: protocol.ExactSessionTarget{LifecycleID: effect.sess.incarnation, SessionName: effect.sess.name},
148 Ephemeral: effect.sess.ephemeral,149 Ephemeral: effect.sess.ephemeral,
149 }150 }
150 effect.sess.mu.Unlock()151 effect.sess.mu.Unlock()
@@ -188,9 +189,9 @@ func (d *Daemon) remoteTargetMatchesSessionLocked(sess *session, target domain.R
188 }189 }
189 190
190 func remoteTargetError(err error) error {191 func remoteTargetError(err error) error {
191- var protocol *protoErr192+ var protocolErr *protoErr
192- if errors.As(err, &protocol) {193+ if errors.As(err, &protocolErr) {
193- return protocol194+ return protocolErr
194 }195 }
195 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {196 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
196 return err197 return err
M internal/usecase/daemon/remote_attach_test.go +3 -2
@@ -8,6 +8,7 @@ import (
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 "github.com/bnema/vev/internal/usecase/picker"12 "github.com/bnema/vev/internal/usecase/picker"
12 )13 )
13 14
@@ -34,7 +35,7 @@ func TestLocalPickerOfferCarriesExactLifecycle(t *testing.T) {
34 require.Empty(t, got.Endpoint)35 require.Empty(t, got.Endpoint)
35 require.Equal(t, ports.IntentAttach, got.Intent)36 require.Equal(t, ports.IntentAttach, got.Intent)
36 require.True(t, got.SamePeer)37 require.True(t, got.SamePeer)
37- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)38+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)
38 require.Same(t, source, ac.currentAttachmentSession(), "the source remains attached until the client confirms the switch")39 require.Same(t, source, ac.currentAttachmentSession(), "the source remains attached until the client confirms the switch")
39 }40 }
40 41
@@ -66,7 +67,7 @@ func TestStoppedLocalPickerHandoffWaitsForClientClose(t *testing.T) {
66 target, err := ports.UnmarshalAttachTarget(handoff.Payload)67 target, err := ports.UnmarshalAttachTarget(handoff.Payload)
67 require.NoError(t, err)68 require.NoError(t, err)
68 require.False(t, target.SamePeer)69 require.False(t, target.SamePeer)
69- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "stopped"}, target.ExactTarget)70+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "stopped"}, target.ExactTarget)
70 require.Same(t, source, ac.currentAttachmentSession(), "the source remains attached until the client receives the handoff and closes")71 require.Same(t, source, ac.currentAttachmentSession(), "the source remains attached until the client receives the handoff and closes")
71 require.True(t, attachmentRegistered(source, ac))72 require.True(t, attachmentRegistered(source, ac))
72 }73 }
M internal/usecase/daemon/remote_picker_test.go +7 -6
@@ -17,6 +17,7 @@ import (
17 "github.com/bnema/vev/internal/domain"17 "github.com/bnema/vev/internal/domain"
18 "github.com/bnema/vev/internal/ports"18 "github.com/bnema/vev/internal/ports"
19 portsmocks "github.com/bnema/vev/internal/ports/mocks"19 portsmocks "github.com/bnema/vev/internal/ports/mocks"
20+ "github.com/bnema/vev/internal/protocol"
20 "github.com/bnema/vev/internal/usecase/picker"21 "github.com/bnema/vev/internal/usecase/picker"
21 )22 )
22 23
@@ -842,18 +843,18 @@ func TestRemotePickerResurrectsStoppedRemoteSessionWithoutTabMetadata(t *testing
842 func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {843 func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {
843 tests := []struct {844 tests := []struct {
844 name string845 name string
845- action ports.NavigationAction846+ action protocol.NavigationAction
846 }{847 }{
847- {name: "home picker", action: ports.NavigationOpenHomePicker},848+ {name: "home picker", action: protocol.NavigationOpenHomePicker},
848- {name: "back", action: ports.NavigationBack},849+ {name: "back", action: protocol.NavigationBack},
849 }850 }
850 851
851 for _, tt := range tests {852 for _, tt := range tests {
852 t.Run(tt.name, func(t *testing.T) {853 t.Run(tt.name, func(t *testing.T) {
853 d := newRemotePickerDaemon(nil)854 d := newRemotePickerDaemon(nil)
854 sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local")855 sess, ac, sends := addRemoteRefreshPickerOwner(t, d, "local")
855- if tt.action == ports.NavigationOpenHomePicker {856+ if tt.action == protocol.NavigationOpenHomePicker {
856- ac.navigationCapabilities = ports.NavigationCapabilityHomePicker857+ ac.navigationCapabilities = protocol.NavigationCapabilityHomePicker
857 }858 }
858 token := sess.captureAttachmentCapability(ac, ac.transport())859 token := sess.captureAttachmentCapability(ac, ac.transport())
859 effect, admitted := ac.beginAttachmentEffect(token)860 effect, admitted := ac.beginAttachmentEffect(token)
@@ -865,7 +866,7 @@ func TestNavigationActionHandoffSendsBoundedAction(t *testing.T) {
865 directive, err := ports.UnmarshalNavigationDirective(frame.Payload)866 directive, err := ports.UnmarshalNavigationDirective(frame.Payload)
866 require.NoError(t, err)867 require.NoError(t, err)
867 require.Equal(t, tt.action, directive.Action)868 require.Equal(t, tt.action, directive.Action)
868- if tt.action == ports.NavigationOpenHomePicker {869+ if tt.action == protocol.NavigationOpenHomePicker {
869 require.False(t, directive.LeaseID.IsZero())870 require.False(t, directive.LeaseID.IsZero())
870 } else {871 } else {
871 require.True(t, directive.LeaseID.IsZero())872 require.True(t, directive.LeaseID.IsZero())
M internal/usecase/daemon/remote_preview.go +20 -19
@@ -6,71 +6,72 @@ import (
6 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
9 )10 )
10 11
11 func (d *Daemon) handleRemotePreview(tr ports.Transport, frame ports.Frame) error {12 func (d *Daemon) handleRemotePreview(tr ports.Transport, frame ports.Frame) error {
12 defer func() { _ = tr.Close() }()13 defer func() { _ = tr.Close() }()
13 request, err := ports.UnmarshalRemotePreviewRequest(frame.Payload)14 request, err := ports.UnmarshalRemotePreviewRequest(frame.Payload)
14 if err != nil {15 if err != nil {
15- return d.sendRemotePreview(tr, ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewMalformed})16+ return d.sendRemotePreview(tr, protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
16 }17 }
17 preview, err := d.captureRemotePreview(request)18 preview, err := d.captureRemotePreview(request)
18 if err != nil {19 if err != nil {
19- status := ports.RemotePreviewUnavailable20+ status := protocol.RemotePreviewUnavailable
20 if errors.Is(err, errRemotePreviewNoSuchTarget) {21 if errors.Is(err, errRemotePreviewNoSuchTarget) {
21- status = ports.RemotePreviewNoSuchTarget22+ status = protocol.RemotePreviewNoSuchTarget
22 }23 }
23- preview = ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: status}24+ preview = protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: status}
24 }25 }
25 return d.sendRemotePreview(tr, preview)26 return d.sendRemotePreview(tr, preview)
26 }27 }
27 28
28-func (d *Daemon) sendRemotePreview(tr ports.Transport, preview ports.RemotePreview) error {29+func (d *Daemon) sendRemotePreview(tr ports.Transport, preview protocol.RemotePreview) error {
29 payload := ports.MarshalRemotePreview(preview)30 payload := ports.MarshalRemotePreview(preview)
30 if payload == nil {31 if payload == nil {
31- payload = ports.MarshalRemotePreview(ports.RemotePreview{Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewMalformed})32+ payload = ports.MarshalRemotePreview(protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewMalformed})
32 }33 }
33 return d.boundedControlSend(tr, ports.Frame{Type: ports.MsgRemotePreviewResponse, Payload: payload})34 return d.boundedControlSend(tr, ports.Frame{Type: ports.MsgRemotePreviewResponse, Payload: payload})
34 }35 }
35 36
36 var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")37 var errRemotePreviewNoSuchTarget = errors.New("daemon: remote preview target does not exist")
37 38
38-func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports.RemotePreview, error) {39+func (d *Daemon) captureRemotePreview(request protocol.RemotePreviewRequest) (protocol.RemotePreview, error) {
39 target := request.Target40 target := request.Target
40 d.mu.Lock()41 d.mu.Lock()
41 sess := d.findByNameLocked(target.SessionName)42 sess := d.findByNameLocked(target.SessionName)
42 if sess == nil || sess.incarnation != target.LifecycleID {43 if sess == nil || sess.incarnation != target.LifecycleID {
43 d.mu.Unlock()44 d.mu.Unlock()
44- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget45+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
45 }46 }
46 index, ok := remoteTargetTabIndexLocked(sess, target)47 index, ok := remoteTargetTabIndexLocked(sess, target)
47 d.mu.Unlock()48 d.mu.Unlock()
48 if !ok {49 if !ok {
49- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget50+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
50 }51 }
51 52
52 sess.mu.Lock()53 sess.mu.Lock()
53 if index < 0 || index >= len(sess.tabs) {54 if index < 0 || index >= len(sess.tabs) {
54 sess.mu.Unlock()55 sess.mu.Unlock()
55- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget56+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
56 }57 }
57 tab := sess.tabs[index]58 tab := sess.tabs[index]
58 if tab == nil || domain.TabStableID(tab.stableID) != target.LiveTabID {59 if tab == nil || domain.TabStableID(tab.stableID) != target.LiveTabID {
59 sess.mu.Unlock()60 sess.mu.Unlock()
60- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget61+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
61 }62 }
62 sess.mu.Unlock()63 sess.mu.Unlock()
63 tab.mu.Lock()64 tab.mu.Lock()
64 pane := tab.focusedPane()65 pane := tab.focusedPane()
65 if pane == nil {66 if pane == nil {
66 tab.mu.Unlock()67 tab.mu.Unlock()
67- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget68+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
68 }69 }
69 pane.mu.Lock()70 pane.mu.Lock()
70 if pane.screen == nil {71 if pane.screen == nil {
71 pane.mu.Unlock()72 pane.mu.Unlock()
72 tab.mu.Unlock()73 tab.mu.Unlock()
73- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget74+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
74 }75 }
75 source := pane.screen.Frame.Clone()76 source := pane.screen.Frame.Clone()
76 revision := pane.syncGen77 revision := pane.syncGen
@@ -83,7 +84,7 @@ func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports
83 width := min(int(request.Width), source.Width)84 width := min(int(request.Width), source.Width)
84 height := min(int(request.Height), source.Height)85 height := min(int(request.Height), source.Height)
85 if width <= 0 || height <= 0 {86 if width <= 0 || height <= 0 {
86- return ports.RemotePreview{}, errRemotePreviewNoSuchTarget87+ return protocol.RemotePreview{}, errRemotePreviewNoSuchTarget
87 }88 }
88 startY, endY := source.Height-height, source.Height89 startY, endY := source.Height-height, source.Height
89 if last := remotePreviewLastContentRow(source, width, startY, endY); last >= startY {90 if last := remotePreviewLastContentRow(source, width, startY, endY); last >= startY {
@@ -101,9 +102,9 @@ func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports
101 cells = append(cells, cell)102 cells = append(cells, cell)
102 }103 }
103 }104 }
104- preview := ports.RemotePreview{105+ preview := protocol.RemotePreview{
105- Version: ports.RemotePreviewSchemaVersion,106+ Version: protocol.RemotePreviewSchemaVersion,
106- Status: ports.RemotePreviewOK,107+ Status: protocol.RemotePreviewOK,
107 LifecycleID: target.LifecycleID,108 LifecycleID: target.LifecycleID,
108 TabID: target.LiveTabID,109 TabID: target.LiveTabID,
109 Revision: revision,110 Revision: revision,
@@ -111,10 +112,10 @@ func (d *Daemon) captureRemotePreview(request ports.RemotePreviewRequest) (ports
111 Height: uint16(height),112 Height: uint16(height),
112 Cells: cells,113 Cells: cells,
113 }114 }
114- if err := ports.ValidateRemotePreview(preview); err != nil {115+ if err := protocol.ValidateRemotePreview(preview); err != nil {
115 // A crop must not split a wide cell. Return a safe unavailable response116 // A crop must not split a wide cell. Return a safe unavailable response
116 // instead of exposing a malformed cell stream.117 // instead of exposing a malformed cell stream.
117- return ports.RemotePreview{}, err118+ return protocol.RemotePreview{}, err
118 }119 }
119 return preview, nil120 return preview, nil
120 }121 }
M internal/usecase/daemon/remote_preview_cache.go +22 -22
@@ -9,7 +9,7 @@ import (
9 "time"9 "time"
10 10
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12- "github.com/bnema/vev/internal/ports"12+ "github.com/bnema/vev/internal/protocol"
13 )13 )
14 14
15 const (15 const (
@@ -34,14 +34,14 @@ type remotePreviewCacheKey struct {
34 }34 }
35 35
36 type remotePreviewCacheEntry struct {36 type remotePreviewCacheEntry struct {
37- Preview ports.RemotePreview37+ Preview protocol.RemotePreview
38 Fetched time.Time38 Fetched time.Time
39 Used time.Time39 Used time.Time
40 }40 }
41 41
42 type remotePreviewFlight struct {42 type remotePreviewFlight struct {
43 done chan struct{}43 done chan struct{}
44- preview ports.RemotePreview44+ preview protocol.RemotePreview
45 err error45 err error
46 }46 }
47 47
@@ -53,7 +53,7 @@ type remotePreviewState struct {
53 slots chan struct{}53 slots chan struct{}
54 }54 }
55 55
56-func cloneRemotePreview(preview ports.RemotePreview) ports.RemotePreview {56+func cloneRemotePreview(preview protocol.RemotePreview) protocol.RemotePreview {
57 preview.Cells = slices.Clone(preview.Cells)57 preview.Cells = slices.Clone(preview.Cells)
58 return preview58 return preview
59 }59 }
@@ -82,24 +82,24 @@ func remotePreviewKeyFor(target domain.RemoteSessionTarget, width, height uint16
82 }82 }
83 }83 }
84 84
85-func (d *Daemon) fetchRemotePreview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {85+func (d *Daemon) fetchRemotePreview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
86 if d == nil || d.remotePreviewClient == nil {86 if d == nil || d.remotePreviewClient == nil {
87- return ports.RemotePreview{}, errors.New("remote preview client unavailable")87+ return protocol.RemotePreview{}, errors.New("remote preview client unavailable")
88 }88 }
89 if ctx == nil {89 if ctx == nil {
90 ctx = context.Background()90 ctx = context.Background()
91 }91 }
92 if err := ctx.Err(); err != nil {92 if err := ctx.Err(); err != nil {
93- return ports.RemotePreview{}, err93+ return protocol.RemotePreview{}, err
94 }94 }
95- request := ports.RemotePreviewRequest{95+ request := protocol.RemotePreviewRequest{
96- Version: ports.RemotePreviewSchemaVersion,96+ Version: protocol.RemotePreviewSchemaVersion,
97 Target: target,97 Target: target,
98 Width: width,98 Width: width,
99 Height: height,99 Height: height,
100 }100 }
101- if err := ports.ValidateRemotePreviewRequest(request); err != nil {101+ if err := protocol.ValidateRemotePreviewRequest(request); err != nil {
102- return ports.RemotePreview{}, err102+ return protocol.RemotePreview{}, err
103 }103 }
104 104
105 key := remotePreviewKeyFor(target, width, height)105 key := remotePreviewKeyFor(target, width, height)
@@ -139,7 +139,7 @@ func (d *Daemon) fetchRemotePreview(ctx context.Context, target domain.RemoteSes
139 139
140 if until, cooling := d.remotePreview.cooldowns[key]; cooling && now.Before(until) {140 if until, cooling := d.remotePreview.cooldowns[key]; cooling && now.Before(until) {
141 d.remotePreview.mu.Unlock()141 d.remotePreview.mu.Unlock()
142- return ports.RemotePreview{}, errRemotePreviewCooldown142+ return protocol.RemotePreview{}, errRemotePreviewCooldown
143 }143 }
144 if flight := d.remotePreview.flights[key]; flight != nil {144 if flight := d.remotePreview.flights[key]; flight != nil {
145 d.remotePreview.mu.Unlock()145 d.remotePreview.mu.Unlock()
@@ -160,12 +160,12 @@ func (d *Daemon) remotePreviewContext() context.Context {
160 return context.Background()160 return context.Background()
161 }161 }
162 162
163-func waitRemotePreviewFlight(ctx context.Context, flight *remotePreviewFlight) (ports.RemotePreview, error) {163+func waitRemotePreviewFlight(ctx context.Context, flight *remotePreviewFlight) (protocol.RemotePreview, error) {
164 select {164 select {
165 case <-flight.done:165 case <-flight.done:
166 return cloneRemotePreview(flight.preview), flight.err166 return cloneRemotePreview(flight.preview), flight.err
167 case <-ctx.Done():167 case <-ctx.Done():
168- return ports.RemotePreview{}, ctx.Err()168+ return protocol.RemotePreview{}, ctx.Err()
169 }169 }
170 }170 }
171 171
@@ -173,7 +173,7 @@ func waitRemotePreviewFlight(ctx context.Context, flight *remotePreviewFlight) (
173 // while still publishing the refreshed result once the same-key flight finishes.173 // while still publishing the refreshed result once the same-key flight finishes.
174 // A completed flight is read from the cache without starting a second remote174 // A completed flight is read from the cache without starting a second remote
175 // request.175 // request.
176-func (d *Daemon) awaitRemotePreviewRefresh(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {176+func (d *Daemon) awaitRemotePreviewRefresh(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
177 key := remotePreviewKeyFor(target, width, height)177 key := remotePreviewKeyFor(target, width, height)
178 d.remotePreview.mu.Lock()178 d.remotePreview.mu.Lock()
179 flight := d.remotePreview.flights[key]179 flight := d.remotePreview.flights[key]
@@ -188,20 +188,20 @@ func (d *Daemon) runRemotePreviewFlight(ctx context.Context, key remotePreviewCa
188 select {188 select {
189 case d.remotePreview.slots <- struct{}{}:189 case d.remotePreview.slots <- struct{}{}:
190 case <-ctx.Done():190 case <-ctx.Done():
191- d.finishRemotePreviewFlight(key, flight, ports.RemotePreview{}, ctx.Err())191+ d.finishRemotePreviewFlight(key, flight, protocol.RemotePreview{}, ctx.Err())
192 return192 return
193 }193 }
194- var preview ports.RemotePreview194+ var preview protocol.RemotePreview
195 var err error195 var err error
196 defer func() {196 defer func() {
197 if recovered := recover(); recovered != nil {197 if recovered := recover(); recovered != nil {
198 err = fmt.Errorf("remote preview flight panicked: %v", recovered)198 err = fmt.Errorf("remote preview flight panicked: %v", recovered)
199- preview = ports.RemotePreview{}199+ preview = protocol.RemotePreview{}
200 d.finishRemotePreviewFlight(key, flight, preview, err)200 d.finishRemotePreviewFlight(key, flight, preview, err)
201 panic(recovered)201 panic(recovered)
202 }202 }
203 if err != nil {203 if err != nil {
204- preview = ports.RemotePreview{}204+ preview = protocol.RemotePreview{}
205 }205 }
206 d.finishRemotePreviewFlight(key, flight, preview, err)206 d.finishRemotePreviewFlight(key, flight, preview, err)
207 }()207 }()
@@ -209,9 +209,9 @@ func (d *Daemon) runRemotePreviewFlight(ctx context.Context, key remotePreviewCa
209 209
210 preview, err = d.remotePreviewClient.Preview(ctx, target, width, height)210 preview, err = d.remotePreviewClient.Preview(ctx, target, width, height)
211 if err == nil {211 if err == nil {
212- if validationErr := ports.ValidateRemotePreview(preview); validationErr != nil {212+ if validationErr := protocol.ValidateRemotePreview(preview); validationErr != nil {
213 err = validationErr213 err = validationErr
214- } else if preview.Status != ports.RemotePreviewOK {214+ } else if preview.Status != protocol.RemotePreviewOK {
215 err = fmt.Errorf("remote preview returned status %d", preview.Status)215 err = fmt.Errorf("remote preview returned status %d", preview.Status)
216 } else if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {216 } else if preview.LifecycleID != target.LifecycleID || preview.TabID != target.LiveTabID {
217 err = errors.New("remote preview identity changed")217 err = errors.New("remote preview identity changed")
@@ -219,7 +219,7 @@ func (d *Daemon) runRemotePreviewFlight(ctx context.Context, key remotePreviewCa
219 }219 }
220 }220 }
221 221
222-func (d *Daemon) finishRemotePreviewFlight(key remotePreviewCacheKey, flight *remotePreviewFlight, preview ports.RemotePreview, err error) {222+func (d *Daemon) finishRemotePreviewFlight(key remotePreviewCacheKey, flight *remotePreviewFlight, preview protocol.RemotePreview, err error) {
223 now := d.clock.Now()223 now := d.clock.Now()
224 d.remotePreview.mu.Lock()224 d.remotePreview.mu.Lock()
225 for cooldownKey, until := range d.remotePreview.cooldowns {225 for cooldownKey, until := range d.remotePreview.cooldowns {
M internal/usecase/daemon/remote_preview_cache_test.go +14 -13
@@ -12,6 +12,7 @@ import (
12 renderer "github.com/bnema/vev-vt"12 renderer "github.com/bnema/vev-vt"
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
15 "github.com/bnema/vev/internal/usecase/picker"16 "github.com/bnema/vev/internal/usecase/picker"
16 )17 )
17 18
@@ -37,7 +38,7 @@ func (c *remotePreviewTestClock) Advance(delta time.Duration) {
37 type remotePreviewTestClient struct {38 type remotePreviewTestClient struct {
38 mu sync.Mutex39 mu sync.Mutex
39 calls int40 calls int
40- result ports.RemotePreview41+ result protocol.RemotePreview
41 err error42 err error
42 started chan struct{}43 started chan struct{}
43 finished chan struct{}44 finished chan struct{}
@@ -50,7 +51,7 @@ type remotePreviewTestClient struct {
50 finishedSignal bool51 finishedSignal bool
51 }52 }
52 53
53-func (c *remotePreviewTestClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (ports.RemotePreview, error) {54+func (c *remotePreviewTestClient) Preview(ctx context.Context, target domain.RemoteSessionTarget, width, height uint16) (protocol.RemotePreview, error) {
54 c.mu.Lock()55 c.mu.Lock()
55 c.calls++56 c.calls++
56 c.lastTarget = target57 c.lastTarget = target
@@ -69,7 +70,7 @@ func (c *remotePreviewTestClient) Preview(ctx context.Context, target domain.Rem
69 select {70 select {
70 case <-release:71 case <-release:
71 case <-ctx.Done():72 case <-ctx.Done():
72- return ports.RemotePreview{}, ctx.Err()73+ return protocol.RemotePreview{}, ctx.Err()
73 }74 }
74 }75 }
75 }76 }
@@ -111,13 +112,13 @@ func remotePreviewCacheTarget() domain.RemoteSessionTarget {
111 }112 }
112 }113 }
113 114
114-func remotePreviewCacheResult(target domain.RemoteSessionTarget, revision uint64) ports.RemotePreview {115+func remotePreviewCacheResult(target domain.RemoteSessionTarget, revision uint64) protocol.RemotePreview {
115 return remotePreviewCacheResultRune(target, revision, 'x')116 return remotePreviewCacheResultRune(target, revision, 'x')
116 }117 }
117 118
118-func remotePreviewCacheResultRune(target domain.RemoteSessionTarget, revision uint64, value rune) ports.RemotePreview {119+func remotePreviewCacheResultRune(target domain.RemoteSessionTarget, revision uint64, value rune) protocol.RemotePreview {
119- return ports.RemotePreview{120+ return protocol.RemotePreview{
120- Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,121+ Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewOK,
121 LifecycleID: target.LifecycleID, TabID: target.LiveTabID,122 LifecycleID: target.LifecycleID, TabID: target.LiveTabID,
122 Revision: revision, Width: 1, Height: 1,123 Revision: revision, Width: 1, Height: 1,
123 Cells: []renderer.Cell{{Rune: value, Style: renderer.DefaultStyle()}},124 Cells: []renderer.Cell{{Rune: value, Style: renderer.DefaultStyle()}},
@@ -136,7 +137,7 @@ func TestFetchRemotePreviewSingleFlightAndCopiesCache(t *testing.T) {
136 d.remotePreviewClient = client137 d.remotePreviewClient = client
137 138
138 firstDone := make(chan struct{})139 firstDone := make(chan struct{})
139- var first ports.RemotePreview140+ var first protocol.RemotePreview
140 var firstErr error141 var firstErr error
141 go func() {142 go func() {
142 first, firstErr = d.fetchRemotePreview(context.Background(), target, 1, 1)143 first, firstErr = d.fetchRemotePreview(context.Background(), target, 1, 1)
@@ -145,7 +146,7 @@ func TestFetchRemotePreviewSingleFlightAndCopiesCache(t *testing.T) {
145 <-client.started146 <-client.started
146 147
147 secondDone := make(chan struct{})148 secondDone := make(chan struct{})
148- var second ports.RemotePreview149+ var second protocol.RemotePreview
149 var secondErr error150 var secondErr error
150 go func() {151 go func() {
151 second, secondErr = d.fetchRemotePreview(context.Background(), target, 1, 1)152 second, secondErr = d.fetchRemotePreview(context.Background(), target, 1, 1)
@@ -169,12 +170,12 @@ func TestFetchRemotePreviewSingleFlightAndCopiesCache(t *testing.T) {
169 func TestFetchRemotePreviewAdapterTimeoutAppliesCooldown(t *testing.T) {170 func TestFetchRemotePreviewAdapterTimeoutAppliesCooldown(t *testing.T) {
170 clock := &remotePreviewTestClock{now: time.Unix(250, 0)}171 clock := &remotePreviewTestClock{now: time.Unix(250, 0)}
171 target := remotePreviewCacheTarget()172 target := remotePreviewCacheTarget()
172- client := &remotePreviewTestClient{err: ports.ErrRemotePreviewTimeout}173+ client := &remotePreviewTestClient{err: protocol.ErrRemotePreviewTimeout}
173 d := newTestDaemon(t, nil, clock)174 d := newTestDaemon(t, nil, clock)
174 d.remotePreviewClient = client175 d.remotePreviewClient = client
175 176
176 _, firstErr := d.fetchRemotePreview(context.Background(), target, 1, 1)177 _, firstErr := d.fetchRemotePreview(context.Background(), target, 1, 1)
177- require.ErrorIs(t, firstErr, ports.ErrRemotePreviewTimeout)178+ require.ErrorIs(t, firstErr, protocol.ErrRemotePreviewTimeout)
178 _, secondErr := d.fetchRemotePreview(context.Background(), target, 1, 1)179 _, secondErr := d.fetchRemotePreview(context.Background(), target, 1, 1)
179 require.ErrorIs(t, secondErr, errRemotePreviewCooldown)180 require.ErrorIs(t, secondErr, errRemotePreviewCooldown)
180 require.Equal(t, 1, client.Calls())181 require.Equal(t, 1, client.Calls())
@@ -482,7 +483,7 @@ func TestFetchRemotePreviewRejectsInvalidDimensionsBeforeRemoteIO(t *testing.T)
482 d := newTestDaemon(t, nil, clock)483 d := newTestDaemon(t, nil, clock)
483 d.remotePreviewClient = client484 d.remotePreviewClient = client
484 485
485- _, err := d.fetchRemotePreview(context.Background(), remotePreviewCacheTarget(), ports.RemotePreviewMaxWidth+1, 1)486+ _, err := d.fetchRemotePreview(context.Background(), remotePreviewCacheTarget(), protocol.RemotePreviewMaxWidth+1, 1)
486- require.ErrorIs(t, err, ports.ErrInvalidRemotePreviewRequest)487+ require.ErrorIs(t, err, protocol.ErrInvalidRemotePreviewRequest)
487 require.Zero(t, client.Calls())488 require.Zero(t, client.Calls())
488 }489 }
M internal/usecase/daemon/remote_preview_capture_test.go +8 -8
@@ -8,7 +8,7 @@ import (
8 vt "github.com/bnema/vev-vt"8 vt "github.com/bnema/vev-vt"
9 renderer "github.com/bnema/vev-vt/ansi"9 renderer "github.com/bnema/vev-vt/ansi"
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11- "github.com/bnema/vev/internal/ports"11+ "github.com/bnema/vev/internal/protocol"
12 )12 )
13 13
14 func TestCaptureRemotePreviewUsesBottomRowsForShorterPreview(t *testing.T) {14 func TestCaptureRemotePreviewUsesBottomRowsForShorterPreview(t *testing.T) {
@@ -28,8 +28,8 @@ func TestCaptureRemotePreviewUsesBottomRowsForShorterPreview(t *testing.T) {
28 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,28 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
29 SessionName: "work", LiveTabID: "tab-1",29 SessionName: "work", LiveTabID: "tab-1",
30 }30 }
31- preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{31+ preview, err := d.captureRemotePreview(protocol.RemotePreviewRequest{
32- Version: ports.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 2,32+ Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 2,
33 })33 })
34 require.NoError(t, err)34 require.NoError(t, err)
35 require.Equal(t, []rune{'b', 'c'}, []rune{preview.Cells[0].Rune, preview.Cells[1].Rune})35 require.Equal(t, []rune{'b', 'c'}, []rune{preview.Cells[0].Rune, preview.Cells[1].Rune})
@@ -112,8 +112,8 @@ func TestCaptureRemotePreviewCompactsBlankTail(t *testing.T) {
112 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,112 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
113 SessionName: "work", LiveTabID: "tab-1",113 SessionName: "work", LiveTabID: "tab-1",
114 }114 }
115- preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{115+ preview, err := d.captureRemotePreview(protocol.RemotePreviewRequest{
116- Version: ports.RemotePreviewSchemaVersion, Target: target, Width: test.width, Height: test.height,116+ Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: test.width, Height: test.height,
117 })117 })
118 require.NoError(t, err)118 require.NoError(t, err)
119 require.Equal(t, test.wantHeight, preview.Height)119 require.Equal(t, test.wantHeight, preview.Height)
@@ -156,13 +156,13 @@ func TestCaptureRemotePreviewDoesNotSplitWideRuneAtCropBoundary(t *testing.T) {
156 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,156 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation,
157 SessionName: "work", LiveTabID: "tab-1",157 SessionName: "work", LiveTabID: "tab-1",
158 }158 }
159- preview, err := d.captureRemotePreview(ports.RemotePreviewRequest{159+ preview, err := d.captureRemotePreview(protocol.RemotePreviewRequest{
160- Version: ports.RemotePreviewSchemaVersion, Target: target, Width: 2, Height: 1,160+ Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 2, Height: 1,
161 })161 })
162 require.NoError(t, err)162 require.NoError(t, err)
163 require.Equal(t, uint16(2), preview.Width)163 require.Equal(t, uint16(2), preview.Width)
164 require.Equal(t, rune(' '), preview.Cells[1].Rune, "a crop cannot expose a dangling wide rune")164 require.Equal(t, rune(' '), preview.Cells[1].Rune, "a crop cannot expose a dangling wide rune")
165 require.False(t, preview.Cells[1].Continuation)165 require.False(t, preview.Cells[1].Continuation)
166 require.True(t, preview.Cells[1].Style.Equal(style), "boundary replacement preserves the cell style")166 require.True(t, preview.Cells[1].Style.Equal(style), "boundary replacement preserves the cell style")
167- require.NoError(t, ports.ValidateRemotePreview(preview))167+ require.NoError(t, protocol.ValidateRemotePreview(preview))
168 }168 }
M internal/usecase/daemon/remote_route_test.go +9 -9
@@ -122,9 +122,9 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) {
122 // both success and error paths before returning.122 // both success and error paths before returning.
123 d.mu.Lock()123 d.mu.Lock()
124 _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true)124 _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true)
125- var protocol *protoErr125+ var protocolErr *protoErr
126- require.ErrorAs(t, err, &protocol)126+ require.ErrorAs(t, err, &protocolErr)
127- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)127+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
128 d.mu.Lock()128 d.mu.Lock()
129 _, retained := d.sessions[sess.id]129 _, retained := d.sessions[sess.id]
130 d.mu.Unlock()130 d.mu.Unlock()
@@ -146,9 +146,9 @@ func TestFinishRouteAttachPreservesConcurrentAttachment(t *testing.T) {
146 RemoteTarget: &missing, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,146 RemoteTarget: &missing, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
147 }, true, true)147 }, true, true)
148 148
149- var protocol *protoErr149+ var protocolErr *protoErr
150- require.ErrorAs(t, err, &protocol)150+ require.ErrorAs(t, err, &protocolErr)
151- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)151+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
152 require.Same(t, sess, winner().currentAttachmentSession())152 require.Same(t, sess, winner().currentAttachmentSession())
153 d.mu.Lock()153 d.mu.Lock()
154 require.Same(t, sess, d.sessions[sess.id])154 require.Same(t, sess, d.sessions[sess.id])
@@ -217,7 +217,7 @@ func TestRouteRemoteTargetRejectsSameNameReplacement(t *testing.T) {
217 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: old, SessionName: "work", LiveTabID: "tab-new"}217 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: old, SessionName: "work", LiveTabID: "tab-new"}
218 hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}218 hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}
219 _, _, err := d.routeWithContext(context.Background(), hello, tr)219 _, _, err := d.routeWithContext(context.Background(), hello, tr)
220- var protocol *protoErr220+ var protocolErr *protoErr
221- require.ErrorAs(t, err, &protocol)221+ require.ErrorAs(t, err, &protocolErr)
222- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)222+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
223 }223 }
M internal/usecase/daemon/remote_stopped_route_test.go +16 -16
@@ -30,9 +30,9 @@ func TestDaemonOwnedNoExactTargetRejectsNewAndEphemeral(t *testing.T) {
30 Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,30 Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
31 }31 }
32 _, _, err := d.route(hello, &closeTrackingTransport{})32 _, _, err := d.route(hello, &closeTrackingTransport{})
33- var protocol *protoErr33+ var protocolErr *protoErr
34- require.ErrorAs(t, err, &protocol)34+ require.ErrorAs(t, err, &protocolErr)
35- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)35+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
36 d.mu.Lock()36 d.mu.Lock()
37 sessions := len(d.sessions)37 sessions := len(d.sessions)
38 d.mu.Unlock()38 d.mu.Unlock()
@@ -83,9 +83,9 @@ func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
83 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,83 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
84 }, &closeTrackingTransport{})84 }, &closeTrackingTransport{})
85 85
86- var protocol *protoErr86+ var protocolErr *protoErr
87- require.ErrorAs(t, err, &protocol)87+ require.ErrorAs(t, err, &protocolErr)
88- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)88+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
89 d.mu.Lock()89 d.mu.Lock()
90 defer d.mu.Unlock()90 defer d.mu.Unlock()
91 require.Empty(t, d.sessions)91 require.Empty(t, d.sessions)
@@ -115,9 +115,9 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
115 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,115 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
116 }, &closeTrackingTransport{})116 }, &closeTrackingTransport{})
117 117
118- var protocol *protoErr118+ var protocolErr *protoErr
119- require.ErrorAs(t, err, &protocol)119+ require.ErrorAs(t, err, &protocolErr)
120- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)120+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
121 })121 })
122 }122 }
123 }123 }
@@ -138,9 +138,9 @@ func TestRouteRemoteTargetMapsConcurrentInactiveResumeToNoSuchTarget(t *testing.
138 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,138 EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,
139 }, &closeTrackingTransport{})139 }, &closeTrackingTransport{})
140 140
141- var protocol *protoErr141+ var protocolErr *protoErr
142- require.ErrorAs(t, err, &protocol)142+ require.ErrorAs(t, err, &protocolErr)
143- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)143+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
144 }144 }
145 145
146 func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {146 func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
@@ -172,8 +172,8 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
172 172
173 err := route(d, newTarget(record))173 err := route(d, newTarget(record))
174 require.ErrorIs(t, err, cause)174 require.ErrorIs(t, err, cause)
175- var protocol *protoErr175+ var protocolErr *protoErr
176- require.False(t, errors.As(err, &protocol))176+ require.False(t, errors.As(err, &protocolErr))
177 })177 })
178 178
179 t.Run("catalogue read", func(t *testing.T) {179 t.Run("catalogue read", func(t *testing.T) {
@@ -191,8 +191,8 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
191 191
192 err := route(d, newTarget(record))192 err := route(d, newTarget(record))
193 require.ErrorIs(t, err, cause)193 require.ErrorIs(t, err, cause)
194- var protocol *protoErr194+ var protocolErr *protoErr
195- require.False(t, errors.As(err, &protocol))195+ require.False(t, errors.As(err, &protocolErr))
196 })196 })
197 }197 }
198 198
M internal/usecase/daemon/render_coordinator_test.go +2 -1
@@ -14,6 +14,7 @@ import (
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16 portsmocks "github.com/bnema/vev/internal/ports/mocks"16 portsmocks "github.com/bnema/vev/internal/ports/mocks"
17+ "github.com/bnema/vev/internal/protocol"
17 "github.com/bnema/vev/internal/usecase/keys"18 "github.com/bnema/vev/internal/usecase/keys"
18 "github.com/bnema/vev/internal/usecase/layout"19 "github.com/bnema/vev/internal/usecase/layout"
19 )20 )
@@ -1164,7 +1165,7 @@ func TestProducerInvalidations(t *testing.T) {
1164 file: "client.go",1165 file: "client.go",
1165 name: "client theme application",1166 name: "client theme application",
1166 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {1167 run: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient) {
1167- d.applyTheme(sess, ac, ports.Theme{TrueColor: true})1168+ d.applyTheme(sess, ac, protocol.Theme{TrueColor: true})
1168 },1169 },
1169 },1170 },
1170 {1171 {
M internal/usecase/daemon/render_pipeline.go +3 -2
@@ -6,6 +6,7 @@ import (
6 renderer "github.com/bnema/vev-vt"6 renderer "github.com/bnema/vev-vt"
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8 "github.com/bnema/vev/internal/ports"8 "github.com/bnema/vev/internal/ports"
9+ "github.com/bnema/vev/internal/protocol"
9 "github.com/bnema/vev/internal/usecase/layout"10 "github.com/bnema/vev/internal/usecase/layout"
10 "github.com/bnema/vev/internal/usecase/notices"11 "github.com/bnema/vev/internal/usecase/notices"
11 "github.com/bnema/vev/internal/usecase/palette"12 "github.com/bnema/vev/internal/usecase/palette"
@@ -416,8 +417,8 @@ func (d *Daemon) emitFrame(entry *session, ac *attachedClient, state *capturedRe
416 }417 }
417 entry.core().mu.Lock()418 entry.core().mu.Lock()
418 _, owned := entry.core().attachments[ac]419 _, owned := entry.core().attachments[ac]
419- position := ports.RoutePosition{420+ position := protocol.RoutePosition{
420- Target: ports.ExactSessionTarget{421+ Target: protocol.ExactSessionTarget{
421 LifecycleID: entry.core().incarnation,422 LifecycleID: entry.core().incarnation,
422 SessionName: entry.core().name,423 SessionName: entry.core().name,
423 },424 },
M internal/usecase/daemon/resume_test.go +9 -9
@@ -101,9 +101,9 @@ func TestTokenlessResumeIsRejectedBeforeNameRouting(t *testing.T) {
101 d := newTestDaemon(t, nil, stubClock{})101 d := newTestDaemon(t, nil, stubClock{})
102 tr, _ := newCapturingTransport(t)102 tr, _ := newCapturingTransport(t)
103 _, _, err := d.route(helloResumeCapable(ports.IntentResume, "work", 0), tr)103 _, _, err := d.route(helloResumeCapable(ports.IntentResume, "work", 0), tr)
104- var protocol *protoErr104+ var protocolErr *protoErr
105- require.ErrorAs(t, err, &protocol)105+ require.ErrorAs(t, err, &protocolErr)
106- require.Equal(t, ports.ErrNoSuchSession, protocol.code)106+ require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)
107 d.mu.Lock()107 d.mu.Lock()
108 sessionCount := len(d.sessions)108 sessionCount := len(d.sessions)
109 d.mu.Unlock()109 d.mu.Unlock()
@@ -126,9 +126,9 @@ func TestResumeRejectsMismatchedRemoteTargetBeforeOwnershipMutation(t *testing.T
126 resume.RemoteTarget = &target126 resume.RemoteTarget = &target
127 resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned127 resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
128 _, _, err = d.route(resume, &closeTrackingTransport{})128 _, _, err = d.route(resume, &closeTrackingTransport{})
129- var protocol *protoErr129+ var protocolErr *protoErr
130- require.ErrorAs(t, err, &protocol)130+ require.ErrorAs(t, err, &protocolErr)
131- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)131+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
132 require.Same(t, oldTransport, ac.transport())132 require.Same(t, oldTransport, ac.transport())
133 d.mu.Lock()133 d.mu.Lock()
134 _, parked := d.parked[token]134 _, parked := d.parked[token]
@@ -155,9 +155,9 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
155 resume.RemoteTarget = &target155 resume.RemoteTarget = &target
156 resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned156 resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned
157 _, _, err = d.route(resume, &closeTrackingTransport{})157 _, _, err = d.route(resume, &closeTrackingTransport{})
158- var protocol *protoErr158+ var protocolErr *protoErr
159- require.ErrorAs(t, err, &protocol)159+ require.ErrorAs(t, err, &protocolErr)
160- require.Equal(t, ports.ErrNoSuchTarget, protocol.code)160+ require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)
161 d.mu.Lock()161 d.mu.Lock()
162 parked := d.parked[token]162 parked := d.parked[token]
163 claimed := parked != nil && parked.claimed163 claimed := parked != nil && parked.claimed
M internal/usecase/daemon/route_identity_test_helpers_test.go +5 -5
@@ -2,15 +2,15 @@ package daemon
2 2
3 import (3 import (
4 "github.com/bnema/vev/internal/domain"4 "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/ports"5+ "github.com/bnema/vev/internal/protocol"
6 )6 )
7 7
8-func testRouteTarget(name string, marker byte) ports.ExactSessionTarget {8+func testRouteTarget(name string, marker byte) protocol.ExactSessionTarget {
9- return ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}9+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}
10 }10 }
11 11
12-func testRouteEntry(key, generation uint64, name string, marker byte, kind ports.RouteKind) ports.RecentRouteEntry {12+func testRouteEntry(key, generation uint64, name string, marker byte, kind protocol.RouteKind) protocol.RecentRouteEntry {
13- return ports.RecentRouteEntry{13+ return protocol.RecentRouteEntry{
14 Key: key, Generation: generation, Target: testRouteTarget(name, marker),14 Key: key, Generation: generation, Target: testRouteTarget(name, marker),
15 Name: name, Kind: kind,15 Name: name, Kind: kind,
16 }16 }
M internal/usecase/daemon/route_position_test.go +5 -4
@@ -8,6 +8,7 @@ import (
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
11+ "github.com/bnema/vev/internal/protocol"
11 )12 )
12 13
13 func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {14 func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
@@ -28,7 +29,7 @@ func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
28 sess.tabs = append(sess.tabs, second)29 sess.tabs = append(sess.tabs, second)
29 sess.mu.Unlock()30 sess.mu.Unlock()
30 tr := &closeTrackingTransport{}31 tr := &closeTrackingTransport{}
31- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}32+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
32 hello := ports.Hello{33 hello := ports.Hello{
33 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,34 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
34 Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,35 Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,
@@ -78,7 +79,7 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
78 sess.tabs = append(sess.tabs, second)79 sess.tabs = append(sess.tabs, second)
79 sess.mu.Unlock()80 sess.mu.Unlock()
80 tr := &closeTrackingTransport{}81 tr := &closeTrackingTransport{}
81- target := ports.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}82+ target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
82 hello := ports.Hello{83 hello := ports.Hello{
83 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,84 Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,
84 Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,85 Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,
@@ -93,7 +94,7 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
93 sess.selectAttachmentTab(ac, "tab-2")94 sess.selectAttachmentTab(ac, "tab-2")
94 d.paint(sess, ac, false, nil)95 d.paint(sess, ac, false, nil)
95 96
96- var positions []ports.RoutePosition97+ var positions []protocol.RoutePosition
97 for _, frame := range tr.Sends() {98 for _, frame := range tr.Sends() {
98 if frame.Type != ports.MsgRoutePosition {99 if frame.Type != ports.MsgRoutePosition {
99 continue100 continue
@@ -102,7 +103,7 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
102 require.NoError(t, err)103 require.NoError(t, err)
103 positions = append(positions, position)104 positions = append(positions, position)
104 }105 }
105- require.Equal(t, []ports.RoutePosition{106+ require.Equal(t, []protocol.RoutePosition{
106 {Target: target, ActiveTabID: "tab-1"},107 {Target: target, ActiveTabID: "tab-1"},
107 {Target: target, ActiveTabID: "tab-2"},108 {Target: target, ActiveTabID: "tab-2"},
108 }, positions)109 }, positions)
M internal/usecase/daemon/same_peer_switch.go +10 -9
@@ -3,9 +3,10 @@ package daemon
3 import (3 import (
4 "github.com/bnema/vev/internal/domain"4 "github.com/bnema/vev/internal/domain"
5 "github.com/bnema/vev/internal/ports"5 "github.com/bnema/vev/internal/ports"
6+ "github.com/bnema/vev/internal/protocol"
6 )7 )
7 8
8-func (ac *attachedClient) offerSamePeerTarget(target ports.ExactSessionTarget) {9+func (ac *attachedClient) offerSamePeerTarget(target protocol.ExactSessionTarget) {
9 ac.samePeerOfferMu.Lock()10 ac.samePeerOfferMu.Lock()
10 ac.samePeerOffer = &target11 ac.samePeerOffer = &target
11 ac.samePeerOfferMu.Unlock()12 ac.samePeerOfferMu.Unlock()
@@ -20,7 +21,7 @@ func (ac *attachedClient) clearSamePeerOffer() {
20 // consumeSamePeerOffer linearizes the client confirmation with the daemon's21 // consumeSamePeerOffer linearizes the client confirmation with the daemon's
21 // endpoint-empty offer. A client cannot select an arbitrary local session by22 // endpoint-empty offer. A client cannot select an arbitrary local session by
22 // manufacturing this frame.23 // manufacturing this frame.
23-func (ac *attachedClient) consumeSamePeerOffer(target ports.ExactSessionTarget) bool {24+func (ac *attachedClient) consumeSamePeerOffer(target protocol.ExactSessionTarget) bool {
24 ac.samePeerOfferMu.Lock()25 ac.samePeerOfferMu.Lock()
25 defer ac.samePeerOfferMu.Unlock()26 defer ac.samePeerOfferMu.Unlock()
26 if ac.samePeerOffer == nil || *ac.samePeerOffer != target {27 if ac.samePeerOffer == nil || *ac.samePeerOffer != target {
@@ -33,18 +34,18 @@ func (ac *attachedClient) consumeSamePeerOffer(target ports.ExactSessionTarget)
33 // switchSamePeerForAttachment commits one client-confirmed endpoint-empty34 // switchSamePeerForAttachment commits one client-confirmed endpoint-empty
34 // target. The request never names an endpoint: its authority is the current35 // target. The request never names an endpoint: its authority is the current
35 // authenticated attachment plus the target's exact lifecycle identity.36 // authenticated attachment plus the target's exact lifecycle identity.
36-func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request ports.SamePeerSwitchRequest) {37+func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request protocol.SamePeerSwitchRequest) {
37 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {38 if err := request.Validate(); err != nil || !effect.current() || effect.sess == nil || effect.ac == nil {
38 return39 return
39 }40 }
40 if !effect.ac.consumeSamePeerOffer(request.Target) {41 if !effect.ac.consumeSamePeerOffer(request.Target) {
41- d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget)42+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
42 return43 return
43 }44 }
44 45
45 target, targetTabIndex, ok := d.samePeerTarget(request)46 target, targetTabIndex, ok := d.samePeerTarget(request)
46 if !ok || target == effect.sess {47 if !ok || target == effect.sess {
47- d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget)48+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
48 return49 return
49 }50 }
50 51
@@ -67,7 +68,7 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
67 ready: true,68 ready: true,
68 })69 })
69 if err != nil {70 if err != nil {
70- d.sendSamePeerSwitchFailure(effect, request.RequestID, ports.SamePeerSwitchStaleTarget)71+ d.sendSamePeerSwitchFailure(effect, request.RequestID, protocol.SamePeerSwitchStaleTarget)
71 return72 return
72 }73 }
73 74
@@ -83,7 +84,7 @@ func (d *Daemon) switchSamePeerForAttachment(effect *attachmentEffect, request p
83 // samePeerTarget resolves the exact target and the client-owned tab cursor84 // samePeerTarget resolves the exact target and the client-owned tab cursor
84 // under the daemon/session locks. A missing cursor deliberately falls back to85 // under the daemon/session locks. A missing cursor deliberately falls back to
85 // the target session's current default rather than leaking the source view.86 // the target session's current default rather than leaking the source view.
86-func (d *Daemon) samePeerTarget(request ports.SamePeerSwitchRequest) (*session, int, bool) {87+func (d *Daemon) samePeerTarget(request protocol.SamePeerSwitchRequest) (*session, int, bool) {
87 d.mu.Lock()88 d.mu.Lock()
88 defer d.mu.Unlock()89 defer d.mu.Unlock()
89 90
@@ -108,7 +109,7 @@ func (d *Daemon) samePeerTarget(request ports.SamePeerSwitchRequest) (*session,
108 return target, tabIndex, true109 return target, tabIndex, true
109 }110 }
110 111
111-func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID uint64, code ports.SamePeerSwitchFailureCode) {112+func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID uint64, code protocol.SamePeerSwitchFailureCode) {
112 if effect == nil || effect.ac == nil {113 if effect == nil || effect.ac == nil {
113 return114 return
114 }115 }
@@ -121,7 +122,7 @@ func (d *Daemon) sendSamePeerSwitchFailure(effect *attachmentEffect, requestID u
121 }122 }
122 defer sender.End()123 defer sender.End()
123 }124 }
124- payload, err := ports.MarshalSamePeerSwitchFailure(ports.SamePeerSwitchFailure{RequestID: requestID, Code: code})125+ payload, err := ports.MarshalSamePeerSwitchFailure(protocol.SamePeerSwitchFailure{RequestID: requestID, Code: code})
125 if err != nil {126 if err != nil {
126 return127 return
127 }128 }
M internal/usecase/daemon/same_peer_switch_test.go +13 -12
@@ -7,6 +7,7 @@ import (
7 7
8 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
9 "github.com/bnema/vev/internal/ports"9 "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
10 )11 )
11 12
12 func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {13 func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {
@@ -26,7 +27,7 @@ func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {
26 d.mu.Lock()27 d.mu.Lock()
27 d.sessions[target.id] = target28 d.sessions[target.id] = target
28 d.mu.Unlock()29 d.mu.Unlock()
29- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})30+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
30 31
31 token := beginRecentRoutePaletteEffect(t, d, source, ac)32 token := beginRecentRoutePaletteEffect(t, d, source, ac)
32 d.handleInputForAttachment(token, []byte("\x1b "))33 d.handleInputForAttachment(token, []byte("\x1b "))
@@ -35,9 +36,9 @@ func TestPaletteSessionSelectionCompletesSamePeerSwitch(t *testing.T) {
35 targetFrame := awaitFrame(t, sends, ports.MsgAttachTarget)36 targetFrame := awaitFrame(t, sends, ports.MsgAttachTarget)
36 attachTarget, err := ports.UnmarshalAttachTarget(targetFrame.Payload)37 attachTarget, err := ports.UnmarshalAttachTarget(targetFrame.Payload)
37 require.NoError(t, err)38 require.NoError(t, err)
38- require.Equal(t, &ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, attachTarget.ExactTarget)39+ require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, attachTarget.ExactTarget)
39 40
40- d.switchSamePeerForAttachment(token, ports.SamePeerSwitchRequest{41+ d.switchSamePeerForAttachment(token, protocol.SamePeerSwitchRequest{
41 RequestID: 1, Target: *attachTarget.ExactTarget,42 RequestID: 1, Target: *attachTarget.ExactTarget,
42 })43 })
43 44
@@ -68,16 +69,16 @@ func TestSamePeerSwitchTransitionsExactTargetAndPreferredTab(t *testing.T) {
68 d.mu.Lock()69 d.mu.Lock()
69 d.sessions[target.id] = target70 d.sessions[target.id] = target
70 d.mu.Unlock()71 d.mu.Unlock()
71- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})72+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
72 73
73 token := source.captureAttachmentCapability(ac, ac.transport())74 token := source.captureAttachmentCapability(ac, ac.transport())
74 effect, admitted := ac.beginAttachmentEffect(token)75 effect, admitted := ac.beginAttachmentEffect(token)
75 require.True(t, admitted)76 require.True(t, admitted)
76 defer effect.End()77 defer effect.End()
77- requestTarget := ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}78+ requestTarget := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}
78 ac.offerSamePeerTarget(requestTarget)79 ac.offerSamePeerTarget(requestTarget)
79 80
80- d.switchSamePeerForAttachment(effect, ports.SamePeerSwitchRequest{81+ d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{
81 RequestID: 1,82 RequestID: 1,
82 Target: requestTarget,83 Target: requestTarget,
83 PreferredTabID: domain.TabStableID(target.tabs[1].stableID),84 PreferredTabID: domain.TabStableID(target.tabs[1].stableID),
@@ -88,13 +89,13 @@ func TestSamePeerSwitchTransitionsExactTargetAndPreferredTab(t *testing.T) {
88 identityFrame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)89 identityFrame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)
89 identity, err := ports.UnmarshalCommittedRouteIdentity(identityFrame.Payload)90 identity, err := ports.UnmarshalCommittedRouteIdentity(identityFrame.Payload)
90 require.NoError(t, err)91 require.NoError(t, err)
91- require.Equal(t, ports.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, identity.Target)92+ require.Equal(t, protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "target"}, identity.Target)
92 }93 }
93 94
94 func TestFinishSendErrorDetachClearsSamePeerOffer(t *testing.T) {95 func TestFinishSendErrorDetachClearsSamePeerOffer(t *testing.T) {
95 d, sess, ac, _, releases := newManualTabSession(t, 1)96 d, sess, ac, _, releases := newManualTabSession(t, 1)
96 defer releaseAll(releases)97 defer releaseAll(releases)
97- ac.offerSamePeerTarget(ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "target"})98+ ac.offerSamePeerTarget(protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "target"})
98 99
99 d.finishSendErrorDetach(sess, ac, ac.transport())100 d.finishSendErrorDetach(sess, ac, ac.transport())
100 101
@@ -106,21 +107,21 @@ func TestFinishSendErrorDetachClearsSamePeerOffer(t *testing.T) {
106 func TestSamePeerSwitchRejectsStaleTargetWithoutMutation(t *testing.T) {107 func TestSamePeerSwitchRejectsStaleTargetWithoutMutation(t *testing.T) {
107 d, source, ac, sends, releases := newManualTabSession(t, 1)108 d, source, ac, sends, releases := newManualTabSession(t, 1)
108 defer releaseAll(releases)109 defer releaseAll(releases)
109- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})110+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
110 111
111 token := source.captureAttachmentCapability(ac, ac.transport())112 token := source.captureAttachmentCapability(ac, ac.transport())
112 effect, admitted := ac.beginAttachmentEffect(token)113 effect, admitted := ac.beginAttachmentEffect(token)
113 require.True(t, admitted)114 require.True(t, admitted)
114 defer effect.End()115 defer effect.End()
115 116
116- d.switchSamePeerForAttachment(effect, ports.SamePeerSwitchRequest{117+ d.switchSamePeerForAttachment(effect, protocol.SamePeerSwitchRequest{
117 RequestID: 1,118 RequestID: 1,
118- Target: ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{9}, SessionName: "missing"},119+ Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{9}, SessionName: "missing"},
119 })120 })
120 121
121 require.Same(t, source, ac.currentAttachmentSession())122 require.Same(t, source, ac.currentAttachmentSession())
122 failureFrame := awaitFrame(t, sends, ports.MsgSamePeerSwitchFailure)123 failureFrame := awaitFrame(t, sends, ports.MsgSamePeerSwitchFailure)
123 failure, err := ports.UnmarshalSamePeerSwitchFailure(failureFrame.Payload)124 failure, err := ports.UnmarshalSamePeerSwitchFailure(failureFrame.Payload)
124 require.NoError(t, err)125 require.NoError(t, err)
125- require.Equal(t, ports.SamePeerSwitchFailure{RequestID: 1, Code: ports.SamePeerSwitchStaleTarget}, failure)126+ require.Equal(t, protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchStaleTarget}, failure)
126 }127 }
M internal/usecase/daemon/session_back.go +2 -2
@@ -2,7 +2,7 @@ package daemon
2 2
3 import (3 import (
4 "github.com/bnema/vev/internal/domain"4 "github.com/bnema/vev/internal/domain"
5- "github.com/bnema/vev/internal/ports"5+ "github.com/bnema/vev/internal/protocol"
6 )6 )
7 7
8 // backSessionForAttachment delegates previous-route ownership to the client8 // backSessionForAttachment delegates previous-route ownership to the client
@@ -22,7 +22,7 @@ func (d *Daemon) backSessionForAttachment(effect *attachmentEffect) error {
22 if snapshot.Previous.Key == 0 || snapshot.Previous.Generation == 0 {22 if snapshot.Previous.Key == 0 || snapshot.Previous.Generation == 0 {
23 return nil23 return nil
24 }24 }
25- return d.sendRecentRouteNavigationActionForAttachment(effect, ports.RouteNavigationAction{25+ return d.sendRecentRouteNavigationActionForAttachment(effect, protocol.RouteNavigationAction{
26 SnapshotGeneration: snapshot.Generation,26 SnapshotGeneration: snapshot.Generation,
27 Key: snapshot.Previous.Key,27 Key: snapshot.Previous.Key,
28 Generation: snapshot.Previous.Generation,28 Generation: snapshot.Previous.Generation,
M internal/usecase/daemon/session_back_route_test.go +7 -6
@@ -4,6 +4,7 @@ import (
4 "testing"4 "testing"
5 5
6 "github.com/bnema/vev/internal/ports"6 "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
7 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
8 )9 )
9 10
@@ -18,17 +19,17 @@ func TestBackSessionUsesClientPreviousRouteAfterSnapshotPublication(t *testing.T
18 effect, admitted := ac.beginAttachmentEffect(token)19 effect, admitted := ac.beginAttachmentEffect(token)
19 require.True(t, admitted)20 require.True(t, admitted)
20 defer effect.End()21 defer effect.End()
21- ac.setRouteSnapshot(ports.RecentRouteSnapshot{22+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
22 Generation: 5,23 Generation: 5,
23- Active: ports.RouteRef{Key: 9, Generation: 5},24+ Active: protocol.RouteRef{Key: 9, Generation: 5},
24- Previous: ports.RouteRef{Key: 7, Generation: 3},25+ Previous: protocol.RouteRef{Key: 7, Generation: 3},
25- Entries: []ports.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, ports.RouteKindLocal)},26+ Entries: []protocol.RecentRouteEntry{testRouteEntry(7, 3, "previous", 7, protocol.RouteKindLocal)},
26 })27 })
27 28
28 require.NoError(t, d.backSessionForAttachment(effect))29 require.NoError(t, d.backSessionForAttachment(effect))
29 frames := transport.Sends()30 frames := transport.Sends()
30 require.NotEmpty(t, frames)31 require.NotEmpty(t, frames)
31- var action ports.RouteNavigationAction32+ var action protocol.RouteNavigationAction
32 found := false33 found := false
33 for _, frame := range frames {34 for _, frame := range frames {
34 if frame.Type != ports.MsgNavigateRecentRoute {35 if frame.Type != ports.MsgNavigateRecentRoute {
@@ -40,5 +41,5 @@ func TestBackSessionUsesClientPreviousRouteAfterSnapshotPublication(t *testing.T
40 found = true41 found = true
41 }42 }
42 require.True(t, found)43 require.True(t, found)
43- require.Equal(t, ports.RouteNavigationAction{SnapshotGeneration: 5, Key: 7, Generation: 3}, action)44+ require.Equal(t, protocol.RouteNavigationAction{SnapshotGeneration: 5, Key: 7, Generation: 3}, action)
44 }45 }
M internal/usecase/daemon/session_rename_purge_test.go +7 -6
@@ -8,6 +8,7 @@ import (
8 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
9 "github.com/bnema/vev/internal/ports"9 "github.com/bnema/vev/internal/ports"
10 portsmocks "github.com/bnema/vev/internal/ports/mocks"10 portsmocks "github.com/bnema/vev/internal/ports/mocks"
11+ "github.com/bnema/vev/internal/protocol"
11 )12 )
12 13
13 func TestRenamePublishesCommittedRouteIdentityToAttachedClients(t *testing.T) {14 func TestRenamePublishesCommittedRouteIdentityToAttachedClients(t *testing.T) {
@@ -19,14 +20,14 @@ func TestRenamePublishesCommittedRouteIdentityToAttachedClients(t *testing.T) {
19 sess.mu.Unlock()20 sess.mu.Unlock()
20 token := sess.captureAttachmentCapability(ac, ac.transport())21 token := sess.captureAttachmentCapability(ac, ac.transport())
21 ac.installTestAttachmentCapability(token)22 ac.installTestAttachmentCapability(token)
22- ac.setRouteSnapshot(ports.RecentRouteSnapshot{Generation: 1})23+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
23 24
24 require.NoError(t, d.renameSession(sess, "vps-infra"))25 require.NoError(t, d.renameSession(sess, "vps-infra"))
25 26
26 frame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)27 frame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)
27 identity, err := ports.UnmarshalCommittedRouteIdentity(frame.Payload)28 identity, err := ports.UnmarshalCommittedRouteIdentity(frame.Payload)
28 require.NoError(t, err)29 require.NoError(t, err)
29- require.Equal(t, ports.ExactSessionTarget{30+ require.Equal(t, protocol.ExactSessionTarget{
30 LifecycleID: domain.IncarnationID{1},31 LifecycleID: domain.IncarnationID{1},
31 SessionName: "vps-infra",32 SessionName: "vps-infra",
32 }, identity.Target)33 }, identity.Target)
@@ -46,11 +47,11 @@ func TestRenameDefersCommittedRouteIdentityUntilFirstRouteSnapshot(t *testing.T)
46 require.NoError(t, d.renameSession(sess, "vps-infra"))47 require.NoError(t, d.renameSession(sess, "vps-infra"))
47 require.Empty(t, sends, "the route ledger must be published before its identity can be updated")48 require.Empty(t, sends, "the route ledger must be published before its identity can be updated")
48 49
49- activeRef := ports.RouteRef{Key: 1, Generation: 1}50+ activeRef := protocol.RouteRef{Key: 1, Generation: 1}
50- snapshot := ports.RecentRouteSnapshot{51+ snapshot := protocol.RecentRouteSnapshot{
51 Generation: 1,52 Generation: 1,
52 Active: activeRef,53 Active: activeRef,
53- ActiveEntry: ports.RecentRouteEntry{Key: 1, Generation: 1, Target: testRouteTarget("0", 1), Name: "0", Kind: ports.RouteKindLocal},54+ ActiveEntry: protocol.RecentRouteEntry{Key: 1, Generation: 1, Target: testRouteTarget("0", 1), Name: "0", Kind: protocol.RouteKindLocal},
54 Home: activeRef,55 Home: activeRef,
55 }56 }
56 payload, err := ports.MarshalRecentRouteSnapshot(snapshot)57 payload, err := ports.MarshalRecentRouteSnapshot(snapshot)
@@ -60,7 +61,7 @@ func TestRenameDefersCommittedRouteIdentityUntilFirstRouteSnapshot(t *testing.T)
60 frame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)61 frame := awaitFrame(t, sends, ports.MsgCommittedRouteIdentity)
61 identity, err := ports.UnmarshalCommittedRouteIdentity(frame.Payload)62 identity, err := ports.UnmarshalCommittedRouteIdentity(frame.Payload)
62 require.NoError(t, err)63 require.NoError(t, err)
63- require.Equal(t, ports.ExactSessionTarget{64+ require.Equal(t, protocol.ExactSessionTarget{
64 LifecycleID: domain.IncarnationID{1},65 LifecycleID: domain.IncarnationID{1},
65 SessionName: "vps-infra",66 SessionName: "vps-infra",
66 }, identity.Target)67 }, identity.Target)
M internal/usecase/daemon/status.go +6 -6
@@ -8,7 +8,7 @@ import (
8 8
9 renderer "github.com/bnema/vev-vt"9 renderer "github.com/bnema/vev-vt"
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11- "github.com/bnema/vev/internal/ports"11+ "github.com/bnema/vev/internal/protocol"
12 "github.com/bnema/vev/internal/usecase/command"12 "github.com/bnema/vev/internal/usecase/command"
13 "github.com/bnema/vev/internal/usecase/palette"13 "github.com/bnema/vev/internal/usecase/palette"
14 themeui "github.com/bnema/vev/internal/usecase/theme"14 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -189,7 +189,7 @@ func (s *session) statusSegments(includeTerminalTitle bool) statusSnapshot {
189 }189 }
190 190
191 func (s *session) statusSegmentsFor(ac *attachedClient, includeTerminalTitle bool) statusSnapshot {191 func (s *session) statusSegmentsFor(ac *attachedClient, includeTerminalTitle bool) statusSnapshot {
192- var routeSnapshot ports.RecentRouteSnapshot192+ var routeSnapshot protocol.RecentRouteSnapshot
193 if ac != nil {193 if ac != nil {
194 routeSnapshot = ac.routeSnapshotCopy()194 routeSnapshot = ac.routeSnapshotCopy()
195 }195 }
@@ -199,7 +199,7 @@ func (s *session) statusSegmentsFor(ac *attachedClient, includeTerminalTitle boo
199 presentation := recentRoutePresentation{name: s.name, ephemeral: s.ephemeral}199 presentation := recentRoutePresentation{name: s.name, ephemeral: s.ephemeral}
200 if active, ok := activeRouteEntryForLifecycle(routeSnapshot, s.incarnation); ok {200 if active, ok := activeRouteEntryForLifecycle(routeSnapshot, s.incarnation); ok {
201 presentation.hostLabel = domain.RemoteDisplayOrigin(active.HostLabel)201 presentation.hostLabel = domain.RemoteDisplayOrigin(active.HostLabel)
202- if active.Kind == ports.RouteKindRemote {202+ if active.Kind == protocol.RouteKindRemote {
203 presentation.kind = recentRouteRemote203 presentation.kind = recentRouteRemote
204 }204 }
205 }205 }
@@ -231,7 +231,7 @@ func tabDisplayName(tb *tab, index int) string {
231 return strconv.Itoa(index + 1)231 return strconv.Itoa(index + 1)
232 }232 }
233 233
234-func rankedRecentForHintsWithSnapshot(hints *palette.ContextualHints, snapshot ports.RecentRouteSnapshot) []rankedRecent {234+func rankedRecentForHintsWithSnapshot(hints *palette.ContextualHints, snapshot protocol.RecentRouteSnapshot) []rankedRecent {
235 if hints == nil || hints.Kind != command.ContextHintRecentSessions {235 if hints == nil || hints.Kind != command.ContextHintRecentSessions {
236 return nil236 return nil
237 }237 }
@@ -270,10 +270,10 @@ func (d *Daemon) barStateForPaletteHints(cur *session, statusFeedback string, hi
270 // attachment. Bar scripts remain local-session-only because270 // attachment. Bar scripts remain local-session-only because
271 // their existing execution contract depends on local tabs and PTYs.271 // their existing execution contract depends on local tabs and PTYs.
272 func (d *Daemon) barStateForAttachmentPaletteHints(cur *session, statusFeedback string, hints *palette.ContextualHints) barState {272 func (d *Daemon) barStateForAttachmentPaletteHints(cur *session, statusFeedback string, hints *palette.ContextualHints) barState {
273- return d.barStateForAttachmentPaletteHintsFor(cur, nil, statusFeedback, hints, ports.RecentRouteSnapshot{})273+ return d.barStateForAttachmentPaletteHintsFor(cur, nil, statusFeedback, hints, protocol.RecentRouteSnapshot{})
274 }274 }
275 275
276-func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attachedClient, statusFeedback string, hints *palette.ContextualHints, capturedRouteSnapshot ports.RecentRouteSnapshot) barState {276+func (d *Daemon) barStateForAttachmentPaletteHintsFor(cur *session, ac *attachedClient, statusFeedback string, hints *palette.ContextualHints, capturedRouteSnapshot protocol.RecentRouteSnapshot) barState {
277 routeSnapshot := capturedRouteSnapshot277 routeSnapshot := capturedRouteSnapshot
278 if routeSnapshot.Generation == 0 && ac != nil {278 if routeSnapshot.Generation == 0 && ac != nil {
279 routeSnapshot = ac.routeSnapshotCopy()279 routeSnapshot = ac.routeSnapshotCopy()
M internal/usecase/daemon/status_test.go +21 -20
@@ -14,6 +14,7 @@ import (
14 renderer "github.com/bnema/vev-vt/ansi"14 renderer "github.com/bnema/vev-vt/ansi"
15 "github.com/bnema/vev/internal/domain"15 "github.com/bnema/vev/internal/domain"
16 "github.com/bnema/vev/internal/ports"16 "github.com/bnema/vev/internal/ports"
17+ "github.com/bnema/vev/internal/protocol"
17 "github.com/bnema/vev/internal/usecase/command"18 "github.com/bnema/vev/internal/usecase/command"
18 "github.com/bnema/vev/internal/usecase/layout"19 "github.com/bnema/vev/internal/usecase/layout"
19 "github.com/bnema/vev/internal/usecase/palette"20 "github.com/bnema/vev/internal/usecase/palette"
@@ -126,14 +127,14 @@ func TestStatusSegmentsResolvesActiveLifecyclePresentation(t *testing.T) {
126 127
127 sess.name = "vive"128 sess.name = "vive"
128 sess.incarnation = domain.SessionLifecycleID{1}129 sess.incarnation = domain.SessionLifecycleID{1}
129- ref := ports.RouteRef{Key: 1, Generation: 1}130+ ref := protocol.RouteRef{Key: 1, Generation: 1}
130- ac.setRouteSnapshot(ports.RecentRouteSnapshot{131+ ac.setRouteSnapshot(protocol.RecentRouteSnapshot{
131 Generation: 1,132 Generation: 1,
132 Active: ref,133 Active: ref,
133- ActiveEntry: ports.RecentRouteEntry{134+ ActiveEntry: protocol.RecentRouteEntry{
134 Key: ref.Key, Generation: ref.Generation,135 Key: ref.Key, Generation: ref.Generation,
135- Target: ports.ExactSessionTarget{LifecycleID: tt.routeLifecycle, SessionName: sess.name},136+ Target: protocol.ExactSessionTarget{LifecycleID: tt.routeLifecycle, SessionName: sess.name},
136- Name: "vive", HostLabel: "user@arch", Kind: ports.RouteKindRemote,137+ Name: "vive", HostLabel: "user@arch", Kind: protocol.RouteKindRemote,
137 },138 },
138 })139 })
139 140
@@ -466,7 +467,7 @@ func TestStatusApplyThemeStoresClientAndPropagatesScreens(t *testing.T) {
466 d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)467 d, sess, ac, _ := newManualSessionWithPTYs(t, p1, p2)
467 defer releasePTY1()468 defer releasePTY1()
468 defer releasePTY2()469 defer releasePTY2()
469- msg := ports.Theme{HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, SchemeKnown: true, Light: true}470+ msg := protocol.Theme{HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3}, HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6}, SchemeKnown: true, Light: true}
470 471
471 d.applyTheme(sess, ac, msg)472 d.applyTheme(sess, ac, msg)
472 473
@@ -484,7 +485,7 @@ func TestApplyThemePropagatesToFloatingPane(t *testing.T) {
484 tb := testAttachmentTab(sess)485 tb := testAttachmentTab(sess)
485 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})486 floating := newPane("floating", nil, domain.Size{Cols: 20, Rows: 5})
486 installTestFloating(tb, floating, true)487 installTestFloating(tb, floating, true)
487- clientTheme := ports.Theme{488+ clientTheme := protocol.Theme{
488 HasForeground: true,489 HasForeground: true,
489 Foreground: renderer.RGB{R: 1, G: 2, B: 3},490 Foreground: renderer.RGB{R: 1, G: 2, B: 3},
490 HasBackground: true,491 HasBackground: true,
@@ -520,7 +521,7 @@ func TestApplyThemeForcedBuiltinThemePropagatesToChromeAndPanes(t *testing.T) {
520 defer releasePTY2()521 defer releasePTY2()
521 d.ApplyConfig(domain.Config{Theme: tc.mode})522 d.ApplyConfig(domain.Config{Theme: tc.mode})
522 523
523- d.applyTheme(sess, ac, ports.Theme{524+ d.applyTheme(sess, ac, protocol.Theme{
524 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},525 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
525 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},526 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
526 TrueColor: true, SchemeKnown: true, Light: !tc.want.Light,527 TrueColor: true, SchemeKnown: true, Light: !tc.want.Light,
@@ -575,7 +576,7 @@ func TestAutoThemeDetachClearsPaneColorScheme(t *testing.T) {
575 d, sess, ac, _ := newManualSessionWithPTYs(t, p)576 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
576 defer releasePTY()577 defer releasePTY()
577 d.ApplyConfig(domain.Config{Theme: domain.ThemeAuto})578 d.ApplyConfig(domain.Config{Theme: domain.ThemeAuto})
578- d.applyTheme(sess, ac, ports.Theme{579+ d.applyTheme(sess, ac, protocol.Theme{
579 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},580 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
580 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},581 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
581 TrueColor: true, SchemeKnown: true, Light: true,582 TrueColor: true, SchemeKnown: true, Light: true,
@@ -592,7 +593,7 @@ func TestAttachClientClearsStaleColorSchemeOnReplacement(t *testing.T) {
592 d, sess, ac, _ := newManualSessionWithPTYs(t, p)593 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
593 defer releasePTY()594 defer releasePTY()
594 d.ApplyConfig(domain.Config{Theme: domain.ThemeAuto})595 d.ApplyConfig(domain.Config{Theme: domain.ThemeAuto})
595- d.applyTheme(sess, ac, ports.Theme{596+ d.applyTheme(sess, ac, protocol.Theme{
596 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},597 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
597 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},598 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
598 TrueColor: true, SchemeKnown: true, Light: true,599 TrueColor: true, SchemeKnown: true, Light: true,
@@ -611,7 +612,7 @@ func TestForcedThemeDetachPreservesBuiltinOnPanes(t *testing.T) {
611 d, sess, ac, _ := newManualSessionWithPTYs(t, p)612 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
612 defer releasePTY()613 defer releasePTY()
613 d.ApplyConfig(domain.Config{Theme: domain.ThemeDark})614 d.ApplyConfig(domain.Config{Theme: domain.ThemeDark})
614- d.applyTheme(sess, ac, ports.Theme{615+ d.applyTheme(sess, ac, protocol.Theme{
615 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},616 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
616 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},617 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
617 TrueColor: true, SchemeKnown: true, Light: true,618 TrueColor: true, SchemeKnown: true, Light: true,
@@ -628,12 +629,12 @@ func TestApplyThemeAutoUnknownDoesNotClobberPaneColorScheme(t *testing.T) {
628 d, sess, ac, _ := newManualSessionWithPTYs(t, p)629 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
629 defer releasePTY()630 defer releasePTY()
630 631
631- d.applyTheme(sess, ac, ports.Theme{632+ d.applyTheme(sess, ac, protocol.Theme{
632 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},633 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
633 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},634 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
634 TrueColor: true, SchemeKnown: true, Light: true,635 TrueColor: true, SchemeKnown: true, Light: true,
635 })636 })
636- d.applyTheme(sess, ac, ports.Theme{637+ d.applyTheme(sess, ac, protocol.Theme{
637 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},638 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},
638 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},639 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},
639 TrueColor: true,640 TrueColor: true,
@@ -703,7 +704,7 @@ func TestAttachClientClearsStaleScreenDefaultColors(t *testing.T) {
703 p, release := newBlockingPTY(t)704 p, release := newBlockingPTY(t)
704 d, sess, ac, _ := newManualSessionWithPTYs(t, p)705 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
705 defer release()706 defer release()
706- d.applyTheme(sess, ac, ports.Theme{707+ d.applyTheme(sess, ac, protocol.Theme{
707 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},708 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
708 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},709 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
709 })710 })
@@ -723,7 +724,7 @@ func TestClientGoneResetsScreenDefaultColors(t *testing.T) {
723 p, release := newBlockingPTY(t)724 p, release := newBlockingPTY(t)
724 d, sess, ac, _ := newManualSessionWithPTYs(t, p)725 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
725 defer release()726 defer release()
726- d.applyTheme(sess, ac, ports.Theme{727+ d.applyTheme(sess, ac, protocol.Theme{
727 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},728 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
728 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},729 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
729 })730 })
@@ -747,7 +748,7 @@ func TestDetachOnSendErrorResetsScreenDefaultColors(t *testing.T) {
747 p, release := newBlockingPTY(t)748 p, release := newBlockingPTY(t)
748 d, sess, ac, _ := newManualSessionWithPTYs(t, p)749 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
749 defer release()750 defer release()
750- d.applyTheme(sess, ac, ports.Theme{751+ d.applyTheme(sess, ac, protocol.Theme{
751 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},752 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},
752 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},753 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},
753 })754 })
@@ -766,7 +767,7 @@ func TestDetachOnSendErrorParkPreservesScreenDefaultColors(t *testing.T) {
766 d, sess, ac, _ := newManualSessionWithPTYs(t, p)767 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
767 defer release()768 defer release()
768 ac.resumeCapable = true769 ac.resumeCapable = true
769- d.applyTheme(sess, ac, ports.Theme{770+ d.applyTheme(sess, ac, protocol.Theme{
770 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},771 HasForeground: true, Foreground: renderer.RGB{R: 7, G: 8, B: 9},
771 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},772 HasBackground: true, Background: renderer.RGB{R: 10, G: 11, B: 12},
772 })773 })
@@ -988,9 +989,9 @@ func TestBarStateForContextualRecentUsesClientSnapshot(t *testing.T) {
988 d, sess, ac, _ := newManualSessionWithPTYs(t, p)989 d, sess, ac, _ := newManualSessionWithPTYs(t, p)
989 defer releasePTY()990 defer releasePTY()
990 991
991- snapshot := ports.RecentRouteSnapshot{992+ snapshot := protocol.RecentRouteSnapshot{
992 Generation: 1,993 Generation: 1,
993- Entries: []ports.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: ports.RouteKindLocal, Attention: true}},994+ Entries: []protocol.RecentRouteEntry{{Key: 2, Generation: 1, Target: testRouteTarget("captured", 2), Name: "captured", Kind: protocol.RouteKindLocal, Attention: true}},
994 }995 }
995 hints := palette.ContextualHints{996 hints := palette.ContextualHints{
996 Kind: command.ContextHintRecentSessions,997 Kind: command.ContextHintRecentSessions,
@@ -1444,7 +1445,7 @@ func TestStatusCoalescesCreateSwitchAndResize(t *testing.T) {
1444 frameInput([]byte("\x1b ")),1445 frameInput([]byte("\x1b ")),
1445 frameInput([]byte("CNT\r")),1446 frameInput([]byte("CNT\r")),
1446 frameInput([]byte("\x1b1")),1447 frameInput([]byte("\x1b1")),
1447- ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(ports.Resize{Size: domain.Size{Cols: 22, Rows: 6}})},1448+ ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(protocol.Resize{Size: domain.Size{Cols: 22, Rows: 6}})},
1448 )1449 )
1449 1450
1450 var hg sync.WaitGroup1451 var hg sync.WaitGroup
M internal/usecase/daemon/theme_cache_test.go +2 -2
@@ -7,7 +7,7 @@ import (
7 7
8 renderer "github.com/bnema/vev-vt"8 renderer "github.com/bnema/vev-vt"
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"10+ "github.com/bnema/vev/internal/protocol"
11 "github.com/bnema/vev/internal/usecase/layout"11 "github.com/bnema/vev/internal/usecase/layout"
12 themeui "github.com/bnema/vev/internal/usecase/theme"12 themeui "github.com/bnema/vev/internal/usecase/theme"
13 )13 )
@@ -26,7 +26,7 @@ func cacheTheme() themeui.Theme {
26 func TestThemeAccentHotReloadRebuildsAppliedSnapshot(t *testing.T) {26 func TestThemeAccentHotReloadRebuildsAppliedSnapshot(t *testing.T) {
27 d, sess, ac, _ := newManualSessionWithPTYs(t)27 d, sess, ac, _ := newManualSessionWithPTYs(t)
28 raw := cacheTheme()28 raw := cacheTheme()
29- d.applyTheme(sess, ac, ports.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, TrueColor: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown})29+ d.applyTheme(sess, ac, protocol.Theme{Foreground: raw.Foreground, Background: raw.Background, HasForeground: true, HasBackground: true, TrueColor: true, Palette: raw.Palette, PaletteKnown: raw.PaletteKnown})
30 before := ac.getAppliedTheme()30 before := ac.getAppliedTheme()
31 31
32 cfg := domain.Defaults()32 cfg := domain.Defaults()
M internal/usecase/daemon/theme_integration_test.go +3 -3
@@ -7,12 +7,12 @@ import (
7 7
8 renderer "github.com/bnema/vev-vt/ansi"8 renderer "github.com/bnema/vev-vt/ansi"
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"10+ "github.com/bnema/vev/internal/protocol"
11 themeui "github.com/bnema/vev/internal/usecase/theme"11 themeui "github.com/bnema/vev/internal/usecase/theme"
12 )12 )
13 13
14-func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) ports.Theme {14+func lifecycleTheme(foreground, background renderer.RGB, palette map[uint8]renderer.RGB) protocol.Theme {
15- out := ports.Theme{15+ out := protocol.Theme{
16 Foreground: foreground, Background: background,16 Foreground: foreground, Background: background,
17 HasForeground: true, HasBackground: true, TrueColor: true,17 HasForeground: true, HasBackground: true, TrueColor: true,
18 SchemeKnown: true,18 SchemeKnown: true,
M internal/usecase/daemon/wire.go +6 -5
@@ -4,14 +4,15 @@ package daemon
4 import (4 import (
5 "github.com/bnema/vev/internal/domain"5 "github.com/bnema/vev/internal/domain"
6 "github.com/bnema/vev/internal/ports"6 "github.com/bnema/vev/internal/ports"
7+ "github.com/bnema/vev/internal/protocol"
7 )8 )
8 9
9 func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {10 func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
10 s.mu.Lock()11 s.mu.Lock()
11- var identity *ports.CommittedRouteIdentity12+ var identity *protocol.CommittedRouteIdentity
12 if s.incarnation != (domain.SessionLifecycleID{}) {13 if s.incarnation != (domain.SessionLifecycleID{}) {
13- identity = &ports.CommittedRouteIdentity{14+ identity = &protocol.CommittedRouteIdentity{
14- Target: ports.ExactSessionTarget{LifecycleID: s.incarnation, SessionName: s.name},15+ Target: protocol.ExactSessionTarget{LifecycleID: s.incarnation, SessionName: s.name},
15 Ephemeral: s.ephemeral,16 Ephemeral: s.ephemeral,
16 }17 }
17 }18 }
@@ -26,7 +27,7 @@ func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
26 w.ResumeToken = resumeToken27 w.ResumeToken = resumeToken
27 payload := ports.MarshalWelcome(w)28 payload := ports.MarshalWelcome(w)
28 if payload == nil {29 if payload == nil {
29- return ports.Frame{}, ports.ErrInvalidRouteWire30+ return ports.Frame{}, protocol.ErrInvalidRouteWire
30 }31 }
31 return ports.Frame{Type: ports.MsgWelcome, Payload: payload}, nil32 return ports.Frame{Type: ports.MsgWelcome, Payload: payload}, nil
32 }33 }
@@ -36,7 +37,7 @@ func frameError(code uint16, text string) ports.Frame {
36 }37 }
37 38
38 func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64) (ports.Frame, error) {39 func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64) (ports.Frame, error) {
39- payload, err := ports.MarshalOutput(ports.Output{40+ payload, err := ports.MarshalOutput(protocol.Output{
40 Epoch: 1, Base: baseState, New: state, Echo: echoAck,41 Epoch: 1, Base: baseState, New: state, Echo: echoAck,
41 Size: domain.Size{Cols: 1, Rows: 1}, Full: state != 0 && baseState == 0, Data: b,42 Size: domain.Size{Cols: 1, Rows: 1}, Full: state != 0 && baseState == 0, Data: b,
42 })43 })
M internal/usecase/daemon/wire_test_helpers_test.go +7 -4
@@ -1,8 +1,11 @@
1 package daemon1 package daemon
2 2
3-import "github.com/bnema/vev/internal/ports"3+import (
4+ "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
6+)
4 7
5-func mustMarshalOutput(m ports.Output) []byte {8+func mustMarshalOutput(m protocol.Output) []byte {
6 payload, err := ports.MarshalOutput(m)9 payload, err := ports.MarshalOutput(m)
7 if err != nil {10 if err != nil {
8 panic(err)11 panic(err)
@@ -10,7 +13,7 @@ func mustMarshalOutput(m ports.Output) []byte {
10 return payload13 return payload
11 }14 }
12 15
13-func mustMarshalAck(m ports.Ack) []byte {16+func mustMarshalAck(m protocol.Ack) []byte {
14 payload, err := ports.MarshalAck(m)17 payload, err := ports.MarshalAck(m)
15 if err != nil {18 if err != nil {
16 panic(err)19 panic(err)
@@ -18,7 +21,7 @@ func mustMarshalAck(m ports.Ack) []byte {
18 return payload21 return payload
19 }22 }
20 23
21-func mustMarshalResize(m ports.Resize) []byte {24+func mustMarshalResize(m protocol.Resize) []byte {
22 payload, err := ports.MarshalResize(m)25 payload, err := ports.MarshalResize(m)
23 if err != nil {26 if err != nil {
24 panic(err)27 panic(err)
M internal/usecase/palette/fuzzy_test.go +3 -3
@@ -4,7 +4,7 @@ import (
4 "testing"4 "testing"
5 "time"5 "time"
6 6
7- "github.com/bnema/vev/internal/ports"7+ "github.com/bnema/vev/internal/protocol"
8 "github.com/bnema/vev/internal/usecase/command"8 "github.com/bnema/vev/internal/usecase/command"
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
10 )10 )
@@ -87,7 +87,7 @@ func TestFuzzyOrdersMixedResults(t *testing.T) {
87 wantText []string87 wantText []string
88 wantKinds []ResultKind88 wantKinds []ResultKind
89 wantPositions [][]int89 wantPositions [][]int
90- wantActiveTargets map[int]ports.ExactSessionTarget90+ wantActiveTargets map[int]protocol.ExactSessionTarget
91 }{91 }{
92 {92 {
93 name: "command shortcode precedes sessions and description matches",93 name: "command shortcode precedes sessions and description matches",
@@ -128,7 +128,7 @@ func TestFuzzyOrdersMixedResults(t *testing.T) {
128 wantText: []string{"AX", "Resume session aAlpha", "Resume session aBravo", "Switch to session aEcho", "Switch to session aEcho", "Switch to session aZulu"},128 wantText: []string{"AX", "Resume session aAlpha", "Resume session aBravo", "Switch to session aEcho", "Switch to session aEcho", "Switch to session aZulu"},
129 wantKinds: []ResultKind{ResultKindCommand, ResultKindStoppedSession, ResultKindStoppedSession, ResultKindActiveSession, ResultKindActiveSession, ResultKindActiveSession},129 wantKinds: []ResultKind{ResultKindCommand, ResultKindStoppedSession, ResultKindStoppedSession, ResultKindActiveSession, ResultKindActiveSession, ResultKindActiveSession},
130 wantPositions: [][]int{{0}, {15}, {15}, {18}, {18}, {18}},130 wantPositions: [][]int{{0}, {15}, {15}, {18}, {18}, {18}},
131- wantActiveTargets: map[int]ports.ExactSessionTarget{131+ wantActiveTargets: map[int]protocol.ExactSessionTarget{
132 3: testExactTarget("aEcho", 4), 4: testExactTarget("aEcho", 5), 5: testExactTarget("aZulu", 3),132 3: testExactTarget("aEcho", 4), 4: testExactTarget("aEcho", 5), 5: testExactTarget("aZulu", 3),
133 },133 },
134 },134 },
M internal/usecase/palette/result.go +11 -11
@@ -4,7 +4,7 @@ import (
4 "time"4 "time"
5 5
6 "github.com/bnema/vev/internal/domain"6 "github.com/bnema/vev/internal/domain"
7- "github.com/bnema/vev/internal/ports"7+ "github.com/bnema/vev/internal/protocol"
8 "github.com/bnema/vev/internal/usecase/command"8 "github.com/bnema/vev/internal/usecase/command"
9 )9 )
10 10
@@ -32,7 +32,7 @@ type Result struct {
32 type sessionPayload struct {32 type sessionPayload struct {
33 display string33 display string
34 createdAt time.Time34 createdAt time.Time
35- target ports.ExactSessionTarget35+ target protocol.ExactSessionTarget
36 }36 }
37 37
38 type remoteSessionPayload struct {38 type remoteSessionPayload struct {
@@ -43,7 +43,7 @@ type remoteSessionPayload struct {
43 43
44 type routePayload struct {44 type routePayload struct {
45 label string45 label string
46- action ports.RouteNavigationAction46+ action protocol.RouteNavigationAction
47 }47 }
48 48
49 // NewCommandResult creates a static command palette target.49 // NewCommandResult creates a static command palette target.
@@ -52,13 +52,13 @@ func NewCommandResult(cmd command.Command) Result {
52 }52 }
53 53
54 // NewActiveSessionResult creates an immutable active named-session target.54 // NewActiveSessionResult creates an immutable active named-session target.
55-func NewActiveSessionResult(target ports.ExactSessionTarget, createdAt time.Time) Result {55+func NewActiveSessionResult(target protocol.ExactSessionTarget, createdAt time.Time) Result {
56 return NewActiveSessionResultWithDisplayOrigin(target, createdAt, "")56 return NewActiveSessionResultWithDisplayOrigin(target, createdAt, "")
57 }57 }
58 58
59 // NewActiveSessionResultWithDisplayOrigin creates an active named-session target59 // NewActiveSessionResultWithDisplayOrigin creates an active named-session target
60 // qualified for presentation through a remote attachment origin.60 // qualified for presentation through a remote attachment origin.
61-func NewActiveSessionResultWithDisplayOrigin(target ports.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {61+func NewActiveSessionResultWithDisplayOrigin(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
62 return Result{62 return Result{
63 kind: ResultKindActiveSession,63 kind: ResultKindActiveSession,
64 session: newSessionPayload(target, createdAt, displayOrigin),64 session: newSessionPayload(target, createdAt, displayOrigin),
@@ -66,20 +66,20 @@ func NewActiveSessionResultWithDisplayOrigin(target ports.ExactSessionTarget, cr
66 }66 }
67 67
68 // NewStoppedSessionResult creates an immutable stopped named-session target.68 // NewStoppedSessionResult creates an immutable stopped named-session target.
69-func NewStoppedSessionResult(target ports.ExactSessionTarget, createdAt time.Time) Result {69+func NewStoppedSessionResult(target protocol.ExactSessionTarget, createdAt time.Time) Result {
70 return NewStoppedSessionResultWithDisplayOrigin(target, createdAt, "")70 return NewStoppedSessionResultWithDisplayOrigin(target, createdAt, "")
71 }71 }
72 72
73 // NewStoppedSessionResultWithDisplayOrigin creates a stopped named-session target73 // NewStoppedSessionResultWithDisplayOrigin creates a stopped named-session target
74 // qualified for presentation through a remote attachment origin.74 // qualified for presentation through a remote attachment origin.
75-func NewStoppedSessionResultWithDisplayOrigin(target ports.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {75+func NewStoppedSessionResultWithDisplayOrigin(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) Result {
76 return Result{76 return Result{
77 kind: ResultKindStoppedSession,77 kind: ResultKindStoppedSession,
78 session: newSessionPayload(target, createdAt, displayOrigin),78 session: newSessionPayload(target, createdAt, displayOrigin),
79 }79 }
80 }80 }
81 81
82-func newSessionPayload(target ports.ExactSessionTarget, createdAt time.Time, displayOrigin string) sessionPayload {82+func newSessionPayload(target protocol.ExactSessionTarget, createdAt time.Time, displayOrigin string) sessionPayload {
83 display := target.SessionName83 display := target.SessionName
84 if displayOrigin != "" {84 if displayOrigin != "" {
85 display = domain.RemoteSessionDisplay(target.SessionName, displayOrigin)85 display = domain.RemoteSessionDisplay(target.SessionName, displayOrigin)
@@ -98,7 +98,7 @@ func NewRemoteSessionResult(key domain.RemoteSessionKey, target domain.RemoteSes
98 }98 }
99 99
100 // NewRecentRouteResult creates an immutable client-ledger route target.100 // NewRecentRouteResult creates an immutable client-ledger route target.
101-func NewRecentRouteResult(label string, action ports.RouteNavigationAction) Result {101+func NewRecentRouteResult(label string, action protocol.RouteNavigationAction) Result {
102 return Result{kind: ResultKindRecentRoute, route: routePayload{label: label, action: action}}102 return Result{kind: ResultKindRecentRoute, route: routePayload{label: label, action: action}}
103 }103 }
104 104
@@ -163,7 +163,7 @@ func (r Result) SessionCreatedAt() (time.Time, bool) {
163 163
164 // SessionTarget returns the exact lifecycle UUID and name for local active and164 // SessionTarget returns the exact lifecycle UUID and name for local active and
165 // stopped session results.165 // stopped session results.
166-func (r Result) SessionTarget() (ports.ExactSessionTarget, bool) {166+func (r Result) SessionTarget() (protocol.ExactSessionTarget, bool) {
167 return r.session.target, r.kind == ResultKindActiveSession || r.kind == ResultKindStoppedSession167 return r.session.target, r.kind == ResultKindActiveSession || r.kind == ResultKindStoppedSession
168 }168 }
169 169
@@ -187,7 +187,7 @@ func (r Result) RemoteSessionUnavailableReason() (string, bool) {
187 187
188 // RouteNavigationAction returns the exact client-ledger target only for a188 // RouteNavigationAction returns the exact client-ledger target only for a
189 // recent-route result.189 // recent-route result.
190-func (r Result) RouteNavigationAction() (ports.RouteNavigationAction, bool) {190+func (r Result) RouteNavigationAction() (protocol.RouteNavigationAction, bool) {
191 return r.route.action, r.kind == ResultKindRecentRoute191 return r.route.action, r.kind == ResultKindRecentRoute
192 }192 }
193 193
M internal/usecase/palette/result_test.go +4 -4
@@ -5,13 +5,13 @@ import (
5 "time"5 "time"
6 6
7 "github.com/bnema/vev/internal/domain"7 "github.com/bnema/vev/internal/domain"
8- "github.com/bnema/vev/internal/ports"8+ "github.com/bnema/vev/internal/protocol"
9 "github.com/bnema/vev/internal/usecase/command"9 "github.com/bnema/vev/internal/usecase/command"
10 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
11 )11 )
12 12
13-func testExactTarget(name string, marker byte) ports.ExactSessionTarget {13+func testExactTarget(name string, marker byte) protocol.ExactSessionTarget {
14- return ports.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}14+ return protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{marker}, SessionName: name}
15 }15 }
16 16
17 func TestResultKindsAndSessionLifecycleTargets(t *testing.T) {17 func TestResultKindsAndSessionLifecycleTargets(t *testing.T) {
@@ -73,7 +73,7 @@ func TestResultKindsAndSessionLifecycleTargets(t *testing.T) {
73 }73 }
74 74
75 func TestRecentRouteResultCarriesExactNavigationAction(t *testing.T) {75 func TestRecentRouteResultCarriesExactNavigationAction(t *testing.T) {
76- action := ports.RouteNavigationAction{SnapshotGeneration: 4, Key: 7, Generation: 3}76+ action := protocol.RouteNavigationAction{SnapshotGeneration: 4, Key: 7, Generation: 3}
77 result := NewRecentRouteResult("logs@edge", action)77 result := NewRecentRouteResult("logs@edge", action)
78 78
79 require.Equal(t, ResultKindRecentRoute, result.Kind())79 require.Equal(t, ResultKindRecentRoute, result.Kind())
M scripts/remote-picker-harness/main.go +3 -2
@@ -20,6 +20,7 @@ import (
20 "github.com/bnema/vev/internal/adapters/sshstdio"20 "github.com/bnema/vev/internal/adapters/sshstdio"
21 "github.com/bnema/vev/internal/domain"21 "github.com/bnema/vev/internal/domain"
22 "github.com/bnema/vev/internal/ports"22 "github.com/bnema/vev/internal/ports"
23+ "github.com/bnema/vev/internal/protocol"
23 )24 )
24 25
25 const (26 const (
@@ -660,7 +661,7 @@ func attachAndCheck(ctx context.Context, tr *harnessTransport, hello ports.Hello
660 return fmt.Errorf("welcome frame type %d", frame.Type)661 return fmt.Errorf("welcome frame type %d", frame.Type)
661 }662 }
662 tr.probe.recordControl(probeEventWelcome)663 tr.probe.recordControl(probeEventWelcome)
663- if err := tr.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(ports.Theme{TrueColor: hello.TrueColor, SchemeKnown: true})}); err != nil {664+ if err := tr.Send(ports.Frame{Type: ports.MsgTheme, Payload: ports.MarshalTheme(protocol.Theme{TrueColor: hello.TrueColor, SchemeKnown: true})}); err != nil {
664 return fmt.Errorf("send initial theme: %w", err)665 return fmt.Errorf("send initial theme: %w", err)
665 }666 }
666 return nil667 return nil
@@ -756,7 +757,7 @@ func awaitRemotePickerPreview(ctx context.Context, tr *harnessTransport, marker
756 }757 }
757 }758 }
758 759
759-func remotePreviewContains(preview ports.RemotePreview, want string) bool {760+func remotePreviewContains(preview protocol.RemotePreview, want string) bool {
760 var text strings.Builder761 var text strings.Builder
761 for _, cell := range preview.Cells {762 for _, cell := range preview.Cells {
762 if !cell.Continuation {763 if !cell.Continuation {
M scripts/remote-picker-harness/visual_probe.go +6 -5
@@ -12,6 +12,7 @@ import (
12 vt "github.com/bnema/vev-vt"12 vt "github.com/bnema/vev-vt"
13 "github.com/bnema/vev/internal/domain"13 "github.com/bnema/vev/internal/domain"
14 "github.com/bnema/vev/internal/ports"14 "github.com/bnema/vev/internal/ports"
15+ "github.com/bnema/vev/internal/protocol"
15 )16 )
16 17
17 const (18 const (
@@ -38,7 +39,7 @@ type visualOutputState struct {
38 initialized bool39 initialized bool
39 }40 }
40 41
41-func (s visualOutputState) next(output ports.Output) (visualOutputState, bool) {42+func (s visualOutputState) next(output protocol.Output) (visualOutputState, bool) {
42 if output.Epoch == 0 {43 if output.Epoch == 0 {
43 return visualOutputState{}, false44 return visualOutputState{}, false
44 }45 }
@@ -102,7 +103,7 @@ type visualCheckpoint struct {
102 type visualOutputResult struct {103 type visualOutputResult struct {
103 Accepted bool104 Accepted bool
104 StateBearing bool105 StateBearing bool
105- Ack ports.Ack106+ Ack protocol.Ack
106 event *probeEvent107 event *probeEvent
107 }108 }
108 109
@@ -180,13 +181,13 @@ func (p *visualProbe) recordControl(kind string) {
180 p.record(probeEvent{Kind: kind, Accepted: true})181 p.record(probeEvent{Kind: kind, Accepted: true})
181 }182 }
182 183
183-func (p *visualProbe) apply(output ports.Output) visualOutputResult {184+func (p *visualProbe) apply(output protocol.Output) visualOutputResult {
184 if p == nil {185 if p == nil {
185 return visualOutputResult{}186 return visualOutputResult{}
186 }187 }
187 p.outputFrames++188 p.outputFrames++
188 p.outputBytes += len(output.Data)189 p.outputBytes += len(output.Data)
189- accepted := ports.ValidateOutput(output) == nil190+ accepted := protocol.ValidateOutput(output) == nil
190 next := visualOutputState{}191 next := visualOutputState{}
191 if accepted {192 if accepted {
192 next, accepted = p.state.next(output)193 next, accepted = p.state.next(output)
@@ -220,7 +221,7 @@ func (p *visualProbe) apply(output ports.Output) visualOutputResult {
220 p.checkpoints = append(p.checkpoints, checkpoint)221 p.checkpoints = append(p.checkpoints, checkpoint)
221 result := visualOutputResult{Accepted: true, StateBearing: output.New != 0, event: event}222 result := visualOutputResult{Accepted: true, StateBearing: output.New != 0, event: event}
222 if result.StateBearing {223 if result.StateBearing {
223- result.Ack = ports.Ack{Epoch: output.Epoch, State: output.New}224+ result.Ack = protocol.Ack{Epoch: output.Epoch, State: output.New}
224 }225 }
225 return result226 return result
226 }227 }
M scripts/remote-picker-harness/visual_probe_test.go +27 -26
@@ -9,6 +9,7 @@ import (
9 9
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11 "github.com/bnema/vev/internal/ports"11 "github.com/bnema/vev/internal/ports"
12+ "github.com/bnema/vev/internal/protocol"
12 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
13 )14 )
14 15
@@ -29,7 +30,7 @@ func (t *probeTestTransport) Send(frame ports.Frame) error {
29 func (*probeTestTransport) Recv() (ports.Frame, error) { return ports.Frame{}, errors.New("not used") }30 func (*probeTestTransport) Recv() (ports.Frame, error) { return ports.Frame{}, errors.New("not used") }
30 func (*probeTestTransport) Close() error { return nil }31 func (*probeTestTransport) Close() error { return nil }
31 32
32-func probeTestOutput(t *testing.T, output ports.Output) ports.Frame {33+func probeTestOutput(t *testing.T, output protocol.Output) ports.Frame {
33 t.Helper()34 t.Helper()
34 payload, err := ports.MarshalOutput(output)35 payload, err := ports.MarshalOutput(output)
35 if err != nil {36 if err != nil {
@@ -43,19 +44,19 @@ func TestVisualProbePersistsFullAndIncrementalOutput(t *testing.T) {
43 probe := newVisualProbe(size)44 probe := newVisualProbe(size)
44 for _, tt := range []struct {45 for _, tt := range []struct {
45 name string46 name string
46- output ports.Output47+ output protocol.Output
47 want string48 want string
48 state uint6449 state uint64
49 }{50 }{
50 {51 {
51 name: "full",52 name: "full",
52- output: ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},53+ output: protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},
53 want: "full",54 want: "full",
54 state: 1,55 state: 1,
55 },56 },
56 {57 {
57 name: "incremental",58 name: "incremental",
58- output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte(" incremental")},59+ output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte(" incremental")},
59 want: "full incremental",60 want: "full incremental",
60 state: 2,61 state: 2,
61 },62 },
@@ -87,7 +88,7 @@ func TestVisualProbePersistsFullAndIncrementalOutput(t *testing.T) {
87 func TestVisualProbeResizesToAcceptedOutput(t *testing.T) {88 func TestVisualProbeResizesToAcceptedOutput(t *testing.T) {
88 probe := newVisualProbe(domain.Size{Cols: probeTestCols, Rows: probeTestRows})89 probe := newVisualProbe(domain.Size{Cols: probeTestCols, Rows: probeTestRows})
89 outputSize := domain.Size{Cols: 48, Rows: 3}90 outputSize := domain.Size{Cols: 48, Rows: 3}
90- result := probe.apply(ports.Output{91+ result := probe.apply(protocol.Output{
91 Epoch: 1, New: 1, Full: true, ViewRevision: 1, Size: outputSize, Data: []byte("resized"),92 Epoch: 1, New: 1, Full: true, ViewRevision: 1, Size: outputSize, Data: []byte("resized"),
92 })93 })
93 94
@@ -103,30 +104,30 @@ func TestVisualProbeRejectsOutputWithoutMutationOrAck(t *testing.T) {
103 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}104 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
104 for _, tt := range []struct {105 for _, tt := range []struct {
105 name string106 name string
106- output ports.Output107+ output protocol.Output
107 }{108 }{
108 {109 {
109 name: "base gap",110 name: "base gap",
110- output: ports.Output{Epoch: 1, Base: 0, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("rejected")},111+ output: protocol.Output{Epoch: 1, Base: 0, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("rejected")},
111 },112 },
112 {113 {
113 name: "revision gap",114 name: "revision gap",
114- output: ports.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 5, Size: size, Data: []byte("rejected")},115+ output: protocol.Output{Epoch: 1, Base: 1, New: 2, ViewRevision: 5, Size: size, Data: []byte("rejected")},
115 },116 },
116 {117 {
117 name: "stale epoch",118 name: "stale epoch",
118- output: ports.Output{Epoch: 0, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte("rejected")},119+ output: protocol.Output{Epoch: 0, Base: 1, New: 2, ViewRevision: 4, Size: size, Data: []byte("rejected")},
119 },120 },
120 } {121 } {
121 t.Run(tt.name, func(t *testing.T) {122 t.Run(tt.name, func(t *testing.T) {
122 probe := newVisualProbe(size)123 probe := newVisualProbe(size)
123- first := ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("keep")}124+ first := protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("keep")}
124 if result := probe.apply(first); !result.Accepted {125 if result := probe.apply(first); !result.Accepted {
125 t.Fatal("seed output was rejected")126 t.Fatal("seed output was rejected")
126 }127 }
127 beforeText, beforeState, beforeCheckpoints := probe.text(), probe.state, len(probe.checkpoints)128 beforeText, beforeState, beforeCheckpoints := probe.text(), probe.state, len(probe.checkpoints)
128 result := probe.apply(tt.output)129 result := probe.apply(tt.output)
129- if result.Accepted || result.StateBearing || result.Ack != (ports.Ack{}) {130+ if result.Accepted || result.StateBearing || result.Ack != (protocol.Ack{}) {
130 t.Fatalf("result = %+v, want rejected output without ACK", result)131 t.Fatalf("result = %+v, want rejected output without ACK", result)
131 }132 }
132 if got := probe.text(); got != beforeText {133 if got := probe.text(); got != beforeText {
@@ -144,13 +145,13 @@ func TestVisualProbeRejectsOutputWithoutMutationOrAck(t *testing.T) {
144 145
145 func TestVisualProbeRejectsInvalidUninitializedSideEffects(t *testing.T) {146 func TestVisualProbeRejectsInvalidUninitializedSideEffects(t *testing.T) {
146 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}147 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
147- for _, output := range []ports.Output{148+ for _, output := range []protocol.Output{
148 {Epoch: 1, Base: 1, Size: size, Data: []byte("invalid base")},149 {Epoch: 1, Base: 1, Size: size, Data: []byte("invalid base")},
149 {Epoch: 1, Full: true, Size: size, Data: []byte("invalid full")},150 {Epoch: 1, Full: true, Size: size, Data: []byte("invalid full")},
150 } {151 } {
151 probe := newVisualProbe(size)152 probe := newVisualProbe(size)
152 result := probe.apply(output)153 result := probe.apply(output)
153- if result.Accepted || result.StateBearing || result.Ack != (ports.Ack{}) {154+ if result.Accepted || result.StateBearing || result.Ack != (protocol.Ack{}) {
154 t.Fatalf("result = %+v, want rejected uninitialized side effect", result)155 t.Fatalf("result = %+v, want rejected uninitialized side effect", result)
155 }156 }
156 require.Len(t, probe.events, 1)157 require.Len(t, probe.events, 1)
@@ -163,17 +164,17 @@ func TestVisualProbeSideEffectsApplyWithoutAck(t *testing.T) {
163 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}164 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
164 for _, tt := range []struct {165 for _, tt := range []struct {
165 name string166 name string
166- seed *ports.Output167+ seed *protocol.Output
167- effect ports.Output168+ effect protocol.Output
168 }{169 }{
169 {170 {
170 name: "before state-bearing output",171 name: "before state-bearing output",
171- effect: ports.Output{Epoch: 1, Size: size, Data: []byte("side effect")},172+ effect: protocol.Output{Epoch: 1, Size: size, Data: []byte("side effect")},
172 },173 },
173 {174 {
174 name: "after state-bearing output",175 name: "after state-bearing output",
175- seed: &ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},176+ seed: &protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},
176- effect: ports.Output{Epoch: 1, ViewRevision: 4, Size: size, Data: []byte(" side effect")},177+ effect: protocol.Output{Epoch: 1, ViewRevision: 4, Size: size, Data: []byte(" side effect")},
177 },178 },
178 } {179 } {
179 t.Run(tt.name, func(t *testing.T) {180 t.Run(tt.name, func(t *testing.T) {
@@ -183,7 +184,7 @@ func TestVisualProbeSideEffectsApplyWithoutAck(t *testing.T) {
183 }184 }
184 beforeState, beforeCheckpoints := probe.state, len(probe.checkpoints)185 beforeState, beforeCheckpoints := probe.state, len(probe.checkpoints)
185 result := probe.apply(tt.effect)186 result := probe.apply(tt.effect)
186- if !result.Accepted || result.StateBearing || result.Ack != (ports.Ack{}) {187+ if !result.Accepted || result.StateBearing || result.Ack != (protocol.Ack{}) {
187 t.Fatalf("result = %+v, want accepted side effect without ACK", result)188 t.Fatalf("result = %+v, want accepted side effect without ACK", result)
188 }189 }
189 if !strings.Contains(probe.text(), string(tt.effect.Data)) {190 if !strings.Contains(probe.text(), string(tt.effect.Data)) {
@@ -203,24 +204,24 @@ func TestProcessOutputFrameACKsOnlyAcceptedStateBearingFrames(t *testing.T) {
203 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}204 size := domain.Size{Cols: probeTestCols, Rows: probeTestRows}
204 for _, tt := range []struct {205 for _, tt := range []struct {
205 name string206 name string
206- seed *ports.Output207+ seed *protocol.Output
207- output ports.Output208+ output protocol.Output
208 wantACKs int209 wantACKs int
209 wantResets int210 wantResets int
210 }{211 }{
211 {212 {
212 name: "accepted full",213 name: "accepted full",
213- output: ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},214+ output: protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("full")},
214 wantACKs: 1,215 wantACKs: 1,
215 },216 },
216 {217 {
217 name: "accepted side effect",218 name: "accepted side effect",
218- output: ports.Output{Epoch: 1, Size: size, Data: []byte("side effect")},219+ output: protocol.Output{Epoch: 1, Size: size, Data: []byte("side effect")},
219 },220 },
220 {221 {
221 name: "rejected state",222 name: "rejected state",
222- seed: &ports.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},223+ seed: &protocol.Output{Epoch: 1, New: 1, Full: true, ViewRevision: 4, Size: size, Data: []byte("seed")},
223- output: ports.Output{Epoch: 1, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("gap")},224+ output: protocol.Output{Epoch: 1, New: 3, Full: true, ViewRevision: 4, Size: size, Data: []byte("gap")},
224 wantResets: 1,225 wantResets: 1,
225 },226 },
226 } {227 } {
@@ -307,7 +308,7 @@ func TestHarnessArtifactIsBoundedAndContainsOnlyMetadata(t *testing.T) {
307 artifact.registerProbe(probe)308 artifact.registerProbe(probe)
308 secret := "not-for-artifact"309 secret := "not-for-artifact"
309 for state := uint64(1); state <= maxProbeEvents+1; state++ {310 for state := uint64(1); state <= maxProbeEvents+1; state++ {
310- output := ports.Output{Epoch: 1, Base: state - 1, New: state, Size: size, Data: []byte(secret)}311+ output := protocol.Output{Epoch: 1, Base: state - 1, New: state, Size: size, Data: []byte(secret)}
311 if state == 1 {312 if state == 1 {
312 output.Full = true313 output.Full = true
313 }314 }
M transport_replay_integration_test.go +3 -2
@@ -11,6 +11,7 @@ import (
11 "github.com/bnema/vev/internal/adapters/ipc"11 "github.com/bnema/vev/internal/adapters/ipc"
12 "github.com/bnema/vev/internal/adapters/sshstdio"12 "github.com/bnema/vev/internal/adapters/sshstdio"
13 "github.com/bnema/vev/internal/ports"13 "github.com/bnema/vev/internal/ports"
14+ "github.com/bnema/vev/internal/protocol"
14 "github.com/bnema/vev/internal/testutil/replaytest"15 "github.com/bnema/vev/internal/testutil/replaytest"
15 "github.com/stretchr/testify/require"16 "github.com/stretchr/testify/require"
16 )17 )
@@ -53,7 +54,7 @@ func TestTransportReplayIntegration(t *testing.T) {
53 // SSH-stdio transports. The transports only carry frames; neither can infer54 // SSH-stdio transports. The transports only carry frames; neither can infer
54 // or alter the palette lifecycle.55 // or alter the palette lifecycle.
55 func TestThemeGenerationTransportSequences(t *testing.T) {56 func TestThemeGenerationTransportSequences(t *testing.T) {
56- cleared := ports.Theme{57+ cleared := protocol.Theme{
57 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},58 HasForeground: true, Foreground: renderer.RGB{R: 1, G: 2, B: 3},
58 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},59 HasBackground: true, Background: renderer.RGB{R: 4, G: 5, B: 6},
59 TrueColor: true, SchemeKnown: true,60 TrueColor: true, SchemeKnown: true,
@@ -62,7 +63,7 @@ func TestThemeGenerationTransportSequences(t *testing.T) {
62 definitive := cleared63 definitive := cleared
63 definitive.Palette = palette64 definitive.Palette = palette
64 definitive.PaletteKnown = 1<<2 | 1<<1065 definitive.PaletteKnown = 1<<2 | 1<<10
65- sequence := []ports.Theme{cleared, definitive}66+ sequence := []protocol.Theme{cleared, definitive}
66 67
67 tests := []struct {68 tests := []struct {
68 name string69 name string

Keyboard shortcuts

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

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

Branches 12

Tags 11

↑↓ moveenter openesc close