detached b21386d

refactor(protocol): extract session control contracts (#233)

* refactor(protocol): extract session control contracts

* docs(architecture): record session contract ownership
commit
b21386ddeb77d3332261a35a70adec59ce534281
author
bnema · · 2026-08-28 11:44 UTC
committer
GitHub · · 2026-08-28 11:44 UTC
parent
be1cf92
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
122 files · +2209 -2141
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-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`.64+Semantic presentation, route, and session-control values live in `internal/protocol`. 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/proxy_test.go +4 -4
@@ -89,7 +89,7 @@ func TestProxyRuntimeClampsDatagramHelloOutputWindowAndPreservesControl(t *testi
89 daemon := newFakeTransport()89 daemon := newFakeTransport()
90 errCh := make(chan error, 1)90 errCh := make(chan error, 1)
91 go func() { errCh <- ProxyRuntime{Client: client, Daemon: daemon, IdleTTL: time.Hour}.Run(t.Context()) }()91 go func() { errCh <- ProxyRuntime{Client: client, Daemon: daemon, IdleTTL: time.Hour}.Run(t.Context()) }()
92- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: requested}92+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: requested}
93 client.recv <- recvResult{frame: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}}93 client.recv <- recvResult{frame: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}}
94 94
95 select {95 select {
@@ -490,7 +490,7 @@ func TestProxyCopierStopsRetryingWhenContextCanceled(t *testing.T) {
490 490
491 func TestClampDatagramHelloOutputWindowPreservesProtocolValidation(t *testing.T) {491 func TestClampDatagramHelloOutputWindowPreservesProtocolValidation(t *testing.T) {
492 t.Run("malformed payload remains malformed", func(t *testing.T) {492 t.Run("malformed payload remains malformed", func(t *testing.T) {
493- payload := append(ports.MarshalHello(ports.Hello{Version: ports.ProtocolVersion, Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: 8}), 0xff)493+ payload := append(ports.MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: 8}), 0xff)
494 frame := ports.Frame{Type: ports.MsgHello, Payload: payload}494 frame := ports.Frame{Type: ports.MsgHello, Payload: payload}
495 got := clampDatagramHelloOutputWindow(frame)495 got := clampDatagramHelloOutputWindow(frame)
496 if !reflect.DeepEqual(got, frame) {496 if !reflect.DeepEqual(got, frame) {
@@ -502,8 +502,8 @@ func TestClampDatagramHelloOutputWindowPreservesProtocolValidation(t *testing.T)
502 })502 })
503 503
504 t.Run("mismatched version remains mismatched", func(t *testing.T) {504 t.Run("mismatched version remains mismatched", func(t *testing.T) {
505- wantVersion := ports.ProtocolVersion + 1505+ wantVersion := protocol.Version + 1
506- frame := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{506+ frame := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
507 Version: wantVersion, Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: 8,507 Version: wantVersion, Size: domain.Size{Cols: 80, Rows: 24}, MaxOutputInFlight: 8,
508 })}508 })}
509 got, err := ports.UnmarshalHello(clampDatagramHelloOutputWindow(frame).Payload)509 got, err := ports.UnmarshalHello(clampDatagramHelloOutputWindow(frame).Payload)
M internal/adapters/ipc/transport_test.go +3 -3
@@ -24,8 +24,8 @@ func TestTransportSendRecvBothDirections(t *testing.T) {
24 client := NewTransport(c1)24 client := NewTransport(c1)
25 server := NewTransport(c2)25 server := NewTransport(c2)
26 26
27- clientToServer := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{Version: 1, Intent: ports.IntentNew, Name: "w0"})}27+ clientToServer := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{Version: 1, Intent: protocol.IntentNew, Name: "w0"})}
28- serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"})}28+ serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: "main"})}
29 29
30 var wg sync.WaitGroup30 var wg sync.WaitGroup
31 wg.Go(func() {31 wg.Go(func() {
@@ -140,7 +140,7 @@ func TestTransportManySmallFramesBackToBack(t *testing.T) {
140 frames[i] = ports.Frame{Type: ports.MsgPing, Payload: nil}140 frames[i] = ports.Frame{Type: ports.MsgPing, Payload: nil}
141 }141 }
142 // Vary a couple to prove ordering and content are both preserved.142 // Vary a couple to prove ordering and content are both preserved.
143- 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(protocol.Input{Data: []byte("hop")})}
144 frames[150] = ports.Frame{Type: ports.MsgResize, Payload: mustMarshalResize(protocol.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}})}
145 145
146 var wg sync.WaitGroup146 var wg sync.WaitGroup
M internal/adapters/remote/catalog.go +3 -2
@@ -17,6 +17,7 @@ import (
17 "github.com/bnema/vev/internal/adapters/sshstdio"17 "github.com/bnema/vev/internal/adapters/sshstdio"
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 )21 )
21 22
22 const (23 const (
@@ -158,10 +159,10 @@ func decodeRemoteCatalog(raw []byte) (ports.RemoteCatalog, error) {
158 Kind: "catalog",159 Kind: "catalog",
159 }160 }
160 }161 }
161- if *version.ProtocolVersion != ports.ProtocolVersion {162+ if *version.ProtocolVersion != protocol.Version {
162 return ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{163 return ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{
163 Got: *version.ProtocolVersion,164 Got: *version.ProtocolVersion,
164- Want: ports.ProtocolVersion,165+ Want: protocol.Version,
165 Kind: "protocol",166 Kind: "protocol",
166 }167 }
167 }168 }
M internal/adapters/remote/catalog_test.go +15 -14
@@ -13,6 +13,7 @@ import (
13 13
14 "github.com/bnema/vev/internal/adapters/sshstdio"14 "github.com/bnema/vev/internal/adapters/sshstdio"
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 TestRemoteCatalogClientCommandConstruction(t *testing.T) {19 func TestRemoteCatalogClientCommandConstruction(t *testing.T) {
@@ -37,7 +38,7 @@ func TestRemoteCatalogClientCommandConstruction(t *testing.T) {
37 gotPath = name38 gotPath = name
38 gotArgs = append([]string(nil), args...)39 gotArgs = append([]string(nil), args...)
39 return stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{40 return stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{
40- ProtocolVersion: ports.ProtocolVersion,41+ ProtocolVersion: protocol.Version,
41 SchemaVersion: ports.RemoteCatalogSchemaVersion,42 SchemaVersion: ports.RemoteCatalogSchemaVersion,
42 Sessions: []ports.RemoteCatalogSession{},43 Sessions: []ports.RemoteCatalogSession{},
43 }))44 }))
@@ -83,7 +84,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
83 t.Parallel()84 t.Parallel()
84 85
85 valid := ports.RemoteCatalog{86 valid := ports.RemoteCatalog{
86- ProtocolVersion: ports.ProtocolVersion,87+ ProtocolVersion: protocol.Version,
87 SchemaVersion: ports.RemoteCatalogSchemaVersion,88 SchemaVersion: ports.RemoteCatalogSchemaVersion,
88 Sessions: []ports.RemoteCatalogSession{89 Sessions: []ports.RemoteCatalogSession{
89 {LifecycleID: [16]byte{1}, Name: "work", State: "up", Ephemeral: false, Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}, {ID: "tab-2", Index: 1}}, Attached: true},90 {LifecycleID: [16]byte{1}, Name: "work", State: "up", Ephemeral: false, Tabs: []ports.RemoteCatalogTab{{ID: "tab-1"}, {ID: "tab-2", Index: 1}}, Attached: true},
@@ -112,14 +113,14 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
112 },113 },
113 {114 {
114 name: "mismatching protocol version",115 name: "mismatching protocol version",
115- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion + 1, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{}}),116+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version + 1, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{}}),
116 check: func(t *testing.T, err error) {117 check: func(t *testing.T, err error) {
117 t.Helper()118 t.Helper()
118 var mismatch *ports.RemoteCatalogVersionMismatchError119 var mismatch *ports.RemoteCatalogVersionMismatchError
119 if !errors.As(err, &mismatch) {120 if !errors.As(err, &mismatch) {
120 t.Fatalf("error = %v, want RemoteCatalogVersionMismatchError", err)121 t.Fatalf("error = %v, want RemoteCatalogVersionMismatchError", err)
121 }122 }
122- if mismatch.Kind != "protocol" || mismatch.Got != ports.ProtocolVersion+1 || mismatch.Want != ports.ProtocolVersion {123+ if mismatch.Kind != "protocol" || mismatch.Got != protocol.Version+1 || mismatch.Want != protocol.Version {
123 t.Fatalf("mismatch = %#v", mismatch)124 t.Fatalf("mismatch = %#v", mismatch)
124 }125 }
125 },126 },
@@ -127,7 +128,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
127 {128 {
128 name: "obsolete count-only schema is classified before session decoding",129 name: "obsolete count-only schema is classified before session decoding",
129 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":2,"sessions":[{"tabs":1}]}`,130 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":2,"sessions":[{"tabs":1}]}`,
130- ports.ProtocolVersion),131+ protocol.Version),
131 check: func(t *testing.T, err error) {132 check: func(t *testing.T, err error) {
132 t.Helper()133 t.Helper()
133 var mismatch *ports.RemoteCatalogVersionMismatchError134 var mismatch *ports.RemoteCatalogVersionMismatchError
@@ -138,7 +139,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
138 },139 },
139 {140 {
140 name: "unknown catalogue schema",141 name: "unknown catalogue schema",
141- stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":4,"sessions":[]}`, ports.ProtocolVersion),142+ stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":4,"sessions":[]}`, protocol.Version),
142 check: func(t *testing.T, err error) {143 check: func(t *testing.T, err error) {
143 t.Helper()144 t.Helper()
144 var mismatch *ports.RemoteCatalogVersionMismatchError145 var mismatch *ports.RemoteCatalogVersionMismatchError
@@ -160,19 +161,19 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
160 {161 {
161 name: "count-only tabs rejected",162 name: "count-only tabs rejected",
162 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false}]}`,163 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","ephemeral":false,"tabs":1,"attached":false}]}`,
163- ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),164+ protocol.Version, ports.RemoteCatalogSchemaVersion),
164 wantErr: errCatalogDecode,165 wantErr: errCatalogDecode,
165 },166 },
166 {167 {
167 name: "unknown fields rejected",168 name: "unknown fields rejected",
168 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[],"future":true}`,169 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[],"future":true}`,
169- ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),170+ protocol.Version, ports.RemoteCatalogSchemaVersion),
170 wantErr: errCatalogDecode,171 wantErr: errCatalogDecode,
171 },172 },
172 {173 {
173 name: "missing required session field",174 name: "missing required session field",
174 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","tabs":[],"attached":false}]}`,175 stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d,"sessions":[{"lifecycle_id":"01000000000000000000000000000000","name":"work","state":"up","tabs":[],"attached":false}]}`,
175- ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),176+ protocol.Version, ports.RemoteCatalogSchemaVersion),
176 wantErr: errCatalogRequired,177 wantErr: errCatalogRequired,
177 },178 },
178 {179 {
@@ -187,7 +188,7 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
187 },188 },
188 {189 {
189 name: "missing catalogue schema",190 name: "missing catalogue schema",
190- stdout: fmt.Sprintf(`{"protocol_version":%d,"sessions":[]}`+"\n", ports.ProtocolVersion),191+ stdout: fmt.Sprintf(`{"protocol_version":%d,"sessions":[]}`+"\n", protocol.Version),
191 check: func(t *testing.T, err error) {192 check: func(t *testing.T, err error) {
192 t.Helper()193 t.Helper()
193 var mismatch *ports.RemoteCatalogVersionMismatchError194 var mismatch *ports.RemoteCatalogVersionMismatchError
@@ -198,19 +199,19 @@ func TestRemoteCatalogClientDecode(t *testing.T) {
198 },199 },
199 {200 {
200 name: "missing sessions",201 name: "missing sessions",
201- stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d}`+"\n", ports.ProtocolVersion, ports.RemoteCatalogSchemaVersion),202+ stdout: fmt.Sprintf(`{"protocol_version":%d,"schema_version":%d}`+"\n", protocol.Version, ports.RemoteCatalogSchemaVersion),
202 wantErr: errCatalogRequired,203 wantErr: errCatalogRequired,
203 },204 },
204 {205 {
205 name: "invalid session name",206 name: "invalid session name",
206- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{207+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
207 LifecycleID: [16]byte{1}, Name: "bad name", State: "up", Tabs: []ports.RemoteCatalogTab{},208 LifecycleID: [16]byte{1}, Name: "bad name", State: "up", Tabs: []ports.RemoteCatalogTab{},
208 }}}),209 }}}),
209 wantErr: ports.ErrInvalidRemoteCatalog,210 wantErr: ports.ErrInvalidRemoteCatalog,
210 },211 },
211 {212 {
212 name: "invalid session state",213 name: "invalid session state",
213- stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{214+ stdout: mustCatalogJSON(t, ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{{
214 LifecycleID: [16]byte{1}, Name: "work", State: "weird", Tabs: []ports.RemoteCatalogTab{},215 LifecycleID: [16]byte{1}, Name: "work", State: "weird", Tabs: []ports.RemoteCatalogTab{},
215 }}}),216 }}}),
216 wantErr: ports.ErrRemoteCatalogUnknownState,217 wantErr: ports.ErrRemoteCatalogUnknownState,
@@ -292,7 +293,7 @@ func TestRemoteCatalogClientAppliesLifetimeAndWaitDelay(t *testing.T) {
292 command: func(ctx context.Context, name string, args ...string) *exec.Cmd {293 command: func(ctx context.Context, name string, args ...string) *exec.Cmd {
293 commandCtx = ctx294 commandCtx = ctx
294 cmd = stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{295 cmd = stdoutCmd(ctx, mustCatalogJSON(t, ports.RemoteCatalog{
295- ProtocolVersion: ports.ProtocolVersion,296+ ProtocolVersion: protocol.Version,
296 SchemaVersion: ports.RemoteCatalogSchemaVersion,297 SchemaVersion: ports.RemoteCatalogSchemaVersion,
297 Sessions: []ports.RemoteCatalogSession{},298 Sessions: []ports.RemoteCatalogSession{},
298 }))299 }))
M internal/adapters/sshstdio/transport_test.go +5 -4
@@ -15,6 +15,7 @@ import (
15 15
16 "github.com/bnema/vev/internal/domain"16 "github.com/bnema/vev/internal/domain"
17 "github.com/bnema/vev/internal/ports"17 "github.com/bnema/vev/internal/ports"
18+ "github.com/bnema/vev/internal/protocol"
18 )19 )
19 20
20 func TestBuildCommandUsesExecArgs(t *testing.T) {21 func TestBuildCommandUsesExecArgs(t *testing.T) {
@@ -158,7 +159,7 @@ func TestTransportRoundTripAndVersionMismatchFrame(t *testing.T) {
158 defer func() { _ = client.Close() }()159 defer func() { _ = client.Close() }()
159 defer func() { _ = server.Close() }()160 defer func() { _ = server.Close() }()
160 161
161- hello := ports.Hello{Version: ports.ProtocolVersion + 1, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}}162+ hello := protocol.Hello{Version: protocol.Version + 1, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}}
162 go func() {163 go func() {
163 f, err := server.Recv()164 f, err := server.Recv()
164 if err != nil {165 if err != nil {
@@ -170,10 +171,10 @@ func TestTransportRoundTripAndVersionMismatchFrame(t *testing.T) {
170 t.Errorf("UnmarshalHello: %v", err)171 t.Errorf("UnmarshalHello: %v", err)
171 return172 return
172 }173 }
173- if got.Version == ports.ProtocolVersion {174+ if got.Version == protocol.Version {
174 t.Errorf("test did not send a mismatched version")175 t.Errorf("test did not send a mismatched version")
175 }176 }
176- _ = server.Send(ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "protocol version mismatch"})})177+ _ = server.Send(ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})})
177 }()178 }()
178 179
179 if err := client.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil {180 if err := client.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil {
@@ -190,7 +191,7 @@ func TestTransportRoundTripAndVersionMismatchFrame(t *testing.T) {
190 if err != nil {191 if err != nil {
191 t.Fatalf("UnmarshalErrorMsg: %v", err)192 t.Fatalf("UnmarshalErrorMsg: %v", err)
192 }193 }
193- if em.Code != ports.ErrVersionMismatch {194+ if em.Code != protocol.ErrVersionMismatch {
194 t.Fatalf("error code = %d, want ErrVersionMismatch", em.Code)195 t.Fatalf("error code = %d, want ErrVersionMismatch", em.Code)
195 }196 }
196 }197 }
M internal/app/cmd.go +4 -3
@@ -13,6 +13,7 @@ import (
13 "github.com/bnema/vev/internal/adapters/clock"13 "github.com/bnema/vev/internal/adapters/clock"
14 "github.com/bnema/vev/internal/adapters/ipc"14 "github.com/bnema/vev/internal/adapters/ipc"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
16 commandusecase "github.com/bnema/vev/internal/usecase/command"17 commandusecase "github.com/bnema/vev/internal/usecase/command"
17 "github.com/bnema/vev/internal/usecase/daemon"18 "github.com/bnema/vev/internal/usecase/daemon"
18 )19 )
@@ -149,8 +150,8 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps)
149 _, err := fmt.Fprintln(deps.stdout, cmdHelp(invocation))150 _, err := fmt.Fprintln(deps.stdout, cmdHelp(invocation))
150 return err151 return err
151 }152 }
152- request := ports.CommandRequest{153+ request := protocol.CommandRequest{
153- Version: ports.ProtocolVersion,154+ Version: protocol.Version,
154 Self: invocation.self,155 Self: invocation.self,
155 Slug: invocation.slug,156 Slug: invocation.slug,
156 Args: invocation.args,157 Args: invocation.args,
@@ -225,7 +226,7 @@ func runCmdWithDeps(ctx context.Context, invocation cmdInvocation, deps cmdDeps)
225 return &exitCoded{code: 3, err: err}226 return &exitCoded{code: 3, err: err}
226 }227 }
227 if !result.OK {228 if !result.OK {
228- if result.Code == ports.ErrInvalidCommandArgs {229+ if result.Code == protocol.ErrInvalidCommandArgs {
229 return &exitCoded{code: 2, err: errors.New(result.Text)}230 return &exitCoded{code: 2, err: errors.New(result.Text)}
230 }231 }
231 return errors.New(result.Text)232 return errors.New(result.Text)
M internal/app/cmd_test.go +17 -16
@@ -13,6 +13,7 @@ import (
13 13
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/daemon"17 "github.com/bnema/vev/internal/usecase/daemon"
17 "github.com/stretchr/testify/mock"18 "github.com/stretchr/testify/mock"
18 "github.com/stretchr/testify/require"19 "github.com/stretchr/testify/require"
@@ -140,7 +141,7 @@ func TestMoveCmdPreservesPositionalArguments(t *testing.T) {
140 t.Run(tt.slug, func(t *testing.T) {141 t.Run(tt.slug, func(t *testing.T) {
141 parsed, err := parseArgs(append([]string{"cmd", tt.slug}, tt.args...))142 parsed, err := parseArgs(append([]string{"cmd", tt.slug}, tt.args...))
142 require.NoError(t, err)143 require.NoError(t, err)
143- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}144+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
144 require.NoError(t, runCmdWithDeps(context.Background(), parsed.cmd, cmdDeps{145 require.NoError(t, runCmdWithDeps(context.Background(), parsed.cmd, cmdDeps{
145 stdout: io.Discard,146 stdout: io.Discard,
146 getenv: func(string) string { return "" },147 getenv: func(string) string { return "" },
@@ -151,13 +152,13 @@ func TestMoveCmdPreservesPositionalArguments(t *testing.T) {
151 require.NoError(t, err)152 require.NoError(t, err)
152 require.Equal(t, tt.slug, request.Slug)153 require.Equal(t, tt.slug, request.Slug)
153 require.Equal(t, tt.args, request.Args)154 require.Equal(t, tt.args, request.Args)
154- require.Equal(t, ports.ProtocolVersion, request.Version)155+ require.Equal(t, protocol.Version, request.Version)
155 })156 })
156 }157 }
157 }158 }
158 159
159 func TestRemoteCatalogCommandEnsuresDaemon(t *testing.T) {160 func TestRemoteCatalogCommandEnsuresDaemon(t *testing.T) {
160- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}161+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
161 ensureCalls := 0162 ensureCalls := 0
162 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "remote-catalog", jsonOut: true}, cmdDeps{163 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "remote-catalog", jsonOut: true}, cmdDeps{
163 stdout: io.Discard,164 stdout: io.Discard,
@@ -183,8 +184,8 @@ func TestMoveCmdInvalidArgumentResultExitsTwo(t *testing.T) {
183 {slug: "move-tab", args: []string{"work", "extra"}},184 {slug: "move-tab", args: []string{"work", "extra"}},
184 } {185 } {
185 t.Run(invocation.slug+"/"+strconv.Itoa(len(invocation.args)), func(t *testing.T) {186 t.Run(invocation.slug+"/"+strconv.Itoa(len(invocation.args)), func(t *testing.T) {
186- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{187+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{
187- Code: ports.ErrInvalidCommandArgs, Text: "invalid command arguments",188+ Code: protocol.ErrInvalidCommandArgs, Text: "invalid command arguments",
188 })}}189 })}}
189 err := runCmdWithDeps(context.Background(), invocation, cmdDeps{190 err := runCmdWithDeps(context.Background(), invocation, cmdDeps{
190 stdout: io.Discard,191 stdout: io.Discard,
@@ -206,7 +207,7 @@ func TestTargetPaneCmdsParseAndUseSelfTarget(t *testing.T) {
206 t.Run(slug, func(t *testing.T) {207 t.Run(slug, func(t *testing.T) {
207 invocation, err := parseArgs([]string{"cmd", "--self", slug})208 invocation, err := parseArgs([]string{"cmd", "--self", slug})
208 require.NoError(t, err)209 require.NoError(t, err)
209- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}210+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
210 require.NoError(t, runCmdWithDeps(context.Background(), invocation.cmd, cmdDeps{211 require.NoError(t, runCmdWithDeps(context.Background(), invocation.cmd, cmdDeps{
211 stdout: io.Discard,212 stdout: io.Discard,
212 getenv: func(string) string { return "session=work,tab=t_abc,pane=p_def" },213 getenv: func(string) string { return "session=work,tab=t_abc,pane=p_def" },
@@ -225,7 +226,7 @@ func TestTargetPaneCmdsParseAndUseSelfTarget(t *testing.T) {
225 }226 }
226 227
227 func TestRunCmdBuildsOneShotTargetedRequest(t *testing.T) {228 func TestRunCmdBuildsOneShotTargetedRequest(t *testing.T) {
228- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true, Output: "done"})}}229+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true, Output: "done"})}}
229 out := new(strings.Builder)230 out := new(strings.Builder)
230 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right", self: true}, cmdDeps{231 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right", self: true}, cmdDeps{
231 stdout: out,232 stdout: out,
@@ -251,7 +252,7 @@ func TestRunCmdBuildsOneShotTargetedRequest(t *testing.T) {
251 }252 }
252 253
253 func TestRunCmdVEVWithoutSelfUsesIDsOnlyAsSessionLocator(t *testing.T) {254 func TestRunCmdVEVWithoutSelfUsesIDsOnlyAsSessionLocator(t *testing.T) {
254- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}255+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
255 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{256 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{
256 stdout: io.Discard,257 stdout: io.Discard,
257 getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" },258 getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" },
@@ -270,7 +271,7 @@ func TestRunCmdVEVWithoutSelfUsesIDsOnlyAsSessionLocator(t *testing.T) {
270 }271 }
271 272
272 func TestRunCmdExplicitSessionDoesNotUseEnvIDsWithoutSelf(t *testing.T) {273 func TestRunCmdExplicitSessionDoesNotUseEnvIDsWithoutSelf(t *testing.T) {
273- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{OK: true})}}274+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{OK: true})}}
274 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "new-tab", session: "current"}, cmdDeps{275 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "new-tab", session: "current"}, cmdDeps{
275 stdout: io.Discard,276 stdout: io.Discard,
276 getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" },277 getenv: func(string) string { return "session=old,tab=t_abc,pane=p_def" },
@@ -350,8 +351,8 @@ func TestRunCmdTimesOutPendingRequest(t *testing.T) {
350 351
351 func TestRunCmdZeroRequestIDReplyReturnsDaemonError(t *testing.T) {352 func TestRunCmdZeroRequestIDReplyReturnsDaemonError(t *testing.T) {
352 const want = "daemon error"353 const want = "daemon error"
353- transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(ports.CommandResult{354+ transport := &cmdTestTransport{recv: ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(protocol.CommandResult{
354- Code: ports.ErrInternal, Text: want,355+ Code: protocol.ErrInternal, Text: want,
355 })}}356 })}}
356 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{357 err := runCmdWithDeps(context.Background(), cmdInvocation{slug: "split-right"}, cmdDeps{
357 stdout: io.Discard,358 stdout: io.Discard,
@@ -388,27 +389,27 @@ func TestRunCmdDoesNotAutostartAndClassifiesDialFailure(t *testing.T) {
388 func TestRunCmdClassifiesDaemonCommandErrors(t *testing.T) {389 func TestRunCmdClassifiesDaemonCommandErrors(t *testing.T) {
389 tests := []struct {390 tests := []struct {
390 name string391 name string
391- result ports.CommandResult392+ result protocol.CommandResult
392 wantCode int393 wantCode int
393 }{394 }{
394 {395 {
395 name: "invalid command arguments are usage errors",396 name: "invalid command arguments are usage errors",
396- result: ports.CommandResult{Code: ports.ErrInvalidCommandArgs, Text: "usage: toast [-l level] <message>"},397+ result: protocol.CommandResult{Code: protocol.ErrInvalidCommandArgs, Text: "usage: toast [-l level] <message>"},
397 wantCode: 2,398 wantCode: 2,
398 },399 },
399 {400 {
400 name: "missing runtime target is a command failure",401 name: "missing runtime target is a command failure",
401- result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "no live sessions"},402+ result: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "no live sessions"},
402 wantCode: 1,403 wantCode: 1,
403 },404 },
404 {405 {
405 name: "resize not in split remains an exit one failure",406 name: "resize not in split remains an exit one failure",
406- result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "pane is not in a split"},407+ result: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "pane is not in a split"},
407 wantCode: 1,408 wantCode: 1,
408 },409 },
409 {410 {
410 name: "resize minimum remains an exit one failure",411 name: "resize minimum remains an exit one failure",
411- result: ports.CommandResult{Code: ports.ErrNoSuchTarget, Text: "pane cannot be resized further"},412+ result: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "pane cannot be resized further"},
412 wantCode: 1,413 wantCode: 1,
413 },414 },
414 }415 }
M internal/app/daemon_integration_test.go +41 -40
@@ -28,6 +28,7 @@ import (
28 "github.com/bnema/vev/internal/domain"28 "github.com/bnema/vev/internal/domain"
29 "github.com/bnema/vev/internal/persist"29 "github.com/bnema/vev/internal/persist"
30 "github.com/bnema/vev/internal/ports"30 "github.com/bnema/vev/internal/ports"
31+ "github.com/bnema/vev/internal/protocol"
31 "github.com/bnema/vev/internal/usecase/daemon"32 "github.com/bnema/vev/internal/usecase/daemon"
32 "github.com/bnema/vev/internal/usecase/recovery"33 "github.com/bnema/vev/internal/usecase/recovery"
33 )34 )
@@ -90,7 +91,7 @@ func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string,
90 t.Helper()91 t.Helper()
91 tr, err := ipc.DialContext(context.Background(), dir)92 tr, err := ipc.DialContext(context.Background(), dir)
92 require.NoError(t, err)93 require.NoError(t, err)
93- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env}94+ hello := protocol.Hello{Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env}
94 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}))95 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}))
95 p := recvPump(tr)96 p := recvPump(tr)
96 select {97 select {
@@ -103,12 +104,12 @@ func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string,
103 return tr, p104 return tr, p
104 }105 }
105 106
106-func listRemoteSessions(t *testing.T, dir string) ports.Sessions {107+func listRemoteSessions(t *testing.T, dir string) protocol.Sessions {
107 t.Helper()108 t.Helper()
108 tr, err := ipc.DialContext(context.Background(), dir)109 tr, err := ipc.DialContext(context.Background(), dir)
109 require.NoError(t, err)110 require.NoError(t, err)
110 defer func() { _ = tr.Close() }()111 defer func() { _ = tr.Close() }()
111- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}))112+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})}))
112 f, err := tr.Recv()113 f, err := tr.Recv()
113 require.NoError(t, err)114 require.NoError(t, err)
114 require.Equal(t, ports.MsgSessions, f.Type)115 require.Equal(t, ports.MsgSessions, f.Type)
@@ -123,7 +124,7 @@ func killAll(dir string) error {
123 return err124 return err
124 }125 }
125 defer func() { _ = tr.Close() }()126 defer func() { _ = tr.Close() }()
126- if err := tr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})}); err != nil {127+ if err := tr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})}); err != nil {
127 return err128 return err
128 }129 }
129 _, err = tr.Recv()130 _, err = tr.Recv()
@@ -213,7 +214,7 @@ func shellFixture(t *testing.T, label string) string {
213 func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain.Size, wantTestEnv, wantShell, wantRuntimeDir, wantWayland string) {214 func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain.Size, wantTestEnv, wantShell, wantRuntimeDir, wantWayland string) {
214 t.Helper()215 t.Helper()
215 command := "printf '\\033[2J\\033[H'; printf 'VEV_TEST_ENV=%s SHELL=%s XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s TERM=%s COLORTERM=%s TERM_PROGRAM=%s VEV_PREFIX=%.24s\\n' \"$VEV_TEST_ENV\" \"${SHELL##*/}\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$TERM\" \"$COLORTERM\" \"$TERM_PROGRAM\" \"$VEV\"\n"216 command := "printf '\\033[2J\\033[H'; printf 'VEV_TEST_ENV=%s SHELL=%s XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s TERM=%s COLORTERM=%s TERM_PROGRAM=%s VEV_PREFIX=%.24s\\n' \"$VEV_TEST_ENV\" \"${SHELL##*/}\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$TERM\" \"$COLORTERM\" \"$TERM_PROGRAM\" \"$VEV\"\n"
216- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(command)})}))217+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(command)})}))
217 text := awaitScreenText(t, p, sz, "TERM_PROGRAM=vev")218 text := awaitScreenText(t, p, sz, "TERM_PROGRAM=vev")
218 for _, want := range []string{219 for _, want := range []string{
219 "VEV_TEST_ENV=" + wantTestEnv,220 "VEV_TEST_ENV=" + wantTestEnv,
@@ -239,7 +240,7 @@ func TestIntegration_AttachEnvironmentRefreshesFuturePTYChildren(t *testing.T) {
239 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",240 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
240 }241 }
241 242
242- tr1, p1 := attachWithEnvironment(t, dir, ports.IntentNew, "environment", sz, firstEnv)243+ tr1, p1 := attachWithEnvironment(t, dir, protocol.IntentNew, "environment", sz, firstEnv)
243 defer func() { _ = tr1.Close() }()244 defer func() { _ = tr1.Close() }()
244 awaitText(t, p1, sz, "SHELL_COMMAND=first")245 awaitText(t, p1, sz, "SHELL_COMMAND=first")
245 assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")246 assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")
@@ -248,15 +249,15 @@ func TestIntegration_AttachEnvironmentRefreshesFuturePTYChildren(t *testing.T) {
248 "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",249 "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",
249 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",250 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
250 }251 }
251- tr2, p2 := attachWithEnvironment(t, dir, ports.IntentAttach, "environment", sz, secondEnv)252+ tr2, p2 := attachWithEnvironment(t, dir, protocol.IntentAttach, "environment", sz, secondEnv)
252 defer func() { _ = tr2.Close() }()253 defer func() { _ = tr2.Close() }()
253 254
254 // The first shell was already running, so it retains its original environment.255 // The first shell was already running, so it retains its original environment.
255 assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")256 assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")
256 257
257- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))258+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
258 awaitText(t, p2, sz, "Commands")259 awaitText(t, p2, sz, "Commands")
259- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))260+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("CNT\r")})}))
260 awaitText(t, p2, sz, "SHELL_COMMAND=second")261 awaitText(t, p2, sz, "SHELL_COMMAND=second")
261 assertChildEnvironment(t, tr2, p2, sz, "second", secondShell, "/run/second", "wayland-second")262 assertChildEnvironment(t, tr2, p2, sz, "second", secondShell, "/run/second", "wayland-second")
262 }263 }
@@ -272,7 +273,7 @@ func TestIntegration_TwoAttachmentsReceiveSharedMutationPTYOutput(t *testing.T)
272 "VEV_TEST_ENV=first", "SHELL=" + firstShell, "XDG_RUNTIME_DIR=/run/first", "WAYLAND_DISPLAY=wayland-first",273 "VEV_TEST_ENV=first", "SHELL=" + firstShell, "XDG_RUNTIME_DIR=/run/first", "WAYLAND_DISPLAY=wayland-first",
273 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",274 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
274 }275 }
275- tr1, p1 := attachWithEnvironment(t, dir, ports.IntentNew, "environment", sz, firstEnv)276+ tr1, p1 := attachWithEnvironment(t, dir, protocol.IntentNew, "environment", sz, firstEnv)
276 defer func() { _ = tr1.Close() }()277 defer func() { _ = tr1.Close() }()
277 awaitText(t, p1, sz, "SHELL_COMMAND=first")278 awaitText(t, p1, sz, "SHELL_COMMAND=first")
278 assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")279 assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")
@@ -281,23 +282,23 @@ func TestIntegration_TwoAttachmentsReceiveSharedMutationPTYOutput(t *testing.T)
281 "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",282 "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",
282 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",283 "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
283 }284 }
284- tr2, p2 := attachWithEnvironment(t, dir, ports.IntentAttach, "environment", sz, secondEnv)285+ tr2, p2 := attachWithEnvironment(t, dir, protocol.IntentAttach, "environment", sz, secondEnv)
285 defer func() { _ = tr2.Close() }()286 defer func() { _ = tr2.Close() }()
286 assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")287 assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")
287 288
288 // The second attachment mutates shared tab state. Its newly opened PTY must289 // The second attachment mutates shared tab state. Its newly opened PTY must
289 // also be rendered to that attachment, not only to the coordinator primary.290 // also be rendered to that attachment, not only to the coordinator primary.
290- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))291+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
291 awaitText(t, p2, sz, "Commands")292 awaitText(t, p2, sz, "Commands")
292- require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))293+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("CNT\r")})}))
293 awaitText(t, p2, sz, "SHELL_COMMAND=second")294 awaitText(t, p2, sz, "SHELL_COMMAND=second")
294 295
295 // Each attachment owns its selected tab. Move the first attachment to the296 // Each attachment owns its selected tab. Move the first attachment to the
296 // new PTY before releasing its gate, so the fresh PTY output must reach both297 // new PTY before releasing its gate, so the fresh PTY output must reach both
297 // views rather than only being recovered by a later first paint.298 // views rather than only being recovered by a later first paint.
298- require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b2")})}))299+ require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b2")})}))
299 awaitText(t, p1, sz, "SHELL_COMMAND=second")300 awaitText(t, p1, sz, "SHELL_COMMAND=second")
300- require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("go\n")})}))301+ require.NoError(t, tr1.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("go\n")})}))
301 awaitText(t, p2, sz, sharedOutput)302 awaitText(t, p2, sz, sharedOutput)
302 awaitText(t, p1, sz, sharedOutput)303 awaitText(t, p1, sz, sharedOutput)
303 }304 }
@@ -306,7 +307,7 @@ func TestIntegration_AttachFirstOutput(t *testing.T) {
306 sz := domain.Size{Cols: 80, Rows: 24}307 sz := domain.Size{Cols: 80, Rows: 24}
307 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf HELLO; sleep 30"}))308 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf HELLO; sleep 30"}))
308 309
309- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)310+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
310 defer func() { _ = tr.Close() }()311 defer func() { _ = tr.Close() }()
311 312
312 awaitText(t, p, sz, "HELLO")313 awaitText(t, p, sz, "HELLO")
@@ -316,10 +317,10 @@ func TestIntegration_InputRoundtrip(t *testing.T) {
316 sz := domain.Size{Cols: 80, Rows: 24}317 sz := domain.Size{Cols: 80, Rows: 24}
317 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))318 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
318 319
319- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)320+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
320 defer func() { _ = tr.Close() }()321 defer func() { _ = tr.Close() }()
321 322
322- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("PINGPONG\n")})}))323+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("PINGPONG\n")})}))
323 awaitText(t, p, sz, "PINGPONG")324 awaitText(t, p, sz, "PINGPONG")
324 }325 }
325 326
@@ -327,14 +328,14 @@ func TestIntegration_CommandPaletteCreatesTab(t *testing.T) {
327 sz := domain.Size{Cols: 80, Rows: 24}328 sz := domain.Size{Cols: 80, Rows: 24}
328 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))329 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
329 330
330- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)331+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
331 defer func() { _ = tr.Close() }()332 defer func() { _ = tr.Close() }()
332 awaitText(t, p, sz, "READY")333 awaitText(t, p, sz, "READY")
333 334
334- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))335+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
335 awaitText(t, p, sz, "Commands")336 awaitText(t, p, sz, "Commands")
336 337
337- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))338+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("CNT\r")})}))
338 // Tab labels are enriched with the focused pane's title; with no process339 // Tab labels are enriched with the focused pane's title; with no process
339 // inspector wired in this test, that title falls back to the shell's340 // inspector wired in this test, that title falls back to the shell's
340 // basename ("sh").341 // basename ("sh").
@@ -346,25 +347,25 @@ func TestIntegration_CommandPaletteRenamesEphemeralSession(t *testing.T) {
346 sz := domain.Size{Cols: 80, Rows: 24}347 sz := domain.Size{Cols: 80, Rows: 24}
347 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))348 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
348 349
349- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)350+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
350 defer func() { _ = tr.Close() }()351 defer func() { _ = tr.Close() }()
351 text := awaitScreenText(t, p, sz, "READY")352 text := awaitScreenText(t, p, sz, "READY")
352 require.Contains(t, text, " 0* ")353 require.Contains(t, text, " 0* ")
353 354
354- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))355+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1b ")})}))
355 awaitText(t, p, sz, "Commands")356 awaitText(t, p, sz, "Commands")
356 357
357- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("RNS\r")})}))358+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("RNS\r")})}))
358 awaitText(t, p, sz, "Rename session")359 awaitText(t, p, sz, "Rename session")
359 360
360- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x7fwork\r")})}))361+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x7fwork\r")})}))
361 text = awaitScreenText(t, p, sz, " work ")362 text = awaitScreenText(t, p, sz, " work ")
362 require.NotContains(t, text, "work*")363 require.NotContains(t, text, "work*")
363 364
364 listTr, err := ipc.DialContext(context.Background(), dir)365 listTr, err := ipc.DialContext(context.Background(), dir)
365 require.NoError(t, err)366 require.NoError(t, err)
366 defer func() { _ = listTr.Close() }()367 defer func() { _ = listTr.Close() }()
367- require.NoError(t, listTr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}))368+ require.NoError(t, listTr.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})}))
368 f, err := listTr.Recv()369 f, err := listTr.Recv()
369 require.NoError(t, err)370 require.NoError(t, err)
370 require.Equal(t, ports.MsgSessions, f.Type)371 require.Equal(t, ports.MsgSessions, f.Type)
@@ -379,12 +380,12 @@ func TestIntegration_AltCWithoutPaletteDoesNotCreateTab(t *testing.T) {
379 sz := domain.Size{Cols: 80, Rows: 24}380 sz := domain.Size{Cols: 80, Rows: 24}
380 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))381 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf READY; sleep 30"}))
381 382
382- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)383+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
383 defer func() { _ = tr.Close() }()384 defer func() { _ = tr.Close() }()
384 text := awaitScreenText(t, p, sz, "READY")385 text := awaitScreenText(t, p, sz, "READY")
385 require.NotContains(t, text, " 1 2 ")386 require.NotContains(t, text, " 1 2 ")
386 387
387- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1bc")})}))388+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("\x1bc")})}))
388 assertNoTextAfterInput(t, p, sz, " 1 2 ")389 assertNoTextAfterInput(t, p, sz, " 1 2 ")
389 }390 }
390 391
@@ -392,7 +393,7 @@ func TestIntegration_EphemeralSurvivesDetachAndReattaches(t *testing.T) {
392 sz := domain.Size{Cols: 80, Rows: 24}393 sz := domain.Size{Cols: 80, Rows: 24}
393 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf MARKER; sleep 30"}))394 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf MARKER; sleep 30"}))
394 395
395- tr1, p1 := attach(t, dir, ports.IntentEphemeral, "", sz)396+ tr1, p1 := attach(t, dir, protocol.IntentEphemeral, "", sz)
396 awaitText(t, p1, sz, "MARKER")397 awaitText(t, p1, sz, "MARKER")
397 require.NoError(t, tr1.Close())398 require.NoError(t, tr1.Close())
398 399
@@ -402,7 +403,7 @@ func TestIntegration_EphemeralSurvivesDetachAndReattaches(t *testing.T) {
402 case <-time.After(300 * time.Millisecond):403 case <-time.After(300 * time.Millisecond):
403 }404 }
404 405
405- tr2, p2 := attach(t, dir, ports.IntentAttach, "0", sz)406+ tr2, p2 := attach(t, dir, protocol.IntentAttach, "0", sz)
406 defer func() { _ = tr2.Close() }()407 defer func() { _ = tr2.Close() }()
407 awaitText(t, p2, sz, "MARKER")408 awaitText(t, p2, sz, "MARKER")
408 409
@@ -423,7 +424,7 @@ func TestIntegration_EphemeralNotListedAfterDaemonRestart(t *testing.T) {
423 dir := ipc.SocketDir()424 dir := ipc.SocketDir()
424 425
425 _, served := startDaemonInDir(t, dir, daemon.WithShell("/bin/sh", []string{"-c", "printf TEMP; sleep 30"}))426 _, served := startDaemonInDir(t, dir, daemon.WithShell("/bin/sh", []string{"-c", "printf TEMP; sleep 30"}))
426- tr, p := attach(t, dir, ports.IntentEphemeral, "", sz)427+ tr, p := attach(t, dir, protocol.IntentEphemeral, "", sz)
427 awaitText(t, p, sz, "TEMP")428 awaitText(t, p, sz, "TEMP")
428 require.NoError(t, tr.Close())429 require.NoError(t, tr.Close())
429 430
@@ -452,7 +453,7 @@ func TestIntegration_NamedSurvivesReattach(t *testing.T) {
452 sz := domain.Size{Cols: 80, Rows: 24}453 sz := domain.Size{Cols: 80, Rows: 24}
453 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf MARKER; sleep 30"}))454 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf MARKER; sleep 30"}))
454 455
455- tr1, p1 := attach(t, dir, ports.IntentNew, "work", sz)456+ tr1, p1 := attach(t, dir, protocol.IntentNew, "work", sz)
456 awaitText(t, p1, sz, "MARKER")457 awaitText(t, p1, sz, "MARKER")
457 458
458 // Detach: a named session must survive.459 // Detach: a named session must survive.
@@ -466,7 +467,7 @@ func TestIntegration_NamedSurvivesReattach(t *testing.T) {
466 }467 }
467 468
468 // Re-attach: the first paint must reproduce the retained screen state.469 // Re-attach: the first paint must reproduce the retained screen state.
469- tr2, p2 := attach(t, dir, ports.IntentAttach, "work", sz)470+ tr2, p2 := attach(t, dir, protocol.IntentAttach, "work", sz)
470 defer func() { _ = tr2.Close() }()471 defer func() { _ = tr2.Close() }()
471 awaitText(t, p2, sz, "MARKER")472 awaitText(t, p2, sz, "MARKER")
472 }473 }
@@ -800,8 +801,8 @@ func publishRestorableCheckpoint(t *testing.T, stateDir string, repository *snap
800 served := make(chan error, 1)801 served := make(chan error, 1)
801 go func() { served <- d.Serve(ctx, listener) }()802 go func() { served <- d.Serve(ctx, listener) }()
802 803
803- tr, _ := attach(t, runtimeDir, ports.IntentNew, name, domain.Size{Cols: 80, Rows: 24})804+ tr, _ := attach(t, runtimeDir, protocol.IntentNew, name, domain.Size{Cols: 80, Rows: 24})
804- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("checkpoint me\n")})}))805+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("checkpoint me\n")})}))
805 require.Eventually(t, func() bool {806 require.Eventually(t, func() bool {
806 record, ok, _ := opened.Catalogue.Record(name)807 record, ok, _ := opened.Catalogue.Record(name)
807 return ok && record.Committed != nil && record.DegradedReason == ""808 return ok && record.Committed != nil && record.DegradedReason == ""
@@ -886,8 +887,8 @@ func runBlockedSnapshotWriterShutdown(t *testing.T) blockedSnapshotWriterShutdow
886 }()887 }()
887 888
888 awaitLifecycleStage(t, listenerReady, "daemon listener")889 awaitLifecycleStage(t, listenerReady, "daemon listener")
889- tr, _ := attach(t, runtimeDir, ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24})890+ tr, _ := attach(t, runtimeDir, protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24})
890- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("dirty state\n")})}))891+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte("dirty state\n")})}))
891 awaitLifecycleStage(t, repository.entered, "snapshot publication")892 awaitLifecycleStage(t, repository.entered, "snapshot publication")
892 require.NoError(t, tr.Close())893 require.NoError(t, tr.Close())
893 require.Eventually(t, func() bool {894 require.Eventually(t, func() bool {
@@ -1249,15 +1250,15 @@ func TestIntegration_KillAllShutsDownDaemon(t *testing.T) {
1249 sz := domain.Size{Cols: 80, Rows: 24}1250 sz := domain.Size{Cols: 80, Rows: 24}
1250 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "sleep 30"}))1251 dir, served := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "sleep 30"}))
1251 1252
1252- tr1, _ := attach(t, dir, ports.IntentNew, "one", sz)1253+ tr1, _ := attach(t, dir, protocol.IntentNew, "one", sz)
1253 require.NoError(t, tr1.Close())1254 require.NoError(t, tr1.Close())
1254- tr2, _ := attach(t, dir, ports.IntentNew, "two", sz)1255+ tr2, _ := attach(t, dir, protocol.IntentNew, "two", sz)
1255 require.NoError(t, tr2.Close())1256 require.NoError(t, tr2.Close())
1256 1257
1257 killTr, err := ipc.DialContext(context.Background(), dir)1258 killTr, err := ipc.DialContext(context.Background(), dir)
1258 require.NoError(t, err)1259 require.NoError(t, err)
1259 defer func() { _ = killTr.Close() }()1260 defer func() { _ = killTr.Close() }()
1260- require.NoError(t, killTr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})}))1261+ require.NoError(t, killTr.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})}))
1261 1262
1262 _, err = killTr.Recv()1263 _, err = killTr.Recv()
1263 require.ErrorIs(t, err, io.EOF)1264 require.ErrorIs(t, err, io.EOF)
M internal/app/final_acceptance_test.go +12 -12
@@ -15,7 +15,7 @@ import (
15 15
16 func sendAcceptanceInput(t *testing.T, tr ports.Transport, data string) {16 func sendAcceptanceInput(t *testing.T, tr ports.Transport, data string) {
17 t.Helper()17 t.Helper()
18- require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})}))18+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(data)})}))
19 }19 }
20 20
21 func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate func(ports.Frame) bool) ports.Frame {21 func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate func(ports.Frame) bool) ports.Frame {
@@ -35,7 +35,7 @@ func awaitAcceptanceFrame(t *testing.T, p *pump, typ ports.MsgType, predicate fu
35 }35 }
36 }36 }
37 37
38-func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) ports.CommandResult {38+func awaitAcceptanceCommand(t *testing.T, p *pump, requestID uint64) protocol.CommandResult {
39 t.Helper()39 t.Helper()
40 frame := awaitAcceptanceFrame(t, p, ports.MsgCommandResult, func(frame ports.Frame) bool {40 frame := awaitAcceptanceFrame(t, p, ports.MsgCommandResult, func(frame ports.Frame) bool {
41 result, err := ports.UnmarshalCommandResult(frame.Payload)41 result, err := ports.UnmarshalCommandResult(frame.Payload)
@@ -59,10 +59,10 @@ func awaitAcceptanceOutput(t *testing.T, p *pump, predicate func(protocol.Output
59 return output59 return output
60 }60 }
61 61
62-func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) ports.CommandResult {62+func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, requestID uint64, slug string) protocol.CommandResult {
63 t.Helper()63 t.Helper()
64- payload, err := ports.MarshalCommandRequest(ports.CommandRequest{64+ payload, err := ports.MarshalCommandRequest(protocol.CommandRequest{
65- Version: ports.ProtocolVersion, RequestID: requestID, Attached: true, Slug: slug,65+ Version: protocol.Version, RequestID: requestID, Attached: true, Slug: slug,
66 })66 })
67 require.NoError(t, err)67 require.NoError(t, err)
68 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgCommand, Payload: payload}))68 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgCommand, Payload: payload}))
@@ -72,8 +72,8 @@ func awaitAcceptanceCommandResult(t *testing.T, tr ports.Transport, p *pump, req
72 func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {72 func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {
73 size := domain.Size{Cols: 80, Rows: 24}73 size := domain.Size{Cols: 80, Rows: 24}
74 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))74 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
75- first, firstPump := attach(t, dir, ports.IntentNew, "shared", size)75+ first, firstPump := attach(t, dir, protocol.IntentNew, "shared", size)
76- second, secondPump := attach(t, dir, ports.IntentAttach, "shared", size)76+ second, secondPump := attach(t, dir, protocol.IntentAttach, "shared", size)
77 t.Cleanup(func() { _ = first.Close() })77 t.Cleanup(func() { _ = first.Close() })
78 t.Cleanup(func() { _ = second.Close() })78 t.Cleanup(func() { _ = second.Close() })
79 79
@@ -121,12 +121,12 @@ func TestAcceptanceTwoLocalAttachmentsKeepViewsOverTransports(t *testing.T) {
121 require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size)121 require.Equal(t, domain.Size{Cols: 100, Rows: 30}, resized.Size)
122 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")122 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
123 123
124- 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(protocol.OutputResetRequest{})}))
125 reset := awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Base == 0 && output.Full })125 reset := awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Base == 0 && output.Full })
126 require.True(t, reset.Full)126 require.True(t, reset.Full)
127 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")127 assertNoTextAfterInput(t, secondPump, size, "FIRST_PANE_INPUT")
128 128
129- require.NoError(t, first.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}))129+ require.NoError(t, first.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})}))
130 deadline := time.NewTimer(5 * time.Second)130 deadline := time.NewTimer(5 * time.Second)
131 defer deadline.Stop()131 defer deadline.Stop()
132 for {132 for {
@@ -149,8 +149,8 @@ detached:
149 func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {149 func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {
150 size := domain.Size{Cols: 80, Rows: 24}150 size := domain.Size{Cols: 80, Rows: 24}
151 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))151 dir, _ := startDaemon(t, daemon.WithShell("/bin/cat", nil))
152- first, firstPump := attach(t, dir, ports.IntentNew, "commands", size)152+ first, firstPump := attach(t, dir, protocol.IntentNew, "commands", size)
153- second, secondPump := attach(t, dir, ports.IntentAttach, "commands", size)153+ second, secondPump := attach(t, dir, protocol.IntentAttach, "commands", size)
154 t.Cleanup(func() { _ = first.Close() })154 t.Cleanup(func() { _ = first.Close() })
155 t.Cleanup(func() { _ = second.Close() })155 t.Cleanup(func() { _ = second.Close() })
156 156
@@ -173,7 +173,7 @@ func TestAcceptanceAttachedCommandUsesItsConnectionOnly(t *testing.T) {
173 }173 }
174 174
175 drained:175 drained:
176- 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(protocol.OutputResetRequest{})}))
177 awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Full && output.Base == 0 })177 awaitAcceptanceOutput(t, firstPump, func(output protocol.Output) bool { return output.Full && output.Base == 0 })
178 assertNoTextAfterInput(t, secondPump, size, "Commands")178 assertNoTextAfterInput(t, secondPump, size, "Commands")
179 }179 }
M internal/app/remote_final_acceptance_test.go +11 -11
@@ -23,7 +23,7 @@ import (
23 type acceptanceRemoteFactory struct {23 type acceptanceRemoteFactory struct {
24 mu sync.Mutex24 mu sync.Mutex
25 calls []string25 calls []string
26- handoff map[string]ports.AttachTarget26+ handoff map[string]protocol.AttachTarget
27 outputs chan string27 outputs chan string
28 }28 }
29 29
@@ -37,7 +37,7 @@ func (f *acceptanceRemoteFactory) DialerForRemote(target string, _ string, _ por
37 37
38 type acceptanceRemoteDialer struct {38 type acceptanceRemoteDialer struct {
39 target string39 target string
40- handoff ports.AttachTarget40+ handoff protocol.AttachTarget
41 outputs chan<- string41 outputs chan<- string
42 }42 }
43 43
@@ -49,7 +49,7 @@ func (d acceptanceRemoteDialer) Dial(context.Context) (ports.Transport, error) {
49 return clientTransport, nil49 return clientTransport, nil
50 }50 }
51 51
52-func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.AttachTarget, outputs chan<- string) {52+func serveAcceptanceRemote(tr ports.Transport, target string, handoff protocol.AttachTarget, outputs chan<- string) {
53 defer func() { _ = tr.Close() }()53 defer func() { _ = tr.Close() }()
54 welcomed := false54 welcomed := false
55 for {55 for {
@@ -61,7 +61,7 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta
61 if frame.Type != ports.MsgHello {61 if frame.Type != ports.MsgHello {
62 continue62 continue
63 }63 }
64- if err := tr.Send(ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: target})}); err != nil {64+ if err := tr.Send(ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: target})}); err != nil {
65 return65 return
66 }66 }
67 welcomed = true67 welcomed = true
@@ -88,7 +88,7 @@ func serveAcceptanceRemote(tr ports.Transport, target string, handoff ports.Atta
88 if err := tr.Send(ports.Frame{Type: ports.MsgOutput, Payload: payload}); err != nil {88 if err := tr.Send(ports.Frame{Type: ports.MsgOutput, Payload: payload}); err != nil {
89 return89 return
90 }90 }
91- _ = tr.Send(ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})})91+ _ = tr.Send(ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})})
92 for {92 for {
93 if _, err := tr.Recv(); err != nil {93 if _, err := tr.Recv(); err != nil {
94 return94 return
@@ -139,7 +139,7 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
139 result := client.NewRunner(deps).Run(ctx, request)139 result := client.NewRunner(deps).Run(ctx, request)
140 if request.SessionName == "work" {140 if request.SessionName == "work" {
141 require.NoError(t, result, "the client handoff completes before the test injects the composition-root handoff")141 require.NoError(t, result, "the client handoff completes before the test injects the composition-root handoff")
142- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: ports.IntentAttach}}142+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: protocol.IntentAttach}}
143 }143 }
144 return result144 return result
145 },145 },
@@ -147,11 +147,11 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
147 147
148 wireTransport, err := (acceptanceRemoteDialer{148 wireTransport, err := (acceptanceRemoteDialer{
149 target: "picker-wire.example",149 target: "picker-wire.example",
150- handoff: ports.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: ports.IntentAttach},150+ handoff: protocol.AttachTarget{Endpoint: "picked.example", Session: "picked", Intent: protocol.IntentAttach},
151 }).Dial(context.Background())151 }).Dial(context.Background())
152 require.NoError(t, err)152 require.NoError(t, err)
153- require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{153+ require.NoError(t, wireTransport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
154- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24},154+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24},
155 })}))155 })}))
156 welcome, err := wireTransport.Recv()156 welcome, err := wireTransport.Recv()
157 require.NoError(t, err)157 require.NoError(t, err)
@@ -165,8 +165,8 @@ func TestAcceptanceRemoteDirectAndPickerUseOnlyRemoteTransports(t *testing.T) {
165 require.Equal(t, "picked.example", target.Endpoint)165 require.Equal(t, "picked.example", target.Endpoint)
166 require.NoError(t, wireTransport.Close())166 require.NoError(t, wireTransport.Close())
167 167
168- require.NoError(t, runAttachWithDeps(context.Background(), ports.IntentAttach, "direct", "direct.example", "", nil, deps))168+ require.NoError(t, runAttachWithDeps(context.Background(), protocol.IntentAttach, "direct", "direct.example", "", nil, deps))
169- require.NoError(t, runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "picker.example", "", nil, deps))169+ require.NoError(t, runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "picker.example", "", nil, deps))
170 factory.mu.Lock()170 factory.mu.Lock()
171 calls := append([]string(nil), factory.calls...)171 calls := append([]string(nil), factory.calls...)
172 factory.mu.Unlock()172 factory.mu.Unlock()
M internal/app/remote_hosts.go +10 -9
@@ -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/platform"15 "github.com/bnema/vev/internal/platform"
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 const (20 const (
@@ -27,7 +28,7 @@ type remoteHostDeps struct {
27 stateDir func() string28 stateDir func() string
28 store ports.RemoteHostStore29 store ports.RemoteHostStore
29 catalog ports.RemoteCatalogClient30 catalog ports.RemoteCatalogClient
30- localList func(context.Context) ([]ports.SessionInfo, error)31+ localList func(context.Context) ([]protocol.SessionInfo, error)
31 stdout io.Writer32 stdout io.Writer
32 }33 }
33 34
@@ -174,7 +175,7 @@ func listAllSessions(ctx context.Context, deps remoteHostDeps, hosts []domain.Re
174 }175 }
175 return err176 return err
176 }177 }
177- all := append([]ports.SessionInfo(nil), local...)178+ all := append([]protocol.SessionInfo(nil), local...)
178 var hostErrs []error179 var hostErrs []error
179 for _, host := range hosts {180 for _, host := range hosts {
180 if err := ctx.Err(); err != nil {181 if err := ctx.Err(); err != nil {
@@ -195,10 +196,10 @@ func listAllSessions(ctx context.Context, deps remoteHostDeps, hosts []domain.Re
195 return nil196 return nil
196 }197 }
197 198
198-func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) []ports.SessionInfo {199+func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) []protocol.SessionInfo {
199- out := make([]ports.SessionInfo, 0, len(sessions))200+ out := make([]protocol.SessionInfo, 0, len(sessions))
200 for _, session := range sessions {201 for _, session := range sessions {
201- info := ports.SessionInfo{202+ info := protocol.SessionInfo{
202 Name: domain.RemoteSessionDisplay(session.Name, host),203 Name: domain.RemoteSessionDisplay(session.Name, host),
203 Ephemeral: session.Ephemeral,204 Ephemeral: session.Ephemeral,
204 Tabs: ports.SaturateUint16(ports.CatalogTabCount(session)),205 Tabs: ports.SaturateUint16(ports.CatalogTabCount(session)),
@@ -206,14 +207,14 @@ func catalogSessionsAsInfo(host string, sessions []ports.RemoteCatalogSession) [
206 }207 }
207 switch session.State {208 switch session.State {
208 case ports.RemoteCatalogSessionUp:209 case ports.RemoteCatalogSessionUp:
209- info.State = ports.SessionUp210+ info.State = protocol.SessionUp
210 case ports.RemoteCatalogSessionDown:211 case ports.RemoteCatalogSessionDown:
211- info.State = ports.SessionDown212+ info.State = protocol.SessionDown
212 case ports.RemoteCatalogSessionBroken:213 case ports.RemoteCatalogSessionBroken:
213- info.State = ports.SessionBroken214+ info.State = protocol.SessionBroken
214 default:215 default:
215 slog.Debug("remote catalog session has unknown state", "host", host, "session", session.Name, "state", session.State)216 slog.Debug("remote catalog session has unknown state", "host", host, "session", session.Name, "state", session.State)
216- info.State = ports.SessionBroken217+ info.State = protocol.SessionBroken
217 }218 }
218 out = append(out, info)219 out = append(out, info)
219 }220 }
M internal/app/remote_test.go +15 -14
@@ -11,6 +11,7 @@ import (
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 portsmocks "github.com/bnema/vev/internal/ports/mocks"13 portsmocks "github.com/bnema/vev/internal/ports/mocks"
14+ "github.com/bnema/vev/internal/protocol"
14 "github.com/bnema/vev/internal/usecase/client"15 "github.com/bnema/vev/internal/usecase/client"
15 "github.com/stretchr/testify/mock"16 "github.com/stretchr/testify/mock"
16 "github.com/stretchr/testify/require"17 "github.com/stretchr/testify/require"
@@ -57,7 +58,7 @@ func TestDecodeSessionListErrorReply(t *testing.T) {
57 t.Run("valid", func(t *testing.T) {58 t.Run("valid", func(t *testing.T) {
58 _, err := decodeSessionListReply(ports.Frame{59 _, err := decodeSessionListReply(ports.Frame{
59 Type: ports.MsgError,60 Type: ports.MsgError,
60- Payload: ports.MarshalErrorMsg(ports.ErrorMsg{Text: "denied"}),61+ Payload: ports.MarshalErrorMsg(protocol.ErrorMsg{Text: "denied"}),
61 })62 })
62 require.EqualError(t, err, "vev: denied")63 require.EqualError(t, err, "vev: denied")
63 })64 })
@@ -193,8 +194,8 @@ func TestListAllSessionsInvariants(t *testing.T) {
193 localErr := errors.New("local release failed")194 localErr := errors.New("local release failed")
194 var out bytes.Buffer195 var out bytes.Buffer
195 err := listAllSessions(context.Background(), remoteHostDeps{196 err := listAllSessions(context.Background(), remoteHostDeps{
196- localList: func(context.Context) ([]ports.SessionInfo, error) {197+ localList: func(context.Context) ([]protocol.SessionInfo, error) {
197- return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, localErr198+ return []protocol.SessionInfo{{Name: "local", State: protocol.SessionUp}}, localErr
198 },199 },
199 stdout: &out,200 stdout: &out,
200 }, nil)201 }, nil)
@@ -215,8 +216,8 @@ func TestListAllSessionsInvariants(t *testing.T) {
215 var out bytes.Buffer216 var out bytes.Buffer
216 err := listAllSessions(ctx, remoteHostDeps{217 err := listAllSessions(ctx, remoteHostDeps{
217 catalog: catalog,218 catalog: catalog,
218- localList: func(context.Context) ([]ports.SessionInfo, error) {219+ localList: func(context.Context) ([]protocol.SessionInfo, error) {
219- return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil220+ return []protocol.SessionInfo{{Name: "local", State: protocol.SessionUp}}, nil
220 },221 },
221 stdout: &out,222 stdout: &out,
222 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})223 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -240,8 +241,8 @@ func TestListAllSessionsInvariants(t *testing.T) {
240 var out bytes.Buffer241 var out bytes.Buffer
241 err := listAllSessions(context.Background(), remoteHostDeps{242 err := listAllSessions(context.Background(), remoteHostDeps{
242 catalog: catalog,243 catalog: catalog,
243- localList: func(context.Context) ([]ports.SessionInfo, error) {244+ localList: func(context.Context) ([]protocol.SessionInfo, error) {
244- return []ports.SessionInfo{{Name: "local", State: ports.SessionUp}}, nil245+ return []protocol.SessionInfo{{Name: "local", State: protocol.SessionUp}}, nil
245 },246 },
246 stdout: &out,247 stdout: &out,
247 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})248 }, []domain.RemoteHost{{Target: "arch"}, {Target: "mule"}})
@@ -262,13 +263,13 @@ func TestCatalogSessionsAsInfoInvariants(t *testing.T) {
262 tests := []struct {263 tests := []struct {
263 name string264 name string
264 session ports.RemoteCatalogSession265 session ports.RemoteCatalogSession
265- wantState ports.SessionState266+ wantState protocol.SessionState
266 wantTabs uint16267 wantTabs uint16
267 }{268 }{
268- {name: "up", session: ports.RemoteCatalogSession{Name: "dev", State: "up", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionUp, wantTabs: 2},269+ {name: "up", session: ports.RemoteCatalogSession{Name: "dev", State: "up", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionUp, wantTabs: 2},
269- {name: "down", session: ports.RemoteCatalogSession{Name: "dev", State: "down", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionDown, wantTabs: 2},270+ {name: "down", session: ports.RemoteCatalogSession{Name: "dev", State: "down", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionDown, wantTabs: 2},
270- {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionBroken, wantTabs: 2},271+ {name: "broken", session: ports.RemoteCatalogSession{Name: "dev", State: "broken", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2},
271- {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: ports.SessionBroken, wantTabs: 2},272+ {name: "unknown fails closed", session: ports.RemoteCatalogSession{Name: "dev", State: "unknown", Tabs: []ports.RemoteCatalogTab{{}, {}}}, wantState: protocol.SessionBroken, wantTabs: 2},
272 }273 }
273 for _, tt := range tests {274 for _, tt := range tests {
274 t.Run(tt.name, func(t *testing.T) {275 t.Run(tt.name, func(t *testing.T) {
@@ -288,7 +289,7 @@ func TestRunAttachWithDepsRemoteLearning(t *testing.T) {
288 factory.EXPECT().DialerForRemote("build@mule", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()289 factory.EXPECT().DialerForRemote("build@mule", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
289 290
290 var learner ports.RemoteHostLearner291 var learner ports.RemoteHostLearner
291- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "build@mule", "", nil, runAttachDeps{292+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "build@mule", "", nil, runAttachDeps{
292 remoteDialerFactory: factory,293 remoteDialerFactory: factory,
293 hostStore: store,294 hostStore: store,
294 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {295 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {
@@ -307,7 +308,7 @@ func TestRunAttachWithDepsAlwaysLearnsRemoteHost(t *testing.T) {
307 factory := portsmocks.NewMockRemoteDialerFactory(t)308 factory := portsmocks.NewMockRemoteDialerFactory(t)
308 factory.EXPECT().DialerForRemote("arch", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()309 factory.EXPECT().DialerForRemote("arch", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
309 var learner ports.RemoteHostLearner310 var learner ports.RemoteHostLearner
310- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "arch", "", nil, runAttachDeps{311+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "arch", "", nil, runAttachDeps{
311 remoteDialerFactory: factory,312 remoteDialerFactory: factory,
312 hostStore: store,313 hostStore: store,
313 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {314 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {
M internal/app/run.go +28 -28
@@ -155,7 +155,7 @@ func Run(args []string) error {
155 // I/O.155 // I/O.
156 func parseArgs(args []string) (command, error) {156 func parseArgs(args []string) (command, error) {
157 if len(args) == 0 {157 if len(args) == 0 {
158- return command{kind: kindAttach, intent: ports.IntentEphemeral}, nil158+ return command{kind: kindAttach, intent: protocol.IntentEphemeral}, nil
159 }159 }
160 160
161 switch args[0] {161 switch args[0] {
@@ -193,7 +193,7 @@ func parseArgs(args []string) (command, error) {
193 if err := domain.ValidateSessionName(args[1]); err != nil {193 if err := domain.ValidateSessionName(args[1]); err != nil {
194 return command{}, err194 return command{}, err
195 }195 }
196- return command{kind: kindAttach, intent: ports.IntentNew, name: args[1]}, nil196+ return command{kind: kindAttach, intent: protocol.IntentNew, name: args[1]}, nil
197 case "attach", "a":197 case "attach", "a":
198 if len(args) < 2 || args[1] == "" {198 if len(args) < 2 || args[1] == "" {
199 return command{}, usagef("`attach` requires a session name")199 return command{}, usagef("`attach` requires a session name")
@@ -201,7 +201,7 @@ func parseArgs(args []string) (command, error) {
201 if len(args) > 2 {201 if len(args) > 2 {
202 return command{}, usagef("`attach` accepts exactly one session name or remote target")202 return command{}, usagef("`attach` accepts exactly one session name or remote target")
203 }203 }
204- cmd := command{kind: kindAttach, intent: ports.IntentAttach, name: args[1]}204+ cmd := command{kind: kindAttach, intent: protocol.IntentAttach, name: args[1]}
205 if target, session, ok := parseRemoteAttachTarget(args[1]); ok {205 if target, session, ok := parseRemoteAttachTarget(args[1]); ok {
206 if err := domain.ValidateRemoteHostTarget(target); err != nil {206 if err := domain.ValidateRemoteHostTarget(target); err != nil {
207 return command{}, err207 return command{}, err
@@ -214,7 +214,7 @@ func parseArgs(args []string) (command, error) {
214 cmd.remoteTarget = target214 cmd.remoteTarget = target
215 cmd.name = session215 cmd.name = session
216 if session == "" {216 if session == "" {
217- cmd.intent = ports.IntentEphemeral217+ cmd.intent = protocol.IntentEphemeral
218 }218 }
219 }219 }
220 return cmd, nil220 return cmd, nil
@@ -828,8 +828,8 @@ func remoteTransportModeFromEnv(value string) (ports.RemoteTransportMode, error)
828 }828 }
829 }829 }
830 830
831-func validateRemoteAttachHandoff(target ports.AttachTarget) error {831+func validateRemoteAttachHandoff(target protocol.AttachTarget) error {
832- if err := ports.ValidateAttachTarget(target); err != nil {832+ if err := protocol.ValidateAttachTarget(target); err != nil {
833 return err833 return err
834 }834 }
835 if err := domain.ValidateRemoteHostTarget(target.Endpoint); err != nil {835 if err := domain.ValidateRemoteHostTarget(target.Endpoint); err != nil {
@@ -839,7 +839,7 @@ func validateRemoteAttachHandoff(target ports.AttachTarget) error {
839 return err839 return err
840 }840 }
841 if target.RemoteTarget != nil {841 if target.RemoteTarget != nil {
842- if target.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {842+ if target.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
843 return errors.New("remote picker handoff must use daemon-owned environment")843 return errors.New("remote picker handoff must use daemon-owned environment")
844 }844 }
845 if target.RemoteTarget.Endpoint != target.Endpoint || target.RemoteTarget.SessionName != target.Session {845 if target.RemoteTarget.Endpoint != target.Endpoint || target.RemoteTarget.SessionName != target.Session {
@@ -874,7 +874,7 @@ const maxAttachTargetHandoffs = 32
874 874
875 func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, activeSession string, log *slog.Logger, deps runAttachDeps) error {875 func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, activeSession string, log *slog.Logger, deps runAttachDeps) error {
876 if activeSession != "" {876 if activeSession != "" {
877- if remoteTarget == "" && intent == ports.IntentNew {877+ if remoteTarget == "" && intent == protocol.IntentNew {
878 return deps.createDetached(ctx, name)878 return deps.createDetached(ctx, name)
879 }879 }
880 return errors.New("vev: sessions should be nested with care; unset VEV to force")880 return errors.New("vev: sessions should be nested with care; unset VEV to force")
@@ -891,7 +891,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
891 mode, modeErr := remoteTransportModeFromEnv(deps.selectedRemoteTransport)891 mode, modeErr := remoteTransportModeFromEnv(deps.selectedRemoteTransport)
892 factory := deps.remoteDialerFactory892 factory := deps.remoteDialerFactory
893 var remoteSelection *domain.RemoteSessionTarget893 var remoteSelection *domain.RemoteSessionTarget
894- var remoteEnvironmentPolicy ports.EnvironmentPolicy894+ var remoteEnvironmentPolicy protocol.EnvironmentPolicy
895 remoteDisplayOrigin := domain.RemoteDisplayOrigin(remoteTarget)895 remoteDisplayOrigin := domain.RemoteDisplayOrigin(remoteTarget)
896 routeOrigin := protocol.RouteOriginLocal896 routeOrigin := protocol.RouteOriginLocal
897 routeOriginKey := "local"897 routeOriginKey := "local"
@@ -906,13 +906,13 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
906 }906 }
907 }907 }
908 pickerHandoff := remoteTarget == ""908 pickerHandoff := remoteTarget == ""
909- pickerEnvironmentPolicy := func(target *domain.RemoteSessionTarget, policy ports.EnvironmentPolicy) ports.EnvironmentPolicy {909+ pickerEnvironmentPolicy := func(target *domain.RemoteSessionTarget, policy protocol.EnvironmentPolicy) protocol.EnvironmentPolicy {
910 if pickerHandoff && target == nil {910 if pickerHandoff && target == nil {
911- return ports.EnvironmentPolicyDaemonOwned911+ return protocol.EnvironmentPolicyDaemonOwned
912 }912 }
913 return policy913 return policy
914 }914 }
915- handoff := func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {915+ handoff := func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
916 if err := validateRemoteAttachHandoff(target); err != nil {916 if err := validateRemoteAttachHandoff(target); err != nil {
917 return nil, client.AttachRequest{}, fmt.Errorf("vev: invalid remote attach handoff: %w", err)917 return nil, client.AttachRequest{}, fmt.Errorf("vev: invalid remote attach handoff: %w", err)
918 }918 }
@@ -1051,11 +1051,11 @@ func (d localDaemonDialer) Dial(ctx context.Context) (ports.Transport, error) {
1051 return ensureDaemonWithLifecycle(ctx, d.dir, dial, realSpawn, defaultBackoff)1051 return ensureDaemonWithLifecycle(ctx, d.dir, dial, realSpawn, defaultBackoff)
1052 }1052 }
1053 1053
1054-func detachedLocalHello(name, cwd string) ports.Hello {1054+func detachedLocalHello(name, cwd string) protocol.Hello {
1055 termEnv := os.Getenv("TERM")1055 termEnv := os.Getenv("TERM")
1056- return ports.Hello{1056+ return protocol.Hello{
1057- Version: ports.ProtocolVersion,1057+ Version: protocol.Version,
1058- Intent: ports.IntentNew,1058+ Intent: protocol.IntentNew,
1059 Name: name,1059 Name: name,
1060 Size: domain.Size{Cols: 80, Rows: 24},1060 Size: domain.Size{Cols: 80, Rows: 24},
1061 TermEnv: termEnv,1061 TermEnv: termEnv,
@@ -1126,7 +1126,7 @@ func requestDaemonStop(ctx context.Context) error {
1126 return errors.Join(errors.New("vev: no daemon running"), owner.Release())1126 return errors.Join(errors.New("vev: no daemon running"), owner.Release())
1127 }1127 }
1128 defer func() { _ = transport.Close() }()1128 defer func() { _ = transport.Close() }()
1129- if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})}); err != nil {1129+ if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})}); err != nil {
1130 return fmt.Errorf("vev: requesting daemon stop: %w", err)1130 return fmt.Errorf("vev: requesting daemon stop: %w", err)
1131 }1131 }
1132 if _, err := transport.Recv(); err != nil && !errors.Is(err, io.EOF) {1132 if _, err := transport.Recv(); err != nil && !errors.Is(err, io.EOF) {
@@ -1456,7 +1456,7 @@ func runList(ctx context.Context, cmd command) (retErr error) {
1456 return nil1456 return nil
1457 }1457 }
1458 1458
1459-func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error) {1459+func listLocalSessions(ctx context.Context) (_ []protocol.SessionInfo, retErr error) {
1460 transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff)1460 transport, owner, err := waitForDaemonOrLifecycle(ctx, ipc.SocketDir(), realDial, defaultBackoff)
1461 if err != nil {1461 if err != nil {
1462 return nil, fmt.Errorf("vev: waiting for durable session state: %w", err)1462 return nil, fmt.Errorf("vev: waiting for durable session state: %w", err)
@@ -1470,19 +1470,19 @@ func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error
1470 }1470 }
1471 return nil, fmt.Errorf("vev: reading stored sessions: %w", loadErr)1471 return nil, fmt.Errorf("vev: reading stored sessions: %w", loadErr)
1472 }1472 }
1473- infos := make([]ports.SessionInfo, 0, len(records))1473+ infos := make([]protocol.SessionInfo, 0, len(records))
1474 for _, r := range records {1474 for _, r := range records {
1475- state := ports.SessionDown1475+ state := protocol.SessionDown
1476 if r.DegradedReason != "" {1476 if r.DegradedReason != "" {
1477- state = ports.SessionBroken1477+ state = protocol.SessionBroken
1478 }1478 }
1479- infos = append(infos, ports.SessionInfo{Name: r.Name, State: state})1479+ infos = append(infos, protocol.SessionInfo{Name: r.Name, State: state})
1480 }1480 }
1481 return infos, nil1481 return infos, nil
1482 }1482 }
1483 defer func() { _ = transport.Close() }()1483 defer func() { _ = transport.Close() }()
1484 1484
1485- if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}); err != nil {1485+ if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})}); err != nil {
1486 return nil, fmt.Errorf("vev: requesting session list: %w", err)1486 return nil, fmt.Errorf("vev: requesting session list: %w", err)
1487 }1487 }
1488 reply, err := transport.Recv()1488 reply, err := transport.Recv()
@@ -1492,7 +1492,7 @@ func listLocalSessions(ctx context.Context) (_ []ports.SessionInfo, retErr error
1492 return decodeSessionListReply(reply)1492 return decodeSessionListReply(reply)
1493 }1493 }
1494 1494
1495-func decodeSessionListReply(reply ports.Frame) ([]ports.SessionInfo, error) {1495+func decodeSessionListReply(reply ports.Frame) ([]protocol.SessionInfo, error) {
1496 if reply.Type == ports.MsgError {1496 if reply.Type == ports.MsgError {
1497 em, err := ports.UnmarshalErrorMsg(reply.Payload)1497 em, err := ports.UnmarshalErrorMsg(reply.Payload)
1498 if err != nil {1498 if err != nil {
@@ -1511,7 +1511,7 @@ func decodeSessionListReply(reply ports.Frame) ([]ports.SessionInfo, error) {
1511 }1511 }
1512 1512
1513 // printSessions renders a session table (or a friendly note when empty).1513 // printSessions renders a session table (or a friendly note when empty).
1514-func printSessions(w io.Writer, sessions []ports.SessionInfo) {1514+func printSessions(w io.Writer, sessions []protocol.SessionInfo) {
1515 if len(sessions) == 0 {1515 if len(sessions) == 0 {
1516 _, _ = fmt.Fprintln(w, "no sessions")1516 _, _ = fmt.Fprintln(w, "no sessions")
1517 return1517 return
@@ -1523,10 +1523,10 @@ func printSessions(w io.Writer, sessions []ports.SessionInfo) {
1523 tabs := fmt.Sprintf("%d", s.Tabs)1523 tabs := fmt.Sprintf("%d", s.Tabs)
1524 attached := "no"1524 attached := "no"
1525 switch s.State {1525 switch s.State {
1526- case ports.SessionDown:1526+ case protocol.SessionDown:
1527 state = "down"1527 state = "down"
1528 tabs = "-"1528 tabs = "-"
1529- case ports.SessionBroken:1529+ case protocol.SessionBroken:
1530 state = "broken"1530 state = "broken"
1531 tabs = "-"1531 tabs = "-"
1532 default:1532 default:
@@ -1596,7 +1596,7 @@ func runKill(ctx context.Context, name string, all, daemon bool) (retErr error)
1596 }1596 }
1597 defer func() { _ = transport.Close() }()1597 defer func() { _ = transport.Close() }()
1598 1598
1599- if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{Name: name, All: all || daemon})}); err != nil {1599+ if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{Name: name, All: all || daemon})}); err != nil {
1600 return fmt.Errorf("vev: requesting kill: %w", err)1600 return fmt.Errorf("vev: requesting kill: %w", err)
1601 }1601 }
1602 reply, err := transport.Recv()1602 reply, err := transport.Recv()
M internal/app/run_test.go +62 -62
@@ -437,20 +437,20 @@ func TestParseArgs(t *testing.T) {
437 wantErr bool437 wantErr bool
438 nonUsageErr bool438 nonUsageErr bool
439 }{439 }{
440- {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},440+ {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral},
441- {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},441+ {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral},
442- {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: ports.IntentNew, wantName: "work"},442+ {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: protocol.IntentNew, wantName: "work"},
443 {name: "new without name", args: []string{"new"}, wantErr: true},443 {name: "new without name", args: []string{"new"}, wantErr: true},
444 {name: "new empty name", args: []string{"new", ""}, wantErr: true},444 {name: "new empty name", args: []string{"new", ""}, wantErr: true},
445 {name: "new command override unsupported", args: []string{"new", "work", "--", "sh"}, wantErr: true},445 {name: "new command override unsupported", args: []string{"new", "work", "--", "sh"}, wantErr: true},
446- {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},446+ {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work"},
447- {name: "attach preserves legacy unsafe name", args: []string{"attach", "my work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "my work"},447+ {name: "attach preserves legacy unsafe name", args: []string{"attach", "my work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "my work"},
448- {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},448+ {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work"},
449- {name: "attach remote host uses ephemeral", args: []string{"attach", "user@example.com"}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral, wantRemote: "user@example.com"},449+ {name: "attach remote host uses ephemeral", args: []string{"attach", "user@example.com"}, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral, wantRemote: "user@example.com"},
450- {name: "attach remote host with empty session uses ephemeral", args: []string{"attach", "user@example.com:"}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral, wantRemote: "user@example.com"},450+ {name: "attach remote host with empty session uses ephemeral", args: []string{"attach", "user@example.com:"}, wantKind: kindAttach, wantIntent: protocol.IntentEphemeral, wantRemote: "user@example.com"},
451- {name: "attach remote host with session", args: []string{"attach", "user@example.com:work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work", wantRemote: "user@example.com"},451+ {name: "attach remote host with session", args: []string{"attach", "user@example.com:work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work", wantRemote: "user@example.com"},
452- {name: "attach remote ipv4 with session", args: []string{"attach", "user@192.0.2.10:work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work", wantRemote: "user@192.0.2.10"},452+ {name: "attach remote ipv4 with session", args: []string{"attach", "user@192.0.2.10:work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work", wantRemote: "user@192.0.2.10"},
453- {name: "attach remote ipv6 with session", args: []string{"attach", "user@[2001:db8::1]:work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work", wantRemote: "user@[2001:db8::1]"},453+ {name: "attach remote ipv6 with session", args: []string{"attach", "user@[2001:db8::1]:work"}, wantKind: kindAttach, wantIntent: protocol.IntentAttach, wantName: "work", wantRemote: "user@[2001:db8::1]"},
454 {name: "attach remote invalid session", args: []string{"attach", "user@example.com:my work"}, wantErr: true, nonUsageErr: true},454 {name: "attach remote invalid session", args: []string{"attach", "user@example.com:my work"}, wantErr: true, nonUsageErr: true},
455 {name: "attach extra arg", args: []string{"attach", "work", "extra"}, wantErr: true},455 {name: "attach extra arg", args: []string{"attach", "work", "extra"}, wantErr: true},
456 {name: "attach without name", args: []string{"attach"}, wantErr: true},456 {name: "attach without name", args: []string{"attach"}, wantErr: true},
@@ -566,10 +566,10 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
566 arg string566 arg string
567 want client.AttachRequest567 want client.AttachRequest
568 }{568 }{
569- {name: "local host", arg: "work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},569+ {name: "local host", arg: "work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
570- {name: "remote host", arg: "user@example.com:work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},570+ {name: "remote host", arg: "user@example.com:work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
571- {name: "remote ipv4", arg: "user@192.0.2.10:work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},571+ {name: "remote ipv4", arg: "user@192.0.2.10:work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
572- {name: "remote ipv6", arg: "user@[2001:db8::1]:work", want: client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}},572+ {name: "remote ipv6", arg: "user@[2001:db8::1]:work", want: client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}},
573 }573 }
574 for _, tt := range tests {574 for _, tt := range tests {
575 t.Run(tt.name, func(t *testing.T) {575 t.Run(tt.name, func(t *testing.T) {
@@ -587,10 +587,10 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
587 587
588 func TestListShowsBroken(t *testing.T) {588 func TestListShowsBroken(t *testing.T) {
589 var out bytes.Buffer589 var out bytes.Buffer
590- printSessions(&out, []ports.SessionInfo{590+ printSessions(&out, []protocol.SessionInfo{
591- {Name: "fresh", State: ports.SessionDown},591+ {Name: "fresh", State: protocol.SessionDown},
592- {Name: "loading", State: ports.SessionDown},592+ {Name: "loading", State: protocol.SessionDown},
593- {Name: "broken", State: ports.SessionBroken},593+ {Name: "broken", State: protocol.SessionBroken},
594 })594 })
595 require.Contains(t, out.String(), "fresh")595 require.Contains(t, out.String(), "fresh")
596 require.Contains(t, out.String(), "down")596 require.Contains(t, out.String(), "down")
@@ -602,9 +602,9 @@ func TestListShowsBroken(t *testing.T) {
602 602
603 func TestPrintSessionsShowsDownState(t *testing.T) {603 func TestPrintSessionsShowsDownState(t *testing.T) {
604 var out bytes.Buffer604 var out bytes.Buffer
605- printSessions(&out, []ports.SessionInfo{605+ printSessions(&out, []protocol.SessionInfo{
606- {Name: "main", State: ports.SessionUp, Tabs: 2, Attached: true},606+ {Name: "main", State: protocol.SessionUp, Tabs: 2, Attached: true},
607- {Name: "old", State: ports.SessionDown},607+ {Name: "old", State: protocol.SessionDown},
608 })608 })
609 got := out.String()609 got := out.String()
610 for _, want := range []string{"NAME", "STATE", "main", "up", "2", "yes", "old", "down", "-"} {610 for _, want := range []string{"NAME", "STATE", "main", "up", "2", "yes", "old", "down", "-"} {
@@ -616,10 +616,10 @@ func TestPrintSessionsShowsDownState(t *testing.T) {
616 616
617 func TestPrintSessionsMarksEphemeral(t *testing.T) {617 func TestPrintSessionsMarksEphemeral(t *testing.T) {
618 var buf bytes.Buffer618 var buf bytes.Buffer
619- printSessions(&buf, []ports.SessionInfo{619+ printSessions(&buf, []protocol.SessionInfo{
620- {Name: "0", State: ports.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},620+ {Name: "0", State: protocol.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
621- {Name: "work", State: ports.SessionUp, Tabs: 2, Attached: true},621+ {Name: "work", State: protocol.SessionUp, Tabs: 2, Attached: true},
622- {Name: "old", State: ports.SessionDown},622+ {Name: "old", State: protocol.SessionDown},
623 })623 })
624 out := buf.String()624 out := buf.String()
625 for _, want := range []string{"0", "temporary", "work", "up", "old", "down"} {625 for _, want := range []string{"0", "temporary", "work", "up", "old", "down"} {
@@ -757,7 +757,7 @@ func captureStdout(t *testing.T, fn func()) string {
757 757
758 func TestRunAttachRejectsNestedVEVBeforeDial(t *testing.T) {758 func TestRunAttachRejectsNestedVEVBeforeDial(t *testing.T) {
759 called := false759 called := false
760- err := runAttachWithDeps(context.Background(), ports.IntentEphemeral, "", "", "outer", nil, runAttachDeps{760+ err := runAttachWithDeps(context.Background(), protocol.IntentEphemeral, "", "", "outer", nil, runAttachDeps{
761 createDetached: func(context.Context, string) error {761 createDetached: func(context.Context, string) error {
762 called = true762 called = true
763 return nil763 return nil
@@ -776,7 +776,7 @@ func TestRunAttachRejectsNestedVEVBeforeDial(t *testing.T) {
776 776
777 func TestRunAttachNestedNewCreatesDetachedSession(t *testing.T) {777 func TestRunAttachNestedNewCreatesDetachedSession(t *testing.T) {
778 var gotName string778 var gotName string
779- err := runAttachWithDeps(context.Background(), ports.IntentNew, "scratch", "", "outer", nil, runAttachDeps{779+ err := runAttachWithDeps(context.Background(), protocol.IntentNew, "scratch", "", "outer", nil, runAttachDeps{
780 createDetached: func(_ context.Context, name string) error {780 createDetached: func(_ context.Context, name string) error {
781 gotName = name781 gotName = name
782 return nil782 return nil
@@ -796,7 +796,7 @@ func TestDetachedLocalHelloIncludesTrueColor(t *testing.T) {
796 796
797 hello := detachedLocalHello("scratch", "/tmp/work")797 hello := detachedLocalHello("scratch", "/tmp/work")
798 798
799- require.Equal(t, ports.IntentNew, hello.Intent)799+ require.Equal(t, protocol.IntentNew, hello.Intent)
800 require.Equal(t, "scratch", hello.Name)800 require.Equal(t, "scratch", hello.Name)
801 require.Equal(t, "xterm-direct", hello.TermEnv)801 require.Equal(t, "xterm-direct", hello.TermEnv)
802 require.Equal(t, "/tmp/work", hello.Cwd)802 require.Equal(t, "/tmp/work", hello.Cwd)
@@ -834,7 +834,7 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
834 factory := portsmocks.NewMockRemoteDialerFactory(t)834 factory := portsmocks.NewMockRemoteDialerFactory(t)
835 factory.EXPECT().DialerForRemote("remote.example", "work", tt.wantMode, mock.Anything).Return(namedDialer{name: "remote"}, nil)835 factory.EXPECT().DialerForRemote("remote.example", "work", tt.wantMode, mock.Anything).Return(namedDialer{name: "remote"}, nil)
836 836
837- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{837+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
838 remoteDialerFactory: factory,838 remoteDialerFactory: factory,
839 selectedRemoteTransport: tt.selectedTransport,839 selectedRemoteTransport: tt.selectedTransport,
840 clipboard: clip,840 clipboard: clip,
@@ -845,11 +845,11 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
845 }845 }
846 gotDialer = nd.name846 gotDialer = nd.name
847 gotRemote = deps.Remote847 gotRemote = deps.Remote
848- if !request.Remote || request.EnvironmentPolicy != ports.EnvironmentPolicyClientOwned {848+ if !request.Remote || request.EnvironmentPolicy != protocol.EnvironmentPolicyClientOwned {
849 t.Fatal("direct CLI remote attach must preserve client-owned environment policy")849 t.Fatal("direct CLI remote attach must preserve client-owned environment policy")
850 }850 }
851 gotClipboard = deps.Clipboard851 gotClipboard = deps.Clipboard
852- if request.Intent != ports.IntentAttach || request.SessionName != "work" {852+ if request.Intent != protocol.IntentAttach || request.SessionName != "work" {
853 t.Fatalf("intent/name = %d/%q, want attach/work", request.Intent, request.SessionName)853 t.Fatalf("intent/name = %d/%q, want attach/work", request.Intent, request.SessionName)
854 }854 }
855 return nil855 return nil
@@ -876,18 +876,18 @@ func TestRunAttachWithDepsDirectLegacyCallbackPreservesClientOwnership(t *testin
876 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once()876 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once()
877 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once()877 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once()
878 878
879- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{879+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
880 remoteDialerFactory: factory,880 remoteDialerFactory: factory,
881 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {881 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
882 require.True(t, request.Remote)882 require.True(t, request.Remote)
883- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)883+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
884- nextDialer, nextRequest, err := deps.AttachHandoff(ports.AttachTarget{884+ nextDialer, nextRequest, err := deps.AttachHandoff(protocol.AttachTarget{
885- Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach,885+ Endpoint: "selected.example", Session: "picked", Intent: protocol.IntentAttach,
886- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,886+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
887 })887 })
888 require.NoError(t, err)888 require.NoError(t, err)
889 require.NotNil(t, nextDialer)889 require.NotNil(t, nextDialer)
890- require.Equal(t, ports.EnvironmentPolicyClientOwned, nextRequest.EnvironmentPolicy)890+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, nextRequest.EnvironmentPolicy)
891 return nil891 return nil
892 },892 },
893 })893 })
@@ -899,15 +899,15 @@ func TestRunAttachWithDepsRemotePickerHandoffReopensDirectConnection(t *testing.
899 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once()899 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-1"}, nil).Once()
900 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once()900 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-2"}, nil).Once()
901 var calls int901 var calls int
902- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{902+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
903 remoteDialerFactory: factory,903 remoteDialerFactory: factory,
904 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {904 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
905 calls++905 calls++
906 require.NotNil(t, deps.Dialer)906 require.NotNil(t, deps.Dialer)
907 require.True(t, request.Remote)907 require.True(t, request.Remote)
908- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy, "direct CLI remote ownership must survive a handoff")908+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy, "direct CLI remote ownership must survive a handoff")
909 if calls == 1 {909 if calls == 1 {
910- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach}}910+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: protocol.IntentAttach}}
911 }911 }
912 return nil912 return nil
913 },913 },
@@ -921,11 +921,11 @@ func TestRunAttachWithDepsAllowsRevisitingRemoteHandoff(t *testing.T) {
921 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()921 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
922 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-revisit"}, nil).Once()922 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote-revisit"}, nil).Once()
923 923
924- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{924+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
925 remoteDialerFactory: factory,925 remoteDialerFactory: factory,
926 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {926 runClient: func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {
927- dialer, request, err := deps.AttachHandoff(ports.AttachTarget{927+ dialer, request, err := deps.AttachHandoff(protocol.AttachTarget{
928- Endpoint: "remote.example", Session: "work", Intent: ports.IntentAttach,928+ Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach,
929 })929 })
930 require.NoError(t, err)930 require.NoError(t, err)
931 require.NotNil(t, dialer)931 require.NotNil(t, dialer)
@@ -941,11 +941,11 @@ func TestRunAttachWithDepsBoundsRepeatedAttachTargetHandoffs(t *testing.T) {
941 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Times(maxAttachTargetHandoffs + 1)941 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Times(maxAttachTargetHandoffs + 1)
942 942
943 calls := 0943 calls := 0
944- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{944+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
945 remoteDialerFactory: factory,945 remoteDialerFactory: factory,
946 runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error {946 runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error {
947 calls++947 calls++
948- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: ports.IntentAttach}}948+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}}
949 },949 },
950 })950 })
951 require.ErrorContains(t, err, "attach handoff exceeded maximum")951 require.ErrorContains(t, err, "attach handoff exceeded maximum")
@@ -957,18 +957,18 @@ func TestRunAttachWithDepsLocalPickerHandoffAttachesSelectedRemote(t *testing.T)
957 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()957 factory.EXPECT().DialerForRemote("selected.example", "picked", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
958 958
959 localCalls, remoteCalls := 0, 0959 localCalls, remoteCalls := 0, 0
960- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "", "", nil, runAttachDeps{960+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "", "", nil, runAttachDeps{
961 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },961 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },
962 remoteDialerFactory: factory,962 remoteDialerFactory: factory,
963 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {963 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
964 if deps.Remote {964 if deps.Remote {
965 remoteCalls++965 remoteCalls++
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+ require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "picked", Remote: true, Origin: protocol.RouteOriginDiscovery, OriginKey: "selected.example", HostLabel: "selected.example", EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, request)
967 return nil967 return nil
968 }968 }
969 localCalls++969 localCalls++
970- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local"}, request)970+ require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local"}, request)
971- return &client.AttachTargetError{Target: ports.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: ports.IntentAttach}}971+ return &client.AttachTargetError{Target: protocol.AttachTarget{Endpoint: "selected.example", Session: "picked", Intent: protocol.IntentAttach}}
972 },972 },
973 })973 })
974 require.NoError(t, err)974 require.NoError(t, err)
@@ -979,20 +979,20 @@ func TestRunAttachWithDepsLocalPickerHandoffAttachesSelectedRemote(t *testing.T)
979 func TestRunAttachWithDepsRejectsInvalidHandoffBeforeDialing(t *testing.T) {979 func TestRunAttachWithDepsRejectsInvalidHandoffBeforeDialing(t *testing.T) {
980 tests := []struct {980 tests := []struct {
981 name string981 name string
982- target ports.AttachTarget982+ target protocol.AttachTarget
983 }{983 }{
984- {name: "invalid host", target: ports.AttachTarget{Endpoint: "remote host", Session: "work", Intent: ports.IntentAttach}},984+ {name: "invalid host", target: protocol.AttachTarget{Endpoint: "remote host", Session: "work", Intent: protocol.IntentAttach}},
985- {name: "invalid session", target: ports.AttachTarget{Endpoint: "remote.example", Session: "bad name", Intent: ports.IntentAttach}},985+ {name: "invalid session", target: protocol.AttachTarget{Endpoint: "remote.example", Session: "bad name", Intent: protocol.IntentAttach}},
986- {name: "tokenless resume", target: ports.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: ports.IntentResume}},986+ {name: "tokenless resume", target: protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentResume}},
987 {987 {
988 name: "tokenless resume with remote target",988 name: "tokenless resume with remote target",
989- target: ports.AttachTarget{989+ target: protocol.AttachTarget{
990- Endpoint: "remote.example", Session: "work", Intent: ports.IntentResume,990+ Endpoint: "remote.example", Session: "work", Intent: protocol.IntentResume,
991 RemoteTarget: &domain.RemoteSessionTarget{991 RemoteTarget: &domain.RemoteSessionTarget{
992 Endpoint: "remote.example", DisplayOrigin: "remote.example",992 Endpoint: "remote.example", DisplayOrigin: "remote.example",
993 LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work", LiveTabID: "tab-1",993 LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work", LiveTabID: "tab-1",
994 },994 },
995- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,995+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
996 },996 },
997 },997 },
998 }998 }
@@ -1001,7 +1001,7 @@ func TestRunAttachWithDepsRejectsInvalidHandoffBeforeDialing(t *testing.T) {
1001 factory := portsmocks.NewMockRemoteDialerFactory(t)1001 factory := portsmocks.NewMockRemoteDialerFactory(t)
1002 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()1002 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(namedDialer{name: "remote"}, nil).Once()
1003 calls := 01003 calls := 0
1004- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{1004+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
1005 remoteDialerFactory: factory,1005 remoteDialerFactory: factory,
1006 runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error {1006 runClient: func(_ context.Context, _ client.Dependencies, _ client.AttachRequest) error {
1007 calls++1007 calls++
@@ -1018,7 +1018,7 @@ func TestRunAttachWithDepsRejectsInvalidRemoteTransportBeforeDialing(t *testing.
1018 factory := portsmocks.NewMockRemoteDialerFactory(t)1018 factory := portsmocks.NewMockRemoteDialerFactory(t)
1019 runClientCalled := false1019 runClientCalled := false
1020 1020
1021- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{1021+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
1022 remoteDialerFactory: factory,1022 remoteDialerFactory: factory,
1023 selectedRemoteTransport: "serial",1023 selectedRemoteTransport: "serial",
1024 runClient: func(context.Context, client.Dependencies, client.AttachRequest) error {1024 runClient: func(context.Context, client.Dependencies, client.AttachRequest) error {
@@ -1040,7 +1040,7 @@ func TestRunAttachWithDepsReturnsFactoryErrorBeforeRunClient(t *testing.T) {
1040 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(nil, factoryErr)1040 factory.EXPECT().DialerForRemote("remote.example", "work", ports.RemoteTransportUDP, mock.Anything).Return(nil, factoryErr)
1041 runClientCalled := false1041 runClientCalled := false
1042 1042
1043- err := runAttachWithDeps(context.Background(), ports.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{1043+ err := runAttachWithDeps(context.Background(), protocol.IntentAttach, "work", "remote.example", "", nil, runAttachDeps{
1044 remoteDialerFactory: factory,1044 remoteDialerFactory: factory,
1045 runClient: func(context.Context, client.Dependencies, client.AttachRequest) error {1045 runClient: func(context.Context, client.Dependencies, client.AttachRequest) error {
1046 runClientCalled = true1046 runClientCalled = true
@@ -1060,7 +1060,7 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
1060 gotRemote := true1060 gotRemote := true
1061 gotClipboard := ports.ClipboardReader(&fakeClipboardReader{})1061 gotClipboard := ports.ClipboardReader(&fakeClipboardReader{})
1062 factory := portsmocks.NewMockRemoteDialerFactory(t)1062 factory := portsmocks.NewMockRemoteDialerFactory(t)
1063- err := runAttachWithDeps(context.Background(), ports.IntentEphemeral, "", "", "", nil, runAttachDeps{1063+ err := runAttachWithDeps(context.Background(), protocol.IntentEphemeral, "", "", "", nil, runAttachDeps{
1064 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },1064 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },
1065 remoteDialerFactory: factory,1065 remoteDialerFactory: factory,
1066 clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach1066 clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach
@@ -1075,7 +1075,7 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
1075 t.Fatal("local carriage metadata must not be in the attach request")1075 t.Fatal("local carriage metadata must not be in the attach request")
1076 }1076 }
1077 gotClipboard = deps.Clipboard1077 gotClipboard = deps.Clipboard
1078- if request.Intent != ports.IntentEphemeral || request.SessionName != "" {1078+ if request.Intent != protocol.IntentEphemeral || request.SessionName != "" {
1079 t.Fatalf("intent/name = %d/%q, want ephemeral/empty", request.Intent, request.SessionName)1079 t.Fatalf("intent/name = %d/%q, want ephemeral/empty", request.Intent, request.SessionName)
1080 }1080 }
1081 return nil1081 return nil
M internal/app/transport_observability_test.go +3 -2
@@ -15,6 +15,7 @@ import (
15 15
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/mock"20 "github.com/stretchr/testify/mock"
20 )21 )
@@ -189,7 +190,7 @@ func TestRunAttachJoinsTraceCloseError(t *testing.T) {
189 t.Setenv("XDG_RUNTIME_DIR", t.TempDir())190 t.Setenv("XDG_RUNTIME_DIR", t.TempDir())
190 t.Setenv(envRemoteTransport, "invalid")191 t.Setenv(envRemoteTransport, "invalid")
191 192
192- err := runAttach(context.Background(), ports.IntentEphemeral, "", "host")193+ err := runAttach(context.Background(), protocol.IntentEphemeral, "", "host")
193 if !strings.Contains(err.Error(), "invalid remote transport") {194 if !strings.Contains(err.Error(), "invalid remote transport") {
194 t.Fatalf("runAttach() error = %v, want remote transport validation error", err)195 t.Fatalf("runAttach() error = %v, want remote transport validation error", err)
195 }196 }
@@ -246,7 +247,7 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) {
246 }247 }
247 t.Cleanup(func() { runClientWithDeps = originalRunClient })248 t.Cleanup(func() { runClientWithDeps = originalRunClient })
248 249
249- if err := runAttach(context.Background(), ports.IntentAttach, "work", "remote.example"); err != nil {250+ if err := runAttach(context.Background(), protocol.IntentAttach, "work", "remote.example"); err != nil {
250 t.Fatal(err)251 t.Fatal(err)
251 }252 }
252 if traceCalls != 1 || factoryCalls != 1 {253 if traceCalls != 1 || factoryCalls != 1 {
M internal/persist/catalogue_test.go +2 -2
@@ -9,7 +9,7 @@ import (
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
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 "github.com/bnema/vev/pkg/kv"13 "github.com/bnema/vev/pkg/kv"
14 "github.com/bnema/vev/pkg/safedir"14 "github.com/bnema/vev/pkg/safedir"
15 )15 )
@@ -57,7 +57,7 @@ func TestOpenOrCreateReportsRecordsFromOlderProtocol(t *testing.T) {
57 TabNames: []string{"shell", "logs"},57 TabNames: []string{"shell", "logs"},
58 Committed: &domain.CheckpointRef{Generation: 3, ManifestDigest: [32]byte{3}},58 Committed: &domain.CheckpointRef{Generation: 3, ManifestDigest: [32]byte{3}},
59 }59 }
60- olderProtocol, err := encodeRecordValueForProtocol(record, ports.ProtocolVersion-1)60+ olderProtocol, err := encodeRecordValueForProtocol(record, protocol.Version-1)
61 require.NoError(t, err)61 require.NoError(t, err)
62 protocolLess, err := encodeRecordValue(record)62 protocolLess, err := encodeRecordValue(record)
63 require.NoError(t, err)63 require.NoError(t, err)
M internal/persist/persist.go +2 -1
@@ -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 "github.com/bnema/vev/pkg/kv"14 "github.com/bnema/vev/pkg/kv"
14 )15 )
15 16
@@ -454,7 +455,7 @@ func (p *Persister) loadIncompatibleRecords() ([]domain.CatalogueRecord, error)
454 decodeErr = err455 decodeErr = err
455 return false456 return false
456 }457 }
457- if stored.protocolVersion != ports.ProtocolVersion {458+ if stored.protocolVersion != protocol.Version {
458 records = append(records, stored.record)459 records = append(records, stored.record)
459 }460 }
460 return true461 return true
M internal/persist/record.go +2 -2
@@ -7,7 +7,7 @@ import (
7 "math"7 "math"
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/protocol"
11 )11 )
12 12
13 const (13 const (
@@ -24,7 +24,7 @@ var catalogueMagic = [4]byte{'V', 'E', 'V', 'C'}
24 type Record = domain.CatalogueRecord24 type Record = domain.CatalogueRecord
25 25
26 func encodeRecordValue(record domain.CatalogueRecord) ([]byte, error) {26 func encodeRecordValue(record domain.CatalogueRecord) ([]byte, error) {
27- return encodeRecordValueForProtocol(record, ports.ProtocolVersion)27+ return encodeRecordValueForProtocol(record, protocol.Version)
28 }28 }
29 29
30 func encodeRecordValueForProtocol(record domain.CatalogueRecord, protocolVersion uint16) ([]byte, error) {30 func encodeRecordValueForProtocol(record domain.CatalogueRecord, protocolVersion uint16) ([]byte, error) {
M internal/ports/frame_test.go +4 -2
@@ -2,11 +2,13 @@ package ports
2 2
3 import (3 import (
4 "testing"4 "testing"
5+
6+ "github.com/bnema/vev/internal/protocol"
5 )7 )
6 8
7 func TestProtocolVersion(t *testing.T) {9 func TestProtocolVersion(t *testing.T) {
8- if ProtocolVersion != 37 {10+ if protocol.Version != 37 {
9- t.Fatalf("ProtocolVersion = %d, want 37", ProtocolVersion)11+ t.Fatalf("ProtocolVersion = %d, want 37", protocol.Version)
10 }12 }
11 }13 }
12 14
M internal/ports/navigation_test.go +14 -14
@@ -133,36 +133,36 @@ func TestParkedRouteWireRoundTripsStrictly(t *testing.T) {
133 }133 }
134 134
135 func TestHelloNavigationValidationTable(t *testing.T) {135 func TestHelloNavigationValidationTable(t *testing.T) {
136- base := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}136+ base := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
137 remoteTarget := &domain.RemoteSessionTarget{137 remoteTarget := &domain.RemoteSessionTarget{
138 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: domain.SessionLifecycleID{1},138 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: domain.SessionLifecycleID{1},
139 SessionName: "work", LiveTabID: "tab-1",139 SessionName: "work", LiveTabID: "tab-1",
140 }140 }
141 tests := []struct {141 tests := []struct {
142 name string142 name string
143- hello Hello143+ hello protocol.Hello
144 valid bool144 valid bool
145 capability protocol.NavigationCapabilities145 capability protocol.NavigationCapabilities
146 overlay protocol.StartupOverlay146 overlay protocol.StartupOverlay
147 wantPayload string147 wantPayload string
148 }{148 }{
149 {name: "ordinary", hello: base, valid: true},149 {name: "ordinary", hello: base, valid: true},
150- {name: "resume route", hello: Hello{Version: ProtocolVersion, Intent: IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true},150+ {name: "resume route", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentResume, Size: domain.Size{Cols: 80, Rows: 24}}, valid: true},
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: "daemon-owned home picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.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: "remote-target home picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityHomePicker, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, valid: true, capability: protocol.NavigationCapabilityHomePicker},
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: "client-owned home picker rejected", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, 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: "daemon-owned back picker rejected", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: protocol.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: "remote-target back picker rejected", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: remoteTarget, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, valid: false},
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: "back with startup picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.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"},
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: "new rejects navigation", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack, StartupOverlay: protocol.StartupOverlaySessionPicker}, 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: "unknown capability", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: 4}, 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: "back without picker", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, NavigationCapabilities: protocol.NavigationCapabilityBack}, 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+ {name: "picker without back", hello: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
161 }161 }
162 162
163 for _, tt := range tests {163 for _, tt := range tests {
164 t.Run(tt.name, func(t *testing.T) {164 t.Run(tt.name, func(t *testing.T) {
165- err := ValidateHello(tt.hello)165+ err := protocol.ValidateHello(tt.hello)
166 if tt.valid {166 if tt.valid {
167 require.NoError(t, err)167 require.NoError(t, err)
168 payload := MarshalHello(tt.hello)168 payload := MarshalHello(tt.hello)
M internal/ports/remote.go +3 -3
@@ -193,8 +193,8 @@ var (
193 // ValidateRemoteCatalog applies the exact current schema and all bounds before193 // ValidateRemoteCatalog applies the exact current schema and all bounds before
194 // a catalogue is cached or rendered.194 // a catalogue is cached or rendered.
195 func ValidateRemoteCatalog(c RemoteCatalog) error {195 func ValidateRemoteCatalog(c RemoteCatalog) error {
196- if c.ProtocolVersion != ProtocolVersion {196+ if c.ProtocolVersion != protocol.Version {
197- return &RemoteCatalogVersionMismatchError{Got: c.ProtocolVersion, Want: ProtocolVersion, Kind: "protocol"}197+ return &RemoteCatalogVersionMismatchError{Got: c.ProtocolVersion, Want: protocol.Version, Kind: "protocol"}
198 }198 }
199 if c.SchemaVersion != RemoteCatalogSchemaVersion {199 if c.SchemaVersion != RemoteCatalogSchemaVersion {
200 return &RemoteCatalogVersionMismatchError{Got: c.SchemaVersion, Want: RemoteCatalogSchemaVersion, Kind: "catalog"}200 return &RemoteCatalogVersionMismatchError{Got: c.SchemaVersion, Want: RemoteCatalogSchemaVersion, Kind: "catalog"}
@@ -309,7 +309,7 @@ func ValidateRemoteCatalogCacheEntries(entries []RemoteCatalogCacheEntry) error
309 return fmt.Errorf("%w: duplicate host", ErrInvalidRemoteCatalog)309 return fmt.Errorf("%w: duplicate host", ErrInvalidRemoteCatalog)
310 }310 }
311 seenHosts[entry.Host] = struct{}{}311 seenHosts[entry.Host] = struct{}{}
312- catalog := RemoteCatalog{ProtocolVersion: ProtocolVersion, SchemaVersion: RemoteCatalogSchemaVersion, Sessions: entry.Sessions}312+ catalog := RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: RemoteCatalogSchemaVersion, Sessions: entry.Sessions}
313 if err := ValidateRemoteCatalog(catalog); err != nil {313 if err := ValidateRemoteCatalog(catalog); err != nil {
314 return fmt.Errorf("%w: host %q: %v", ErrInvalidRemoteCatalog, entry.Host, err)314 return fmt.Errorf("%w: host %q: %v", ErrInvalidRemoteCatalog, entry.Host, err)
315 }315 }
M internal/ports/remote_preview_hardening_test.go +6 -6
@@ -178,7 +178,7 @@ func TestRemoteTargetWireRejectsInvalidSelectorCombinations(t *testing.T) {
178 t.Run(test.name, func(t *testing.T) {178 t.Run(test.name, func(t *testing.T) {
179 target := *testRemoteTarget(false)179 target := *testRemoteTarget(false)
180 test.mutate(&target)180 test.mutate(&target)
181- message := AttachTarget{Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach, RemoteTarget: &target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned}181+ message := protocol.AttachTarget{Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach, RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
182 if MarshalAttachTarget(message) != nil {182 if MarshalAttachTarget(message) != nil {
183 t.Fatal("invalid target was marshaled")183 t.Fatal("invalid target was marshaled")
184 }184 }
@@ -188,13 +188,13 @@ func TestRemoteTargetWireRejectsInvalidSelectorCombinations(t *testing.T) {
188 188
189 func TestRemoteTargetWireRichGoldenRoundTrip(t *testing.T) {189 func TestRemoteTargetWireRichGoldenRoundTrip(t *testing.T) {
190 target := testRemoteTarget(false)190 target := testRemoteTarget(false)
191- got := MarshalAttachTarget(AttachTarget{191+ got := MarshalAttachTarget(protocol.AttachTarget{
192- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,192+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
193- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,193+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
194 })194 })
195 want := []byte{195 want := []byte{
196 0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',196 0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
197- 0x00, 0x04, 'w', 'o', 'r', 'k', IntentAttach,197+ 0x00, 0x04, 'w', 'o', 'r', 'k', protocol.IntentAttach,
198 0x01,198 0x01,
199 0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',199 0x00, 0x0f, 'b', 'u', 'i', 'l', 'd', '@', 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
200 0x00, 0x09, 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',200 0x00, 0x09, 'm', 'u', 'l', 'e', ':', '2', '2', '2', '2',
@@ -210,7 +210,7 @@ func TestRemoteTargetWireRichGoldenRoundTrip(t *testing.T) {
210 if err != nil {210 if err != nil {
211 t.Fatalf("UnmarshalAttachTarget() error = %v", err)211 t.Fatalf("UnmarshalAttachTarget() error = %v", err)
212 }212 }
213- if decoded.RemoteTarget == nil || *decoded.RemoteTarget != *target || decoded.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {213+ if decoded.RemoteTarget == nil || *decoded.RemoteTarget != *target || decoded.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
214 t.Fatalf("decoded target = %#v, policy = %d", decoded.RemoteTarget, decoded.EnvironmentPolicy)214 t.Fatalf("decoded target = %#v, policy = %d", decoded.RemoteTarget, decoded.EnvironmentPolicy)
215 }215 }
216 }216 }
M internal/ports/remote_state_test.go +5 -4
@@ -3,6 +3,7 @@ package ports
3 import (3 import (
4 "testing"4 "testing"
5 5
6+ "github.com/bnema/vev/internal/protocol"
6 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
7 )8 )
8 9
@@ -12,7 +13,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
12 for _, state := range []RemoteCatalogSessionState{RemoteCatalogSessionUp, RemoteCatalogSessionDown, RemoteCatalogSessionBroken} {13 for _, state := range []RemoteCatalogSessionState{RemoteCatalogSessionUp, RemoteCatalogSessionDown, RemoteCatalogSessionBroken} {
13 t.Run(string(state), func(t *testing.T) {14 t.Run(string(state), func(t *testing.T) {
14 err := ValidateRemoteCatalog(RemoteCatalog{15 err := ValidateRemoteCatalog(RemoteCatalog{
15- ProtocolVersion: ProtocolVersion,16+ ProtocolVersion: protocol.Version,
16 SchemaVersion: RemoteCatalogSchemaVersion,17 SchemaVersion: RemoteCatalogSchemaVersion,
17 Sessions: []RemoteCatalogSession{{18 Sessions: []RemoteCatalogSession{{
18 LifecycleID: [16]byte{1}, Name: "work", State: state,19 LifecycleID: [16]byte{1}, Name: "work", State: state,
@@ -24,7 +25,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
24 }25 }
25 26
26 err := ValidateRemoteCatalog(RemoteCatalog{27 err := ValidateRemoteCatalog(RemoteCatalog{
27- ProtocolVersion: ProtocolVersion,28+ ProtocolVersion: protocol.Version,
28 SchemaVersion: RemoteCatalogSchemaVersion,29 SchemaVersion: RemoteCatalogSchemaVersion,
29 Sessions: []RemoteCatalogSession{{30 Sessions: []RemoteCatalogSession{{
30 LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionState("running"),31 LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionState("running"),
@@ -33,7 +34,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
33 })34 })
34 require.ErrorIs(t, err, ErrRemoteCatalogUnknownState)35 require.ErrorIs(t, err, ErrRemoteCatalogUnknownState)
35 36
36- err = ValidateRemoteCatalog(RemoteCatalog{ProtocolVersion: ProtocolVersion, Sessions: []RemoteCatalogSession{}})37+ err = ValidateRemoteCatalog(RemoteCatalog{ProtocolVersion: protocol.Version, Sessions: []RemoteCatalogSession{}})
37 var mismatch *RemoteCatalogVersionMismatchError38 var mismatch *RemoteCatalogVersionMismatchError
38 require.ErrorAs(t, err, &mismatch)39 require.ErrorAs(t, err, &mismatch)
39 require.Equal(t, "catalog", mismatch.Kind)40 require.Equal(t, "catalog", mismatch.Kind)
@@ -42,7 +43,7 @@ func TestRemoteCatalogStateContract(t *testing.T) {
42 43
43 func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) {44 func TestValidateRemoteCatalogRejectsDuplicateSessionNames(t *testing.T) {
44 err := ValidateRemoteCatalog(RemoteCatalog{45 err := ValidateRemoteCatalog(RemoteCatalog{
45- ProtocolVersion: ProtocolVersion,46+ ProtocolVersion: protocol.Version,
46 SchemaVersion: RemoteCatalogSchemaVersion,47 SchemaVersion: RemoteCatalogSchemaVersion,
47 Sessions: []RemoteCatalogSession{48 Sessions: []RemoteCatalogSession{
48 {LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionUp, Tabs: []RemoteCatalogTab{}},49 {LifecycleID: [16]byte{1}, Name: "work", State: RemoteCatalogSessionUp, Tabs: []RemoteCatalogTab{}},
M internal/ports/remote_target_wire_test.go +38 -37
@@ -6,6 +6,7 @@ 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 testRemoteTarget(stopped bool) *domain.RemoteSessionTarget {12 func testRemoteTarget(stopped bool) *domain.RemoteSessionTarget {
@@ -36,13 +37,13 @@ func TestRemoteTargetWireRoundTripPreservesExactSelector(t *testing.T) {
36 }37 }
37 t.Run(name, func(t *testing.T) {38 t.Run(name, func(t *testing.T) {
38 target := testRemoteTarget(stopped)39 target := testRemoteTarget(stopped)
39- msg := Hello{40+ msg := protocol.Hello{
40- Version: ProtocolVersion,41+ Version: protocol.Version,
41- Intent: IntentAttach,42+ Intent: protocol.IntentAttach,
42 Name: target.SessionName,43 Name: target.SessionName,
43 Size: domain.Size{Cols: 80, Rows: 24},44 Size: domain.Size{Cols: 80, Rows: 24},
44 RemoteTarget: target,45 RemoteTarget: target,
45- EnvironmentPolicy: EnvironmentPolicyDaemonOwned,46+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
46 }47 }
47 payload := MarshalHello(msg)48 payload := MarshalHello(msg)
48 if payload == nil {49 if payload == nil {
@@ -55,16 +56,16 @@ func TestRemoteTargetWireRoundTripPreservesExactSelector(t *testing.T) {
55 if got.RemoteTarget == nil || *got.RemoteTarget != *target {56 if got.RemoteTarget == nil || *got.RemoteTarget != *target {
56 t.Fatalf("remote target = %#v, want %#v", got.RemoteTarget, target)57 t.Fatalf("remote target = %#v, want %#v", got.RemoteTarget, target)
57 }58 }
58- if got.EnvironmentPolicy != EnvironmentPolicyDaemonOwned {59+ if got.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
59 t.Fatalf("environment policy = %d, want daemon-owned", got.EnvironmentPolicy)60 t.Fatalf("environment policy = %d, want daemon-owned", got.EnvironmentPolicy)
60 }61 }
61 62
62- attachPayload := MarshalAttachTarget(AttachTarget{63+ attachPayload := MarshalAttachTarget(protocol.AttachTarget{
63 Endpoint: target.Endpoint,64 Endpoint: target.Endpoint,
64 Session: target.SessionName,65 Session: target.SessionName,
65- Intent: IntentAttach,66+ Intent: protocol.IntentAttach,
66 RemoteTarget: target,67 RemoteTarget: target,
67- EnvironmentPolicy: EnvironmentPolicyDaemonOwned,68+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
68 })69 })
69 if attachPayload == nil {70 if attachPayload == nil {
70 t.Fatal("MarshalAttachTarget returned nil")71 t.Fatal("MarshalAttachTarget returned nil")
@@ -94,53 +95,53 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) {
94 {95 {
95 name: "hello",96 name: "hello",
96 validate: func() error {97 validate: func() error {
97- return ValidateHello(Hello{98+ return protocol.ValidateHello(protocol.Hello{
98- Version: ProtocolVersion, Intent: IntentAttach,99+ Version: protocol.Version, Intent: protocol.IntentAttach,
99 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},100 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
100- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,101+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
101 })102 })
102 },103 },
103 marshalBad: func() []byte {104 marshalBad: func() []byte {
104- return MarshalHello(Hello{105+ return MarshalHello(protocol.Hello{
105- Version: ProtocolVersion, Intent: IntentAttach,106+ Version: protocol.Version, Intent: protocol.IntentAttach,
106 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},107 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
107- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,108+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
108 })109 })
109 },110 },
110 marshalGood: func() []byte {111 marshalGood: func() []byte {
111- return MarshalHello(Hello{112+ return MarshalHello(protocol.Hello{
112- Version: ProtocolVersion, Intent: IntentAttach,113+ Version: protocol.Version, Intent: protocol.IntentAttach,
113 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},114 Name: target.SessionName, Size: domain.Size{Cols: 80, Rows: 24},
114- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,115+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
115 })116 })
116 },117 },
117 unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err },118 unmarshal: func(payload []byte) error { _, err := UnmarshalHello(payload); return err },
118 policyOffset: 10,119 policyOffset: 10,
119- wantErr: ErrInvalidHello,120+ wantErr: protocol.ErrInvalidHello,
120 },121 },
121 {122 {
122 name: "attach target",123 name: "attach target",
123 validate: func() error {124 validate: func() error {
124- return ValidateAttachTarget(AttachTarget{125+ return protocol.ValidateAttachTarget(protocol.AttachTarget{
125- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,126+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
126- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,127+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
127 })128 })
128 },129 },
129 marshalBad: func() []byte {130 marshalBad: func() []byte {
130- return MarshalAttachTarget(AttachTarget{131+ return MarshalAttachTarget(protocol.AttachTarget{
131- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,132+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
132- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyClientOwned,133+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
133 })134 })
134 },135 },
135 marshalGood: func() []byte {136 marshalGood: func() []byte {
136- return MarshalAttachTarget(AttachTarget{137+ return MarshalAttachTarget(protocol.AttachTarget{
137- Endpoint: target.Endpoint, Session: target.SessionName, Intent: IntentAttach,138+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
138- RemoteTarget: target, EnvironmentPolicy: EnvironmentPolicyDaemonOwned,139+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
139 })140 })
140 },141 },
141 unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err },142 unmarshal: func(payload []byte) error { _, err := UnmarshalAttachTarget(payload); return err },
142 policyOffset: 3,143 policyOffset: 3,
143- wantErr: ErrInvalidAttachTarget,144+ wantErr: protocol.ErrInvalidAttachTarget,
144 },145 },
145 }146 }
146 147
@@ -165,7 +166,7 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) {
165 if err := tt.unmarshal(trailing); err == nil {166 if err := tt.unmarshal(trailing); err == nil {
166 t.Fatal("trailing garbage unexpectedly decoded")167 t.Fatal("trailing garbage unexpectedly decoded")
167 }168 }
168- payload[len(payload)-tt.policyOffset] = byte(EnvironmentPolicyClientOwned)169+ payload[len(payload)-tt.policyOffset] = byte(protocol.EnvironmentPolicyClientOwned)
169 if err := tt.unmarshal(payload); !errors.Is(err, tt.wantErr) {170 if err := tt.unmarshal(payload); !errors.Is(err, tt.wantErr) {
170 t.Fatalf("mutated policy error = %v, want %v", err, tt.wantErr)171 t.Fatalf("mutated policy error = %v, want %v", err, tt.wantErr)
171 }172 }
@@ -174,19 +175,19 @@ func TestRemoteTargetWireRejectsClientOwnedEnvironmentPolicy(t *testing.T) {
174 }175 }
175 176
176 func TestAttachTargetRejectsResumeIntent(t *testing.T) {177 func TestAttachTargetRejectsResumeIntent(t *testing.T) {
177- tests := []AttachTarget{178+ tests := []protocol.AttachTarget{
178- {Endpoint: "host", Session: "work", Intent: IntentResume},179+ {Endpoint: "host", Session: "work", Intent: protocol.IntentResume},
179 {180 {
180 Endpoint: "build@mule:2222",181 Endpoint: "build@mule:2222",
181 Session: "work",182 Session: "work",
182- Intent: IntentResume,183+ Intent: protocol.IntentResume,
183 RemoteTarget: testRemoteTarget(false),184 RemoteTarget: testRemoteTarget(false),
184- EnvironmentPolicy: EnvironmentPolicyDaemonOwned,185+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
185 },186 },
186 }187 }
187 for _, target := range tests {188 for _, target := range tests {
188- if err := ValidateAttachTarget(target); !errors.Is(err, ErrInvalidAttachTarget) {189+ if err := protocol.ValidateAttachTarget(target); !errors.Is(err, protocol.ErrInvalidAttachTarget) {
189- t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, ErrInvalidAttachTarget)190+ t.Fatalf("ValidateAttachTarget(%#v) error = %v, want %v", target, err, protocol.ErrInvalidAttachTarget)
190 }191 }
191 if got := MarshalAttachTarget(target); got != nil {192 if got := MarshalAttachTarget(target); got != nil {
192 t.Fatalf("MarshalAttachTarget(%#v) = %x, want nil", target, got)193 t.Fatalf("MarshalAttachTarget(%#v) = %x, want nil", target, got)
@@ -195,8 +196,8 @@ func TestAttachTargetRejectsResumeIntent(t *testing.T) {
195 }196 }
196 197
197 func TestTargetWireIncludesRequiredV27Section(t *testing.T) {198 func TestTargetWireIncludesRequiredV27Section(t *testing.T) {
198- want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 0, 0}199+ want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', protocol.IntentAttach, 0, 0, 0, 0}
199- got := MarshalAttachTarget(AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach})200+ got := MarshalAttachTarget(protocol.AttachTarget{Endpoint: "host", Session: "work", Intent: protocol.IntentAttach})
200 if !bytes.Equal(got, want) {201 if !bytes.Equal(got, want) {
201 t.Fatalf("target bytes = %x, want %x", got, want)202 t.Fatalf("target bytes = %x, want %x", got, want)
202 }203 }
M internal/ports/routes_test.go +8 -8
@@ -33,9 +33,9 @@ func TestExactSessionTargetCodecIsStrict(t *testing.T) {
33 33
34 func TestHelloExactTargetRoundTrip(t *testing.T) {34 func TestHelloExactTargetRoundTrip(t *testing.T) {
35 target := testExactTarget()35 target := testExactTarget()
36- msg := Hello{36+ msg := protocol.Hello{
37- Version: ProtocolVersion,37+ Version: protocol.Version,
38- Intent: IntentAttach,38+ Intent: protocol.IntentAttach,
39 Name: target.SessionName,39 Name: target.SessionName,
40 Size: domain.Size{Cols: 80, Rows: 24},40 Size: domain.Size{Cols: 80, Rows: 24},
41 ExactTarget: &target,41 ExactTarget: &target,
@@ -49,12 +49,12 @@ func TestHelloExactTargetRoundTrip(t *testing.T) {
49 require.Equal(t, msg, got)49 require.Equal(t, msg, got)
50 50
51 invalid := msg51 invalid := msg
52- invalid.Intent = IntentNew52+ invalid.Intent = protocol.IntentNew
53- require.Error(t, ValidateHello(invalid))53+ require.Error(t, protocol.ValidateHello(invalid))
54 54
55 invalid = msg55 invalid = msg
56 invalid.PreferredTabID = "bad tab"56 invalid.PreferredTabID = "bad tab"
57- require.Error(t, ValidateHello(invalid))57+ require.Error(t, protocol.ValidateHello(invalid))
58 }58 }
59 59
60 func TestRoutePositionCodecIsStrict(t *testing.T) {60 func TestRoutePositionCodecIsStrict(t *testing.T) {
@@ -78,7 +78,7 @@ func TestRoutePositionCodecIsStrict(t *testing.T) {
78 78
79 func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {79 func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
80 identity := &protocol.CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}80 identity := &protocol.CommittedRouteIdentity{Target: testExactTarget(), Ephemeral: true}
81- want := Welcome{SessionID: "daemon-session", SessionName: "work", Ephemeral: true, CommittedIdentity: identity}81+ want := protocol.Welcome{SessionID: "daemon-session", SessionName: "work", Ephemeral: true, CommittedIdentity: identity}
82 82
83 got, err := UnmarshalWelcome(MarshalWelcome(want))83 got, err := UnmarshalWelcome(MarshalWelcome(want))
84 require.NoError(t, err)84 require.NoError(t, err)
@@ -87,7 +87,7 @@ func TestWelcomeCarriesCommittedRouteIdentity(t *testing.T) {
87 87
88 func TestWelcomeRejectsMismatchedCommittedIdentity(t *testing.T) {88 func TestWelcomeRejectsMismatchedCommittedIdentity(t *testing.T) {
89 identity := &protocol.CommittedRouteIdentity{Target: testExactTarget()}89 identity := &protocol.CommittedRouteIdentity{Target: testExactTarget()}
90- payload := MarshalWelcome(Welcome{SessionID: "daemon-session", SessionName: "other", CommittedIdentity: identity})90+ payload := MarshalWelcome(protocol.Welcome{SessionID: "daemon-session", SessionName: "other", CommittedIdentity: identity})
91 91
92 require.Nil(t, payload)92 require.Nil(t, payload)
93 }93 }
D internal/ports/session_control.go +0 -323
@@ -1,323 +0,0 @@
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 +138 -138
@@ -114,7 +114,7 @@ func (w *payloadWriter) putLongBytes(b []byte) {
114 // A target may be absent for direct local attaches, but its presence marker and114 // A target may be absent for direct local attaches, but its presence marker and
115 // environment policy are never optional: v27 has no extension or legacy tail115 // environment policy are never optional: v27 has no extension or legacy tail
116 // inside this section; the exact-target section follows it.116 // inside this section; the exact-target section follows it.
117-func marshalRemoteTargetSection(w *payloadWriter, target *domain.RemoteSessionTarget, policy EnvironmentPolicy) {117+func marshalRemoteTargetSection(w *payloadWriter, target *domain.RemoteSessionTarget, policy protocol.EnvironmentPolicy) {
118 if target == nil {118 if target == nil {
119 w.putBool(false)119 w.putBool(false)
120 } else {120 } else {
@@ -190,7 +190,7 @@ func skipRemoteTargetSection(r *payloadReader) error {
190 if err != nil {190 if err != nil {
191 return err191 return err
192 }192 }
193- if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {193+ if protocol.ValidateEnvironmentPolicy(protocol.EnvironmentPolicy(policy)) != nil {
194 return errInvalidEnum194 return errInvalidEnum
195 }195 }
196 return nil196 return nil
@@ -256,7 +256,7 @@ func unmarshalRemoteTarget(r *payloadReader) (*domain.RemoteSessionTarget, error
256 return target, nil256 return target, nil
257 }257 }
258 258
259-func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget, EnvironmentPolicy, error) {259+func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget, protocol.EnvironmentPolicy, error) {
260 target, err := unmarshalRemoteTarget(r)260 target, err := unmarshalRemoteTarget(r)
261 if err != nil {261 if err != nil {
262 return nil, 0, err262 return nil, 0, err
@@ -265,10 +265,10 @@ func unmarshalRemoteTargetSection(r *payloadReader) (*domain.RemoteSessionTarget
265 if err != nil {265 if err != nil {
266 return nil, 0, err266 return nil, 0, err
267 }267 }
268- if ValidateEnvironmentPolicy(EnvironmentPolicy(policy)) != nil {268+ if protocol.ValidateEnvironmentPolicy(protocol.EnvironmentPolicy(policy)) != nil {
269 return nil, 0, errInvalidEnum269 return nil, 0, errInvalidEnum
270 }270 }
271- return target, EnvironmentPolicy(policy), nil271+ return target, protocol.EnvironmentPolicy(policy), nil
272 }272 }
273 273
274 // payloadReader consumes a message payload field by field in wire order,274 // payloadReader consumes a message payload field by field in wire order,
@@ -439,8 +439,8 @@ func validateWireRemoteTarget(target domain.RemoteSessionTarget) error {
439 }439 }
440 440
441 // MarshalHello encodes h into a Hello message payload.441 // MarshalHello encodes h into a Hello message payload.
442-func MarshalHello(h Hello) []byte {442+func MarshalHello(h protocol.Hello) []byte {
443- if err := ValidateHello(h); err != nil {443+ if err := protocol.ValidateHello(h); err != nil {
444 return nil444 return nil
445 }445 }
446 w := payloadWriter{}446 w := payloadWriter{}
@@ -478,7 +478,7 @@ func preflightHello(b []byte) error {
478 return err478 return err
479 }479 }
480 if version == 21 {480 if version == 21 {
481- return ErrInvalidHello481+ return protocol.ErrInvalidHello
482 }482 }
483 if _, err := r.getUint8(); err != nil {483 if _, err := r.getUint8(); err != nil {
484 return err484 return err
@@ -554,122 +554,122 @@ func preflightHello(b []byte) error {
554 }554 }
555 555
556 // UnmarshalHello decodes a Hello message payload.556 // UnmarshalHello decodes a Hello message payload.
557-func UnmarshalHello(b []byte) (Hello, error) {557+func UnmarshalHello(b []byte) (protocol.Hello, error) {
558 if len(b) > MaxFrameLen-1 {558 if len(b) > MaxFrameLen-1 {
559- return Hello{}, ErrInvalidHello559+ return protocol.Hello{}, protocol.ErrInvalidHello
560 }560 }
561 if err := preflightHello(b); err != nil {561 if err := preflightHello(b); err != nil {
562- return Hello{}, err562+ return protocol.Hello{}, err
563 }563 }
564 r := payloadReader{b: b}564 r := payloadReader{b: b}
565- var h Hello565+ var h protocol.Hello
566 var err error566 var err error
567 567
568 if h.Version, err = r.getUint16(); err != nil {568 if h.Version, err = r.getUint16(); err != nil {
569- return Hello{}, err569+ return protocol.Hello{}, err
570 }570 }
571 if h.Intent, err = r.getUint8(); err != nil {571 if h.Intent, err = r.getUint8(); err != nil {
572- return Hello{}, err572+ return protocol.Hello{}, err
573 }573 }
574 clientID, err := r.getBytes(len(h.ClientID))574 clientID, err := r.getBytes(len(h.ClientID))
575 if err != nil {575 if err != nil {
576- return Hello{}, err576+ return protocol.Hello{}, err
577 }577 }
578 copy(h.ClientID[:], clientID)578 copy(h.ClientID[:], clientID)
579 if h.ResumeToken, err = r.getUint64(); err != nil {579 if h.ResumeToken, err = r.getUint64(); err != nil {
580- return Hello{}, err580+ return protocol.Hello{}, err
581 }581 }
582 if h.Name, err = r.getString(); err != nil {582 if h.Name, err = r.getString(); err != nil {
583- return Hello{}, err583+ return protocol.Hello{}, err
584 }584 }
585 cols, err := r.getUint16()585 cols, err := r.getUint16()
586 if err != nil {586 if err != nil {
587- return Hello{}, err587+ return protocol.Hello{}, err
588 }588 }
589 rows, err := r.getUint16()589 rows, err := r.getUint16()
590 if err != nil {590 if err != nil {
591- return Hello{}, err591+ return protocol.Hello{}, err
592 }592 }
593 pixelWidth, err := r.getUint16()593 pixelWidth, err := r.getUint16()
594 if err != nil {594 if err != nil {
595- return Hello{}, err595+ return protocol.Hello{}, err
596 }596 }
597 pixelHeight, err := r.getUint16()597 pixelHeight, err := r.getUint16()
598 if err != nil {598 if err != nil {
599- return Hello{}, err599+ return protocol.Hello{}, err
600 }600 }
601 h.Size = domain.Size{Cols: int(cols), Rows: int(rows)}601 h.Size = domain.Size{Cols: int(cols), Rows: int(rows)}
602 h.PixelWidth = int(pixelWidth)602 h.PixelWidth = int(pixelWidth)
603 h.PixelHeight = int(pixelHeight)603 h.PixelHeight = int(pixelHeight)
604 if h.TermEnv, err = r.getString(); err != nil {604 if h.TermEnv, err = r.getString(); err != nil {
605- return Hello{}, err605+ return protocol.Hello{}, err
606 }606 }
607 if h.Cwd, err = r.getString(); err != nil {607 if h.Cwd, err = r.getString(); err != nil {
608- return Hello{}, err608+ return protocol.Hello{}, err
609 }609 }
610 if h.TrueColor, err = r.getBool(); err != nil {610 if h.TrueColor, err = r.getBool(); err != nil {
611- return Hello{}, err611+ return protocol.Hello{}, err
612 }612 }
613 if h.MaxOutputInFlight, err = r.getUint8(); err != nil {613 if h.MaxOutputInFlight, err = r.getUint8(); err != nil {
614- return Hello{}, err614+ return protocol.Hello{}, err
615 }615 }
616 envCount, err := r.getUint32()616 envCount, err := r.getUint32()
617 if err != nil {617 if err != nil {
618- return Hello{}, err618+ return protocol.Hello{}, err
619 }619 }
620 // Each entry has at least its uint32 byte length. Check that before620 // Each entry has at least its uint32 byte length. Check that before
621 // allocating so a malformed count cannot force an excessive allocation.621 // allocating so a malformed count cannot force an excessive allocation.
622 if uint64(envCount) > uint64(len(r.b)/4) {622 if uint64(envCount) > uint64(len(r.b)/4) {
623- return Hello{}, errShortPayload623+ return protocol.Hello{}, errShortPayload
624 }624 }
625 if envCount != 0 {625 if envCount != 0 {
626 h.Env = make([]string, 0, int(envCount))626 h.Env = make([]string, 0, int(envCount))
627 for range int(envCount) {627 for range int(envCount) {
628 entry, err := r.getLongString()628 entry, err := r.getLongString()
629 if err != nil {629 if err != nil {
630- return Hello{}, err630+ return protocol.Hello{}, err
631 }631 }
632 h.Env = append(h.Env, entry)632 h.Env = append(h.Env, entry)
633 }633 }
634 }634 }
635 if h.RemoteTarget, h.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {635 if h.RemoteTarget, h.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {
636- return Hello{}, err636+ return protocol.Hello{}, err
637 }637 }
638 if h.ExactTarget, err = unmarshalExactTargetSection(&r); err != nil {638 if h.ExactTarget, err = unmarshalExactTargetSection(&r); err != nil {
639- return Hello{}, err639+ return protocol.Hello{}, err
640 }640 }
641 preferredTabID, err := r.getString()641 preferredTabID, err := r.getString()
642 if err != nil {642 if err != nil {
643- return Hello{}, err643+ return protocol.Hello{}, err
644 }644 }
645 h.PreferredTabID = domain.TabStableID(preferredTabID)645 h.PreferredTabID = domain.TabStableID(preferredTabID)
646 capabilities, err := r.getUint8()646 capabilities, err := r.getUint8()
647 if err != nil {647 if err != nil {
648- return Hello{}, err648+ return protocol.Hello{}, err
649 }649 }
650 h.NavigationCapabilities = protocol.NavigationCapabilities(capabilities)650 h.NavigationCapabilities = protocol.NavigationCapabilities(capabilities)
651 overlay, err := r.getUint8()651 overlay, err := r.getUint8()
652 if err != nil {652 if err != nil {
653- return Hello{}, err653+ return protocol.Hello{}, err
654 }654 }
655 h.StartupOverlay = protocol.StartupOverlay(overlay)655 h.StartupOverlay = protocol.StartupOverlay(overlay)
656 if h.Remote, err = r.getBool(); err != nil {656 if h.Remote, err = r.getBool(); err != nil {
657- return Hello{}, err657+ return protocol.Hello{}, err
658 }658 }
659 if h.KittyDirectGraphics, err = r.getBool(); err != nil {659 if h.KittyDirectGraphics, err = r.getBool(); err != nil {
660- return Hello{}, err660+ return protocol.Hello{}, err
661 }661 }
662 if err := r.done(); err != nil {662 if err := r.done(); err != nil {
663- return Hello{}, err663+ return protocol.Hello{}, err
664 }664 }
665- if err := ValidateHello(h); err != nil {665+ if err := protocol.ValidateHello(h); err != nil {
666- return Hello{}, err666+ return protocol.Hello{}, err
667 }667 }
668 return h, nil668 return h, nil
669 }669 }
670 670
671 // MarshalInput encodes m into an Input message payload.671 // MarshalInput encodes m into an Input message payload.
672-func MarshalInput(m Input) []byte {672+func MarshalInput(m protocol.Input) []byte {
673 w := payloadWriter{}673 w := payloadWriter{}
674 w.putUint64(m.InputSeq)674 w.putUint64(m.InputSeq)
675 w.putBytes(m.Data)675 w.putBytes(m.Data)
@@ -678,13 +678,13 @@ func MarshalInput(m Input) []byte {
678 678
679 // UnmarshalInput decodes an Input message payload. After the fixed input679 // UnmarshalInput decodes an Input message payload. After the fixed input
680 // sequence, the rest of the payload is data; there is no length prefix.680 // sequence, the rest of the payload is data; there is no length prefix.
681-func UnmarshalInput(b []byte) (Input, error) {681+func UnmarshalInput(b []byte) (protocol.Input, error) {
682 r := payloadReader{b: b}682 r := payloadReader{b: b}
683 seq, err := r.getUint64()683 seq, err := r.getUint64()
684 if err != nil {684 if err != nil {
685- return Input{}, err685+ return protocol.Input{}, err
686 }686 }
687- return Input{InputSeq: seq, Data: r.rest()}, nil687+ return protocol.Input{InputSeq: seq, Data: r.rest()}, nil
688 }688 }
689 689
690 // MarshalImagePush encodes m into an ImagePush message payload.690 // MarshalImagePush encodes m into an ImagePush message payload.
@@ -713,25 +713,25 @@ func UnmarshalImagePush(b []byte) (protocol.ImagePush, error) {
713 }713 }
714 714
715 // MarshalClientNotice encodes a fixed one-byte client notice action.715 // MarshalClientNotice encodes a fixed one-byte client notice action.
716-func MarshalClientNotice(m ClientNotice) []byte {716+func MarshalClientNotice(m protocol.ClientNotice) []byte {
717 return []byte{m.Action}717 return []byte{m.Action}
718 }718 }
719 719
720 // UnmarshalClientNotice decodes a fixed client notice action and rejects both720 // UnmarshalClientNotice decodes a fixed client notice action and rejects both
721 // unknown values and any trailing bytes.721 // unknown values and any trailing bytes.
722-func UnmarshalClientNotice(b []byte) (ClientNotice, error) {722+func UnmarshalClientNotice(b []byte) (protocol.ClientNotice, error) {
723 r := payloadReader{b: b}723 r := payloadReader{b: b}
724 action, err := r.getUint8()724 action, err := r.getUint8()
725 if err != nil {725 if err != nil {
726- return ClientNotice{}, err726+ return protocol.ClientNotice{}, err
727 }727 }
728- if ValidateClientNotice(ClientNotice{Action: action}) != nil {728+ if protocol.ValidateClientNotice(protocol.ClientNotice{Action: action}) != nil {
729- return ClientNotice{}, errors.New("ports: unknown client notice action")729+ return protocol.ClientNotice{}, errors.New("ports: unknown client notice action")
730 }730 }
731 if err := r.done(); err != nil {731 if err := r.done(); err != nil {
732- return ClientNotice{}, err732+ return protocol.ClientNotice{}, err
733 }733 }
734- return ClientNotice{Action: action}, nil734+ return protocol.ClientNotice{Action: action}, nil
735 }735 }
736 736
737 // MarshalResize encodes m into a Resize message payload.737 // MarshalResize encodes m into a Resize message payload.
@@ -883,45 +883,45 @@ func UnmarshalResize(b []byte) (protocol.Resize, error) {
883 }883 }
884 884
885 // MarshalDetach encodes a Detach message payload (always empty).885 // MarshalDetach encodes a Detach message payload (always empty).
886-func MarshalDetach(Detach) []byte {886+func MarshalDetach(protocol.Detach) []byte {
887 return nil887 return nil
888 }888 }
889 889
890 // UnmarshalDetach decodes a Detach message payload.890 // UnmarshalDetach decodes a Detach message payload.
891-func UnmarshalDetach(b []byte) (Detach, error) {891+func UnmarshalDetach(b []byte) (protocol.Detach, error) {
892 r := payloadReader{b: b}892 r := payloadReader{b: b}
893 if err := r.done(); err != nil {893 if err := r.done(); err != nil {
894- return Detach{}, err894+ return protocol.Detach{}, err
895 }895 }
896- return Detach{}, nil896+ return protocol.Detach{}, nil
897 }897 }
898 898
899 // MarshalPing encodes a Ping message payload (always empty).899 // MarshalPing encodes a Ping message payload (always empty).
900-func MarshalPing(Ping) []byte {900+func MarshalPing(protocol.Ping) []byte {
901 return nil901 return nil
902 }902 }
903 903
904 // UnmarshalPing decodes a Ping message payload.904 // UnmarshalPing decodes a Ping message payload.
905-func UnmarshalPing(b []byte) (Ping, error) {905+func UnmarshalPing(b []byte) (protocol.Ping, error) {
906 r := payloadReader{b: b}906 r := payloadReader{b: b}
907 if err := r.done(); err != nil {907 if err := r.done(); err != nil {
908- return Ping{}, err908+ return protocol.Ping{}, err
909 }909 }
910- return Ping{}, nil910+ return protocol.Ping{}, nil
911 }911 }
912 912
913 // MarshalPong encodes a Pong message payload (always empty).913 // MarshalPong encodes a Pong message payload (always empty).
914-func MarshalPong(Pong) []byte {914+func MarshalPong(protocol.Pong) []byte {
915 return nil915 return nil
916 }916 }
917 917
918 // UnmarshalPong decodes a Pong message payload.918 // UnmarshalPong decodes a Pong message payload.
919-func UnmarshalPong(b []byte) (Pong, error) {919+func UnmarshalPong(b []byte) (protocol.Pong, error) {
920 r := payloadReader{b: b}920 r := payloadReader{b: b}
921 if err := r.done(); err != nil {921 if err := r.done(); err != nil {
922- return Pong{}, err922+ return protocol.Pong{}, err
923 }923 }
924- return Pong{}, nil924+ return protocol.Pong{}, nil
925 }925 }
926 926
927 // MarshalAck encodes m into an epoch/state Ack payload.927 // MarshalAck encodes m into an epoch/state Ack payload.
@@ -956,7 +956,7 @@ func UnmarshalAck(b []byte) (protocol.Ack, error) {
956 }956 }
957 957
958 // MarshalWelcome encodes m into a Welcome message payload.958 // MarshalWelcome encodes m into a Welcome message payload.
959-func MarshalWelcome(m Welcome) []byte {959+func MarshalWelcome(m protocol.Welcome) []byte {
960 if m.CommittedIdentity != nil && (m.SessionName != m.CommittedIdentity.Target.SessionName || m.Ephemeral != m.CommittedIdentity.Ephemeral) {960 if m.CommittedIdentity != nil && (m.SessionName != m.CommittedIdentity.Target.SessionName || m.Ephemeral != m.CommittedIdentity.Ephemeral) {
961 return nil961 return nil
962 }962 }
@@ -977,42 +977,42 @@ func MarshalWelcome(m Welcome) []byte {
977 }977 }
978 978
979 // UnmarshalWelcome decodes a Welcome message payload.979 // UnmarshalWelcome decodes a Welcome message payload.
980-func UnmarshalWelcome(b []byte) (Welcome, error) {980+func UnmarshalWelcome(b []byte) (protocol.Welcome, error) {
981 r := payloadReader{b: b}981 r := payloadReader{b: b}
982- var m Welcome982+ var m protocol.Welcome
983 var err error983 var err error
984 984
985 if m.SessionID, err = r.getString(); err != nil {985 if m.SessionID, err = r.getString(); err != nil {
986- return Welcome{}, err986+ return protocol.Welcome{}, err
987 }987 }
988 if m.SessionName, err = r.getString(); err != nil {988 if m.SessionName, err = r.getString(); err != nil {
989- return Welcome{}, err989+ return protocol.Welcome{}, err
990 }990 }
991 var ephemeral bool991 var ephemeral bool
992 if ephemeral, err = r.getBool(); err != nil {992 if ephemeral, err = r.getBool(); err != nil {
993- return Welcome{}, err993+ return protocol.Welcome{}, err
994 }994 }
995 m.Ephemeral = ephemeral995 m.Ephemeral = ephemeral
996 if m.ResumeToken, err = r.getUint64(); err != nil {996 if m.ResumeToken, err = r.getUint64(); err != nil {
997- return Welcome{}, err997+ return protocol.Welcome{}, err
998 }998 }
999 if m.Capabilities, err = r.getUint32(); err != nil {999 if m.Capabilities, err = r.getUint32(); err != nil {
1000- return Welcome{}, err1000+ return protocol.Welcome{}, err
1001 }1001 }
1002 if m.CommittedIdentity, err = unmarshalCommittedIdentitySection(&r); err != nil {1002 if m.CommittedIdentity, err = unmarshalCommittedIdentitySection(&r); err != nil {
1003- return Welcome{}, err1003+ return protocol.Welcome{}, err
1004 }1004 }
1005 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) {
1006- return Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", protocol.ErrInvalidRouteWire)1006+ return protocol.Welcome{}, fmt.Errorf("%w: committed identity does not match welcome", protocol.ErrInvalidRouteWire)
1007 }1007 }
1008 if err := r.done(); err != nil {1008 if err := r.done(); err != nil {
1009- return Welcome{}, err1009+ return protocol.Welcome{}, err
1010 }1010 }
1011 return m, nil1011 return m, nil
1012 }1012 }
1013 1013
1014 // MarshalErrorMsg encodes m into an ErrorMsg message payload.1014 // MarshalErrorMsg encodes m into an ErrorMsg message payload.
1015-func MarshalErrorMsg(m ErrorMsg) []byte {1015+func MarshalErrorMsg(m protocol.ErrorMsg) []byte {
1016 w := payloadWriter{}1016 w := payloadWriter{}
1017 w.putUint16(m.Code)1017 w.putUint16(m.Code)
1018 w.putString(m.Text)1018 w.putString(m.Text)
@@ -1020,19 +1020,19 @@ func MarshalErrorMsg(m ErrorMsg) []byte {
1020 }1020 }
1021 1021
1022 // UnmarshalErrorMsg decodes an ErrorMsg message payload.1022 // UnmarshalErrorMsg decodes an ErrorMsg message payload.
1023-func UnmarshalErrorMsg(b []byte) (ErrorMsg, error) {1023+func UnmarshalErrorMsg(b []byte) (protocol.ErrorMsg, error) {
1024 r := payloadReader{b: b}1024 r := payloadReader{b: b}
1025- var m ErrorMsg1025+ var m protocol.ErrorMsg
1026 var err error1026 var err error
1027 1027
1028 if m.Code, err = r.getUint16(); err != nil {1028 if m.Code, err = r.getUint16(); err != nil {
1029- return ErrorMsg{}, err1029+ return protocol.ErrorMsg{}, err
1030 }1030 }
1031 if m.Text, err = r.getString(); err != nil {1031 if m.Text, err = r.getString(); err != nil {
1032- return ErrorMsg{}, err1032+ return protocol.ErrorMsg{}, err
1033 }1033 }
1034 if err := r.done(); err != nil {1034 if err := r.done(); err != nil {
1035- return ErrorMsg{}, err1035+ return protocol.ErrorMsg{}, err
1036 }1036 }
1037 return m, nil1037 return m, nil
1038 }1038 }
@@ -1198,8 +1198,8 @@ func decompressOutputData(data []byte, decodedLen int) ([]byte, error) {
1198 }1198 }
1199 1199
1200 // MarshalAttachTarget encodes a strict server attach-target payload.1200 // MarshalAttachTarget encodes a strict server attach-target payload.
1201-func MarshalAttachTarget(m AttachTarget) []byte {1201+func MarshalAttachTarget(m protocol.AttachTarget) []byte {
1202- if err := ValidateAttachTarget(m); err != nil {1202+ if err := protocol.ValidateAttachTarget(m); err != nil {
1203 return nil1203 return nil
1204 }1204 }
1205 w := payloadWriter{}1205 w := payloadWriter{}
@@ -1213,118 +1213,118 @@ func MarshalAttachTarget(m AttachTarget) []byte {
1213 }1213 }
1214 1214
1215 // UnmarshalAttachTarget decodes a strict server attach-target payload.1215 // UnmarshalAttachTarget decodes a strict server attach-target payload.
1216-func UnmarshalAttachTarget(b []byte) (AttachTarget, error) {1216+func UnmarshalAttachTarget(b []byte) (protocol.AttachTarget, error) {
1217 // Preflight lengths and intent before getString can allocate either value.1217 // Preflight lengths and intent before getString can allocate either value.
1218 probe := payloadReader{b: b}1218 probe := payloadReader{b: b}
1219 endpointLen, err := probe.getUint16()1219 endpointLen, err := probe.getUint16()
1220 if err != nil || int(endpointLen) > len(probe.b) {1220 if err != nil || int(endpointLen) > len(probe.b) {
1221- return AttachTarget{}, ErrInvalidAttachTarget1221+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
1222 }1222 }
1223 probe.b = probe.b[endpointLen:]1223 probe.b = probe.b[endpointLen:]
1224 sessionLen, err := probe.getUint16()1224 sessionLen, err := probe.getUint16()
1225 if err != nil || sessionLen == 0 || int(sessionLen) > len(probe.b) {1225 if err != nil || sessionLen == 0 || int(sessionLen) > len(probe.b) {
1226- return AttachTarget{}, ErrInvalidAttachTarget1226+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
1227 }1227 }
1228 probe.b = probe.b[sessionLen:]1228 probe.b = probe.b[sessionLen:]
1229 intent, err := probe.getUint8()1229 intent, err := probe.getUint8()
1230- if err != nil || (intent != IntentEphemeral && intent != IntentNew && intent != IntentAttach && intent != IntentResume) {1230+ if err != nil || (intent != protocol.IntentEphemeral && intent != protocol.IntentNew && intent != protocol.IntentAttach && intent != protocol.IntentResume) {
1231- return AttachTarget{}, ErrInvalidAttachTarget1231+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
1232 }1232 }
1233 if err := skipRemoteTargetSection(&probe); err != nil {1233 if err := skipRemoteTargetSection(&probe); err != nil {
1234- return AttachTarget{}, ErrInvalidAttachTarget1234+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
1235 }1235 }
1236 if err := skipExactTargetSection(&probe); err != nil {1236 if err := skipExactTargetSection(&probe); err != nil {
1237- return AttachTarget{}, ErrInvalidAttachTarget1237+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
1238 }1238 }
1239 if _, err := probe.getBool(); err != nil {1239 if _, err := probe.getBool(); err != nil {
1240- return AttachTarget{}, ErrInvalidAttachTarget1240+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
1241 }1241 }
1242 if err := probe.done(); err != nil {1242 if err := probe.done(); err != nil {
1243- return AttachTarget{}, ErrInvalidAttachTarget1243+ return protocol.AttachTarget{}, protocol.ErrInvalidAttachTarget
1244 }1244 }
1245 1245
1246 r := payloadReader{b: b}1246 r := payloadReader{b: b}
1247- var m AttachTarget1247+ var m protocol.AttachTarget
1248 if m.Endpoint, err = r.getString(); err != nil {1248 if m.Endpoint, err = r.getString(); err != nil {
1249- return AttachTarget{}, err1249+ return protocol.AttachTarget{}, err
1250 }1250 }
1251 if m.Session, err = r.getString(); err != nil {1251 if m.Session, err = r.getString(); err != nil {
1252- return AttachTarget{}, err1252+ return protocol.AttachTarget{}, err
1253 }1253 }
1254 if m.Intent, err = r.getUint8(); err != nil {1254 if m.Intent, err = r.getUint8(); err != nil {
1255- return AttachTarget{}, err1255+ return protocol.AttachTarget{}, err
1256 }1256 }
1257 if m.RemoteTarget, m.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {1257 if m.RemoteTarget, m.EnvironmentPolicy, err = unmarshalRemoteTargetSection(&r); err != nil {
1258- return AttachTarget{}, err1258+ return protocol.AttachTarget{}, err
1259 }1259 }
1260 if m.ExactTarget, err = unmarshalExactTargetSection(&r); err != nil {1260 if m.ExactTarget, err = unmarshalExactTargetSection(&r); err != nil {
1261- return AttachTarget{}, err1261+ return protocol.AttachTarget{}, err
1262 }1262 }
1263 if m.SamePeer, err = r.getBool(); err != nil {1263 if m.SamePeer, err = r.getBool(); err != nil {
1264- return AttachTarget{}, err1264+ return protocol.AttachTarget{}, err
1265 }1265 }
1266 if err := r.done(); err != nil {1266 if err := r.done(); err != nil {
1267- return AttachTarget{}, err1267+ return protocol.AttachTarget{}, err
1268 }1268 }
1269- if err := ValidateAttachTarget(m); err != nil {1269+ if err := protocol.ValidateAttachTarget(m); err != nil {
1270- return AttachTarget{}, err1270+ return protocol.AttachTarget{}, err
1271 }1271 }
1272 return m, nil1272 return m, nil
1273 }1273 }
1274 1274
1275 // MarshalDetached encodes m into a Detached message payload.1275 // MarshalDetached encodes m into a Detached message payload.
1276-func MarshalDetached(m Detached) []byte {1276+func MarshalDetached(m protocol.Detached) []byte {
1277 return []byte{m.Reason}1277 return []byte{m.Reason}
1278 }1278 }
1279 1279
1280 // UnmarshalDetached decodes a Detached message payload.1280 // UnmarshalDetached decodes a Detached message payload.
1281-func UnmarshalDetached(b []byte) (Detached, error) {1281+func UnmarshalDetached(b []byte) (protocol.Detached, error) {
1282 r := payloadReader{b: b}1282 r := payloadReader{b: b}
1283 reason, err := r.getUint8()1283 reason, err := r.getUint8()
1284 if err != nil {1284 if err != nil {
1285- return Detached{}, err1285+ return protocol.Detached{}, err
1286 }1286 }
1287- if ValidateDetached(Detached{Reason: reason}) != nil {1287+ if protocol.ValidateDetached(protocol.Detached{Reason: reason}) != nil {
1288- return Detached{}, errInvalidEnum1288+ return protocol.Detached{}, errInvalidEnum
1289 }1289 }
1290 if err := r.done(); err != nil {1290 if err := r.done(); err != nil {
1291- return Detached{}, err1291+ return protocol.Detached{}, err
1292 }1292 }
1293- return Detached{Reason: reason}, nil1293+ return protocol.Detached{Reason: reason}, nil
1294 }1294 }
1295 1295
1296 // MarshalList encodes a List message payload (always empty).1296 // MarshalList encodes a List message payload (always empty).
1297-func MarshalList(List) []byte {1297+func MarshalList(protocol.List) []byte {
1298 return nil1298 return nil
1299 }1299 }
1300 1300
1301 // UnmarshalList decodes a List message payload.1301 // UnmarshalList decodes a List message payload.
1302-func UnmarshalList(b []byte) (List, error) {1302+func UnmarshalList(b []byte) (protocol.List, error) {
1303 r := payloadReader{b: b}1303 r := payloadReader{b: b}
1304 if err := r.done(); err != nil {1304 if err := r.done(); err != nil {
1305- return List{}, err1305+ return protocol.List{}, err
1306 }1306 }
1307- return List{}, nil1307+ return protocol.List{}, nil
1308 }1308 }
1309 1309
1310 // MarshalOutputResetRequest encodes an OutputResetRequest payload (always1310 // MarshalOutputResetRequest encodes an OutputResetRequest payload (always
1311 // empty).1311 // empty).
1312-func MarshalOutputResetRequest(OutputResetRequest) []byte {1312+func MarshalOutputResetRequest(protocol.OutputResetRequest) []byte {
1313 return nil1313 return nil
1314 }1314 }
1315 1315
1316 // UnmarshalOutputResetRequest decodes a strict empty OutputResetRequest1316 // UnmarshalOutputResetRequest decodes a strict empty OutputResetRequest
1317 // payload.1317 // payload.
1318-func UnmarshalOutputResetRequest(b []byte) (OutputResetRequest, error) {1318+func UnmarshalOutputResetRequest(b []byte) (protocol.OutputResetRequest, error) {
1319 r := payloadReader{b: b}1319 r := payloadReader{b: b}
1320 if err := r.done(); err != nil {1320 if err := r.done(); err != nil {
1321- return OutputResetRequest{}, err1321+ return protocol.OutputResetRequest{}, err
1322 }1322 }
1323- return OutputResetRequest{}, nil1323+ return protocol.OutputResetRequest{}, nil
1324 }1324 }
1325 1325
1326 // MarshalKill encodes m into a Kill message payload.1326 // MarshalKill encodes m into a Kill message payload.
1327-func MarshalKill(m Kill) []byte {1327+func MarshalKill(m protocol.Kill) []byte {
1328 w := payloadWriter{}1328 w := payloadWriter{}
1329 w.putString(m.Name)1329 w.putString(m.Name)
1330 if m.All {1330 if m.All {
@@ -1334,28 +1334,28 @@ func MarshalKill(m Kill) []byte {
1334 }1334 }
1335 1335
1336 // UnmarshalKill decodes a Kill message payload.1336 // UnmarshalKill decodes a Kill message payload.
1337-func UnmarshalKill(b []byte) (Kill, error) {1337+func UnmarshalKill(b []byte) (protocol.Kill, error) {
1338 r := payloadReader{b: b}1338 r := payloadReader{b: b}
1339 name, err := r.getString()1339 name, err := r.getString()
1340 if err != nil {1340 if err != nil {
1341- return Kill{}, err1341+ return protocol.Kill{}, err
1342 }1342 }
1343 var all bool1343 var all bool
1344 if len(r.b) > 0 {1344 if len(r.b) > 0 {
1345 all, err = r.getBool()1345 all, err = r.getBool()
1346 if err != nil {1346 if err != nil {
1347- return Kill{}, err1347+ return protocol.Kill{}, err
1348 }1348 }
1349 }1349 }
1350 if err := r.done(); err != nil {1350 if err := r.done(); err != nil {
1351- return Kill{}, err1351+ return protocol.Kill{}, err
1352 }1352 }
1353- return Kill{Name: name, All: all}, nil1353+ return protocol.Kill{Name: name, All: all}, nil
1354 }1354 }
1355 1355
1356 // MarshalSessions encodes m into a Sessions message payload: a uint16 count1356 // MarshalSessions encodes m into a Sessions message payload: a uint16 count
1357 // followed by that many session records.1357 // followed by that many session records.
1358-func MarshalSessions(m Sessions) []byte {1358+func MarshalSessions(m protocol.Sessions) []byte {
1359 w := payloadWriter{}1359 w := payloadWriter{}
1360 w.putUint16(uint16(len(m.Sessions)))1360 w.putUint16(uint16(len(m.Sessions)))
1361 for _, s := range m.Sessions {1361 for _, s := range m.Sessions {
@@ -1378,48 +1378,48 @@ func MarshalSessions(m Sessions) []byte {
1378 }1378 }
1379 1379
1380 // UnmarshalSessions decodes a Sessions message payload.1380 // UnmarshalSessions decodes a Sessions message payload.
1381-func UnmarshalSessions(b []byte) (Sessions, error) {1381+func UnmarshalSessions(b []byte) (protocol.Sessions, error) {
1382 r := payloadReader{b: b}1382 r := payloadReader{b: b}
1383 count, err := r.getUint16()1383 count, err := r.getUint16()
1384 if err != nil {1384 if err != nil {
1385- return Sessions{}, err1385+ return protocol.Sessions{}, err
1386 }1386 }
1387- sessions := make([]SessionInfo, 0, count)1387+ sessions := make([]protocol.SessionInfo, 0, count)
1388 for range int(count) {1388 for range int(count) {
1389- var s SessionInfo1389+ var s protocol.SessionInfo
1390 if s.SessionID, err = r.getString(); err != nil {1390 if s.SessionID, err = r.getString(); err != nil {
1391- return Sessions{}, err1391+ return protocol.Sessions{}, err
1392 }1392 }
1393 if s.Name, err = r.getString(); err != nil {1393 if s.Name, err = r.getString(); err != nil {
1394- return Sessions{}, err1394+ return protocol.Sessions{}, err
1395 }1395 }
1396 eph, err := r.getBool()1396 eph, err := r.getBool()
1397 if err != nil {1397 if err != nil {
1398- return Sessions{}, err1398+ return protocol.Sessions{}, err
1399 }1399 }
1400 s.Ephemeral = eph1400 s.Ephemeral = eph
1401 if s.Tabs, err = r.getUint16(); err != nil {1401 if s.Tabs, err = r.getUint16(); err != nil {
1402- return Sessions{}, err1402+ return protocol.Sessions{}, err
1403 }1403 }
1404 att, err := r.getBool()1404 att, err := r.getBool()
1405 if err != nil {1405 if err != nil {
1406- return Sessions{}, err1406+ return protocol.Sessions{}, err
1407 }1407 }
1408 s.Attached = att1408 s.Attached = att
1409 state, err := r.getUint8()1409 state, err := r.getUint8()
1410 if err != nil {1410 if err != nil {
1411- return Sessions{}, err1411+ return protocol.Sessions{}, err
1412 }1412 }
1413- s.State = SessionState(state)1413+ s.State = protocol.SessionState(state)
1414- if s.State > SessionBroken {1414+ if s.State > protocol.SessionBroken {
1415- return Sessions{}, errors.New("ports: invalid session state")1415+ return protocol.Sessions{}, errors.New("ports: invalid session state")
1416 }1416 }
1417 sessions = append(sessions, s)1417 sessions = append(sessions, s)
1418 }1418 }
1419 if err := r.done(); err != nil {1419 if err := r.done(); err != nil {
1420- return Sessions{}, err1420+ return protocol.Sessions{}, err
1421 }1421 }
1422- return Sessions{Sessions: sessions}, nil1422+ return protocol.Sessions{Sessions: sessions}, nil
1423 }1423 }
1424 func putInt16(w *payloadWriter, n int) { w.putUint16(uint16(int16(n))) }1424 func putInt16(w *payloadWriter, n int) { w.putUint16(uint16(int16(n))) }
1425 1425
@@ -1737,7 +1737,7 @@ func MarshalParkedRouteRequest(request protocol.ParkedRouteRequest) []byte {
1737 w.putUint64(request.RequestID)1737 w.putUint64(request.RequestID)
1738 w.putBytes(request.LeaseID[:])1738 w.putBytes(request.LeaseID[:])
1739 w.putUint8(uint8(request.Action))1739 w.putUint8(uint8(request.Action))
1740- marshalRemoteTargetSection(&w, request.Target, EnvironmentPolicyDaemonOwned)1740+ marshalRemoteTargetSection(&w, request.Target, protocol.EnvironmentPolicyDaemonOwned)
1741 return w.b1741 return w.b
1742 }1742 }
1743 1743
@@ -1757,7 +1757,7 @@ func UnmarshalParkedRouteRequest(b []byte) (protocol.ParkedRouteRequest, error)
1757 return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation1757 return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
1758 }1758 }
1759 target, policy, err := unmarshalRemoteTargetSection(&r)1759 target, policy, err := unmarshalRemoteTargetSection(&r)
1760- if err != nil || policy != EnvironmentPolicyDaemonOwned || r.done() != nil {1760+ if err != nil || policy != protocol.EnvironmentPolicyDaemonOwned || r.done() != nil {
1761 return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation1761 return protocol.ParkedRouteRequest{}, protocol.ErrInvalidNavigation
1762 }1762 }
1763 request := protocol.ParkedRouteRequest{RequestID: requestID, Action: protocol.ParkedRouteAction(action), Target: target}1763 request := protocol.ParkedRouteRequest{RequestID: requestID, Action: protocol.ParkedRouteAction(action), Target: target}
M internal/ports/wire_command.go +27 -49
@@ -4,35 +4,13 @@ import (
4 "encoding/binary"4 "encoding/binary"
5 "errors"5 "errors"
6 "math"6 "math"
7+
8+ "github.com/bnema/vev/internal/protocol"
7 )9 )
8 10
9 // ErrTooManyCommandArgs reports that a request exceeds the wire count limit.11 // ErrTooManyCommandArgs reports that a request exceeds the wire count limit.
10 var ErrTooManyCommandArgs = errors.New("ports: too many command arguments")12 var ErrTooManyCommandArgs = errors.New("ports: too many command arguments")
11 13
12-// CommandRequest asks the daemon to run one control command. Version must
13-// stay first so a future payload layout can still be rejected cleanly.
14-type CommandRequest struct {
15- Version uint16
16- RequestID uint64
17- Attached bool
18- Self bool
19- Slug string
20- Args []string
21- TargetSession string
22- TargetTab string
23- TargetPane string
24- JSON bool
25-}
26-
27-// CommandResult reports a control command's outcome.
28-type CommandResult struct {
29- RequestID uint64
30- OK bool
31- Code uint16
32- Text string
33- Output string
34-}
35-
36 // PeekCommandVersion returns the leading protocol version from a14 // PeekCommandVersion returns the leading protocol version from a
37 // CommandRequest payload.15 // CommandRequest payload.
38 func PeekCommandVersion(b []byte) (uint16, bool) {16 func PeekCommandVersion(b []byte) (uint16, bool) {
@@ -43,7 +21,7 @@ func PeekCommandVersion(b []byte) (uint16, bool) {
43 }21 }
44 22
45 // MarshalCommandRequest encodes m into a CommandRequest payload.23 // MarshalCommandRequest encodes m into a CommandRequest payload.
46-func MarshalCommandRequest(m CommandRequest) ([]byte, error) {24+func MarshalCommandRequest(m protocol.CommandRequest) ([]byte, error) {
47 if len(m.Args) > math.MaxUint16 {25 if len(m.Args) > math.MaxUint16 {
48 return nil, ErrTooManyCommandArgs26 return nil, ErrTooManyCommandArgs
49 }27 }
@@ -66,65 +44,65 @@ func MarshalCommandRequest(m CommandRequest) ([]byte, error) {
66 }44 }
67 45
68 // UnmarshalCommandRequest decodes a strict CommandRequest payload.46 // UnmarshalCommandRequest decodes a strict CommandRequest payload.
69-func UnmarshalCommandRequest(b []byte) (CommandRequest, error) {47+func UnmarshalCommandRequest(b []byte) (protocol.CommandRequest, error) {
70 r := payloadReader{b: b}48 r := payloadReader{b: b}
71- var m CommandRequest49+ var m protocol.CommandRequest
72 var err error50 var err error
73 51
74 if m.Version, err = r.getUint16(); err != nil {52 if m.Version, err = r.getUint16(); err != nil {
75- return CommandRequest{}, err53+ return protocol.CommandRequest{}, err
76 }54 }
77 if m.RequestID, err = r.getUint64(); err != nil {55 if m.RequestID, err = r.getUint64(); err != nil {
78- return CommandRequest{}, err56+ return protocol.CommandRequest{}, err
79 }57 }
80 if m.Attached, err = r.getBool(); err != nil {58 if m.Attached, err = r.getBool(); err != nil {
81- return CommandRequest{}, err59+ return protocol.CommandRequest{}, err
82 }60 }
83 if m.Self, err = r.getBool(); err != nil {61 if m.Self, err = r.getBool(); err != nil {
84- return CommandRequest{}, err62+ return protocol.CommandRequest{}, err
85 }63 }
86 if m.Slug, err = r.getString(); err != nil {64 if m.Slug, err = r.getString(); err != nil {
87- return CommandRequest{}, err65+ return protocol.CommandRequest{}, err
88 }66 }
89 argCount, err := r.getUint16()67 argCount, err := r.getUint16()
90 if err != nil {68 if err != nil {
91- return CommandRequest{}, err69+ return protocol.CommandRequest{}, err
92 }70 }
93 // Every argument requires at least a uint32 length prefix. Reject an71 // Every argument requires at least a uint32 length prefix. Reject an
94 // impossible count before allocating from untrusted input.72 // impossible count before allocating from untrusted input.
95 if uint64(argCount) > uint64(len(r.b)/4) {73 if uint64(argCount) > uint64(len(r.b)/4) {
96- return CommandRequest{}, errShortPayload74+ return protocol.CommandRequest{}, errShortPayload
97 }75 }
98 if argCount != 0 {76 if argCount != 0 {
99 m.Args = make([]string, 0, int(argCount))77 m.Args = make([]string, 0, int(argCount))
100 for range int(argCount) {78 for range int(argCount) {
101 arg, err := r.getLongString()79 arg, err := r.getLongString()
102 if err != nil {80 if err != nil {
103- return CommandRequest{}, err81+ return protocol.CommandRequest{}, err
104 }82 }
105 m.Args = append(m.Args, arg)83 m.Args = append(m.Args, arg)
106 }84 }
107 }85 }
108 if m.TargetSession, err = r.getString(); err != nil {86 if m.TargetSession, err = r.getString(); err != nil {
109- return CommandRequest{}, err87+ return protocol.CommandRequest{}, err
110 }88 }
111 if m.TargetTab, err = r.getString(); err != nil {89 if m.TargetTab, err = r.getString(); err != nil {
112- return CommandRequest{}, err90+ return protocol.CommandRequest{}, err
113 }91 }
114 if m.TargetPane, err = r.getString(); err != nil {92 if m.TargetPane, err = r.getString(); err != nil {
115- return CommandRequest{}, err93+ return protocol.CommandRequest{}, err
116 }94 }
117 if m.JSON, err = r.getBool(); err != nil {95 if m.JSON, err = r.getBool(); err != nil {
118- return CommandRequest{}, err96+ return protocol.CommandRequest{}, err
119 }97 }
120 if err := r.done(); err != nil {98 if err := r.done(); err != nil {
121- return CommandRequest{}, err99+ return protocol.CommandRequest{}, err
122 }100 }
123 return m, nil101 return m, nil
124 }102 }
125 103
126 // MarshalCommandResult encodes m into a CommandResult payload.104 // MarshalCommandResult encodes m into a CommandResult payload.
127-func MarshalCommandResult(m CommandResult) []byte {105+func MarshalCommandResult(m protocol.CommandResult) []byte {
128 w := payloadWriter{}106 w := payloadWriter{}
129 w.putUint64(m.RequestID)107 w.putUint64(m.RequestID)
130 w.putBool(m.OK)108 w.putBool(m.OK)
@@ -135,28 +113,28 @@ func MarshalCommandResult(m CommandResult) []byte {
135 }113 }
136 114
137 // UnmarshalCommandResult decodes a strict CommandResult payload.115 // UnmarshalCommandResult decodes a strict CommandResult payload.
138-func UnmarshalCommandResult(b []byte) (CommandResult, error) {116+func UnmarshalCommandResult(b []byte) (protocol.CommandResult, error) {
139 r := payloadReader{b: b}117 r := payloadReader{b: b}
140- var m CommandResult118+ var m protocol.CommandResult
141 var err error119 var err error
142 120
143 if m.RequestID, err = r.getUint64(); err != nil {121 if m.RequestID, err = r.getUint64(); err != nil {
144- return CommandResult{}, err122+ return protocol.CommandResult{}, err
145 }123 }
146 if m.OK, err = r.getBool(); err != nil {124 if m.OK, err = r.getBool(); err != nil {
147- return CommandResult{}, err125+ return protocol.CommandResult{}, err
148 }126 }
149 if m.Code, err = r.getUint16(); err != nil {127 if m.Code, err = r.getUint16(); err != nil {
150- return CommandResult{}, err128+ return protocol.CommandResult{}, err
151 }129 }
152 if m.Text, err = r.getString(); err != nil {130 if m.Text, err = r.getString(); err != nil {
153- return CommandResult{}, err131+ return protocol.CommandResult{}, err
154 }132 }
155 if m.Output, err = r.getLongString(); err != nil {133 if m.Output, err = r.getLongString(); err != nil {
156- return CommandResult{}, err134+ return protocol.CommandResult{}, err
157 }135 }
158 if err := r.done(); err != nil {136 if err := r.done(); err != nil {
159- return CommandResult{}, err137+ return protocol.CommandResult{}, err
160 }138 }
161 return m, nil139 return m, nil
162 }140 }
M internal/ports/wire_final_test.go +27 -27
@@ -12,21 +12,21 @@ import (
12 )12 )
13 13
14 func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) {14 func TestFinalProtocolVersionAndNoV21Hello(t *testing.T) {
15- if ProtocolVersion != 37 {15+ if protocol.Version != 37 {
16- t.Fatalf("ProtocolVersion = %d, want 37", ProtocolVersion)16+ t.Fatalf("ProtocolVersion = %d, want 37", protocol.Version)
17 }17 }
18- payload := MarshalHello(Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}})18+ payload := MarshalHello(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}})
19 if len(payload) < 2 {19 if len(payload) < 2 {
20 t.Fatal("MarshalHello produced a truncated valid payload")20 t.Fatal("MarshalHello produced a truncated valid payload")
21 }21 }
22 binary.BigEndian.PutUint16(payload, 21)22 binary.BigEndian.PutUint16(payload, 21)
23- if _, err := UnmarshalHello(payload); !errors.Is(err, ErrInvalidHello) {23+ if _, err := UnmarshalHello(payload); !errors.Is(err, protocol.ErrInvalidHello) {
24 t.Fatalf("UnmarshalHello(v21) error = %v, want ErrInvalidHello", err)24 t.Fatalf("UnmarshalHello(v21) error = %v, want ErrInvalidHello", err)
25 }25 }
26 }26 }
27 27
28 func TestFinalHelloGoldenStrict(t *testing.T) {28 func TestFinalHelloGoldenStrict(t *testing.T) {
29- msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}29+ msg := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
30 want := append([]byte{0, 37, 2}, make([]byte, 16+8)...)30 want := append([]byte{0, 37, 2}, make([]byte, 16+8)...)
31 want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)31 want = append(want, 0, 0, 0, 80, 0, 24, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
32 want = append(want, 0, 0)32 want = append(want, 0, 0)
@@ -46,7 +46,7 @@ func TestFinalHelloGoldenStrict(t *testing.T) {
46 }46 }
47 47
48 func TestHelloDeclaresKittyDirectGraphicsAtWireTail(t *testing.T) {48 func TestHelloDeclaresKittyDirectGraphicsAtWireTail(t *testing.T) {
49- msg := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, KittyDirectGraphics: true}49+ msg := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}, KittyDirectGraphics: true}
50 payload := MarshalHello(msg)50 payload := MarshalHello(msg)
51 if len(payload) == 0 || payload[len(payload)-1] != 1 {51 if len(payload) == 0 || payload[len(payload)-1] != 1 {
52 t.Fatalf("Hello capability tail = %x, want trailing true declaration", payload)52 t.Fatalf("Hello capability tail = %x, want trailing true declaration", payload)
@@ -61,20 +61,20 @@ func TestHelloDeclaresKittyDirectGraphicsAtWireTail(t *testing.T) {
61 }61 }
62 62
63 func TestFinalHelloSemanticValidation(t *testing.T) {63 func TestFinalHelloSemanticValidation(t *testing.T) {
64- valid := Hello{Version: ProtocolVersion, Intent: IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}64+ valid := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}
65 for _, tt := range []struct {65 for _, tt := range []struct {
66 name string66 name string
67- mutate func(*Hello)67+ mutate func(*protocol.Hello)
68 }{68 }{
69- {name: "intent", mutate: func(h *Hello) { h.Intent = 99 }},69+ {name: "intent", mutate: func(h *protocol.Hello) { h.Intent = 99 }},
70- {name: "zero columns", mutate: func(h *Hello) { h.Size.Cols = 0 }},70+ {name: "zero columns", mutate: func(h *protocol.Hello) { h.Size.Cols = 0 }},
71- {name: "zero rows", mutate: func(h *Hello) { h.Size.Rows = 0 }},71+ {name: "zero rows", mutate: func(h *protocol.Hello) { h.Size.Rows = 0 }},
72- {name: "area", mutate: func(h *Hello) { h.Size = domain.Size{Cols: 513, Rows: 512} }},72+ {name: "area", mutate: func(h *protocol.Hello) { h.Size = domain.Size{Cols: 513, Rows: 512} }},
73 } {73 } {
74 t.Run(tt.name, func(t *testing.T) {74 t.Run(tt.name, func(t *testing.T) {
75 msg := valid75 msg := valid
76 tt.mutate(&msg)76 tt.mutate(&msg)
77- if err := ValidateHello(msg); err == nil {77+ if err := protocol.ValidateHello(msg); err == nil {
78 t.Fatal("ValidateHello accepted malformed semantics")78 t.Fatal("ValidateHello accepted malformed semantics")
79 }79 }
80 })80 })
@@ -195,7 +195,7 @@ func TestFinalAckGoldenStrict(t *testing.T) {
195 }195 }
196 196
197 func TestFinalAttachTargetGoldenStrict(t *testing.T) {197 func TestFinalAttachTargetGoldenStrict(t *testing.T) {
198- msg := AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach}198+ msg := protocol.AttachTarget{Endpoint: "host", Session: "work", Intent: protocol.IntentAttach}
199 want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', 2, 0, 0, 0, 0}199 want := []byte{0, 4, 'h', 'o', 's', 't', 0, 4, 'w', 'o', 'r', 'k', 2, 0, 0, 0, 0}
200 got := MarshalAttachTarget(msg)200 got := MarshalAttachTarget(msg)
201 if !bytes.Equal(got, want) {201 if !bytes.Equal(got, want) {
@@ -207,12 +207,12 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
207 }207 }
208 assertAllPrefixesFail(t, got, UnmarshalAttachTarget)208 assertAllPrefixesFail(t, got, UnmarshalAttachTarget)
209 assertTrailingGarbageFails(t, got, UnmarshalAttachTarget)209 assertTrailingGarbageFails(t, got, UnmarshalAttachTarget)
210- local := AttachTarget{Session: "work", Intent: IntentAttach}210+ local := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}
211 localPayload := MarshalAttachTarget(local)211 localPayload := MarshalAttachTarget(local)
212 if localPayload == nil {212 if localPayload == nil {
213 t.Fatal("MarshalAttachTarget rejected same-peer route handoff")213 t.Fatal("MarshalAttachTarget rejected same-peer route handoff")
214 }214 }
215- wantLocal := []byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', IntentAttach, 0, 0, 0, 0}215+ wantLocal := []byte{0, 0, 0, 4, 'w', 'o', 'r', 'k', protocol.IntentAttach, 0, 0, 0, 0}
216 if !bytes.Equal(localPayload, wantLocal) {216 if !bytes.Equal(localPayload, wantLocal) {
217 t.Fatalf("same-peer bytes = %x, want %x", localPayload, wantLocal)217 t.Fatalf("same-peer bytes = %x, want %x", localPayload, wantLocal)
218 }218 }
@@ -222,7 +222,7 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
222 }222 }
223 assertAllPrefixesFail(t, localPayload, UnmarshalAttachTarget)223 assertAllPrefixesFail(t, localPayload, UnmarshalAttachTarget)
224 assertTrailingGarbageFails(t, localPayload, UnmarshalAttachTarget)224 assertTrailingGarbageFails(t, localPayload, UnmarshalAttachTarget)
225- for _, bad := range []AttachTarget{{Endpoint: "host", Intent: IntentAttach}, {Endpoint: "host", Session: "work", Intent: 99}} {225+ for _, bad := range []protocol.AttachTarget{{Endpoint: "host", Intent: protocol.IntentAttach}, {Endpoint: "host", Session: "work", Intent: 99}} {
226 if got := MarshalAttachTarget(bad); got != nil {226 if got := MarshalAttachTarget(bad); got != nil {
227 t.Fatalf("MarshalAttachTarget(%+v) = %x, want nil", bad, got)227 t.Fatalf("MarshalAttachTarget(%+v) = %x, want nil", bad, got)
228 }228 }
@@ -232,9 +232,9 @@ func TestFinalAttachTargetGoldenStrict(t *testing.T) {
232 func TestAttachTargetExactTargetWireStrict(t *testing.T) {232 func TestAttachTargetExactTargetWireStrict(t *testing.T) {
233 var lifecycle domain.SessionLifecycleID233 var lifecycle domain.SessionLifecycleID
234 lifecycle[0] = 1234 lifecycle[0] = 1
235- target := AttachTarget{Session: "work", Intent: IntentAttach, ExactTarget: &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}235+ target := protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach, ExactTarget: &protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}, SamePeer: true}
236 payload := MarshalAttachTarget(target)236 payload := MarshalAttachTarget(target)
237- 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', protocol.IntentAttach, 0, 0, 1, 1}, make([]byte, 15)...)
238 want = append(want, 0, 4, 'w', 'o', 'r', 'k', 1)238 want = append(want, 0, 4, 'w', 'o', 'r', 'k', 1)
239 if !bytes.Equal(payload, want) {239 if !bytes.Equal(payload, want) {
240 t.Fatalf("exact target bytes = %x, want %x", payload, want)240 t.Fatalf("exact target bytes = %x, want %x", payload, want)
@@ -261,7 +261,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
261 {261 {
262 name: "hello boolean",262 name: "hello boolean",
263 payload: func() []byte {263 payload: func() []byte {
264- b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})264+ b := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
265 b[len(b)-8] = 2265 b[len(b)-8] = 2
266 return b266 return b
267 }(),267 }(),
@@ -270,7 +270,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
270 {270 {
271 name: "hello remote boolean",271 name: "hello remote boolean",
272 payload: func() []byte {272 payload: func() []byte {
273- b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})273+ b := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
274 b[len(b)-2] = 2274 b[len(b)-2] = 2
275 return b275 return b
276 }(),276 }(),
@@ -279,7 +279,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
279 {279 {
280 name: "hello Kitty graphics boolean",280 name: "hello Kitty graphics boolean",
281 payload: func() []byte {281 payload: func() []byte {
282- b := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})282+ b := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
283 b[len(b)-1] = 2283 b[len(b)-1] = 2
284 return b284 return b
285 }(),285 }(),
@@ -288,7 +288,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
288 {288 {
289 name: "attach target same-peer boolean",289 name: "attach target same-peer boolean",
290 payload: func() []byte {290 payload: func() []byte {
291- b := MarshalAttachTarget(AttachTarget{Endpoint: "host", Session: "work", Intent: IntentAttach})291+ b := MarshalAttachTarget(protocol.AttachTarget{Endpoint: "host", Session: "work", Intent: protocol.IntentAttach})
292 b[len(b)-1] = 2292 b[len(b)-1] = 2
293 return b293 return b
294 }(),294 }(),
@@ -297,7 +297,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
297 {297 {
298 name: "welcome boolean",298 name: "welcome boolean",
299 payload: func() []byte {299 payload: func() []byte {
300- b := MarshalWelcome(Welcome{SessionID: "id"})300+ b := MarshalWelcome(protocol.Welcome{SessionID: "id"})
301 b[6] = 2301 b[6] = 2
302 return b302 return b
303 }(),303 }(),
@@ -307,7 +307,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
307 {307 {
308 name: "kill boolean",308 name: "kill boolean",
309 payload: func() []byte {309 payload: func() []byte {
310- b := MarshalKill(Kill{All: true})310+ b := MarshalKill(protocol.Kill{All: true})
311 b[len(b)-1] = 2311 b[len(b)-1] = 2
312 return b312 return b
313 }(),313 }(),
@@ -316,7 +316,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
316 {316 {
317 name: "sessions ephemeral boolean",317 name: "sessions ephemeral boolean",
318 payload: func() []byte {318 payload: func() []byte {
319- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})319+ b := MarshalSessions(protocol.Sessions{Sessions: []protocol.SessionInfo{{State: protocol.SessionUp}}})
320 b[6] = 2320 b[6] = 2
321 return b321 return b
322 }(),322 }(),
@@ -325,7 +325,7 @@ func TestFinalClosedWireValuesRejectUnknownEnumsAndBooleans(t *testing.T) {
325 {325 {
326 name: "sessions attached boolean",326 name: "sessions attached boolean",
327 payload: func() []byte {327 payload: func() []byte {
328- b := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: SessionUp}}})328+ b := MarshalSessions(protocol.Sessions{Sessions: []protocol.SessionInfo{{State: protocol.SessionUp}}})
329 b[9] = 2329 b[9] = 2
330 return b330 return b
331 }(),331 }(),
M internal/ports/wire_test.go +92 -92
@@ -63,14 +63,14 @@ func appendNoNavigationTail(payload []byte) []byte {
63 func TestHelloGoldenAndRoundTrip(t *testing.T) {63 func TestHelloGoldenAndRoundTrip(t *testing.T) {
64 tests := []struct {64 tests := []struct {
65 name string65 name string
66- msg Hello66+ msg protocol.Hello
67 want []byte67 want []byte
68 }{68 }{
69 {69 {
70 name: "typical",70 name: "typical",
71- msg: Hello{71+ msg: protocol.Hello{
72 Version: 1,72 Version: 1,
73- Intent: IntentNew,73+ Intent: protocol.IntentNew,
74 ClientID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},74 ClientID: [16]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10},
75 ResumeToken: 0x0102030405060708,75 ResumeToken: 0x0102030405060708,
76 Name: "w0",76 Name: "w0",
@@ -84,9 +84,9 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) {
84 },84 },
85 {85 {
86 name: "empty strings",86 name: "empty strings",
87- msg: Hello{87+ msg: protocol.Hello{
88 Version: 1,88 Version: 1,
89- Intent: IntentEphemeral,89+ Intent: protocol.IntentEphemeral,
90 Name: "",90 Name: "",
91 Size: domain.Size{Cols: 1, Rows: 1},91 Size: domain.Size{Cols: 1, Rows: 1},
92 TermEnv: "",92 TermEnv: "",
@@ -129,8 +129,8 @@ func TestHelloGoldenAndRoundTrip(t *testing.T) {
129 129
130 func TestHelloEnvironmentCodec(t *testing.T) {130 func TestHelloEnvironmentCodec(t *testing.T) {
131 t.Run("literal wire layout", func(t *testing.T) {131 t.Run("literal wire layout", func(t *testing.T) {
132- msg := Hello{132+ msg := protocol.Hello{
133- Version: ProtocolVersion,133+ Version: protocol.Version,
134 Size: domain.Size{Cols: 1, Rows: 1},134 Size: domain.Size{Cols: 1, Rows: 1},
135 Env: []string{"A=B", "XY=123"},135 Env: []string{"A=B", "XY=123"},
136 }136 }
@@ -163,20 +163,20 @@ func TestHelloEnvironmentCodec(t *testing.T) {
163 t.Run("lossless order and values", func(t *testing.T) {163 t.Run("lossless order and values", func(t *testing.T) {
164 entryOverUint16 := "LARGE=" + string(bytes.Repeat([]byte("x"), 65536))164 entryOverUint16 := "LARGE=" + string(bytes.Repeat([]byte("x"), 65536))
165 want := []string{"A=first", "TOKEN=a=b=c", "EMPTY=", entryOverUint16, "A=second"}165 want := []string{"A=first", "TOKEN=a=b=c", "EMPTY=", entryOverUint16, "A=second"}
166- payload := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}, Env: want})166+ payload := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}, Env: want})
167 got, err := UnmarshalHello(payload)167 got, err := UnmarshalHello(payload)
168 require.NoError(t, err)168 require.NoError(t, err)
169 require.Equal(t, want, got.Env)169 require.Equal(t, want, got.Env)
170 })170 })
171 171
172 t.Run("empty", func(t *testing.T) {172 t.Run("empty", func(t *testing.T) {
173- payload := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}, Env: []string{}})173+ payload := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}, Env: []string{}})
174 got, err := UnmarshalHello(payload)174 got, err := UnmarshalHello(payload)
175 require.NoError(t, err)175 require.NoError(t, err)
176 require.Empty(t, got.Env)176 require.Empty(t, got.Env)
177 })177 })
178 178
179- base := MarshalHello(Hello{Version: ProtocolVersion, Size: domain.Size{Cols: 1, Rows: 1}})179+ base := MarshalHello(protocol.Hello{Version: protocol.Version, Size: domain.Size{Cols: 1, Rows: 1}})
180 withCount := func(count byte) []byte {180 withCount := func(count byte) []byte {
181 payload := append([]byte(nil), base...)181 payload := append([]byte(nil), base...)
182 // The uint32 environment count occupies bytes len-13 through len-10.182 // The uint32 environment count occupies bytes len-13 through len-10.
@@ -214,11 +214,11 @@ func TestHelloEnvironmentCodec(t *testing.T) {
214 func TestInputGoldenAndRoundTrip(t *testing.T) {214 func TestInputGoldenAndRoundTrip(t *testing.T) {
215 tests := []struct {215 tests := []struct {
216 name string216 name string
217- msg Input217+ msg protocol.Input
218 want []byte218 want []byte
219 }{219 }{
220- {name: "data", msg: Input{InputSeq: 0x0102030405060708, Data: []byte("hi")}, want: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x68, 0x69}},220+ {name: "data", msg: protocol.Input{InputSeq: 0x0102030405060708, Data: []byte("hi")}, want: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x68, 0x69}},
221- {name: "empty", msg: Input{InputSeq: 0, Data: nil}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},221+ {name: "empty", msg: protocol.Input{InputSeq: 0, Data: nil}, want: []byte{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}},
222 }222 }
223 223
224 for _, tt := range tests {224 for _, tt := range tests {
@@ -367,7 +367,7 @@ func TestThemeGenerationClearedWireGoldenPreservesProtocolVersion(t *testing.T)
367 }367 }
368 want := append([]byte{0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00}, make([]byte, 48)...)368 want := append([]byte{0x0f, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x00}, make([]byte, 48)...)
369 require.Equal(t, want, MarshalTheme(cleared))369 require.Equal(t, want, MarshalTheme(cleared))
370- require.Equal(t, uint16(37), ProtocolVersion)370+ require.Equal(t, uint16(37), protocol.Version)
371 }371 }
372 372
373 func TestResizeGoldenAndRoundTrip(t *testing.T) {373 func TestResizeGoldenAndRoundTrip(t *testing.T) {
@@ -394,7 +394,7 @@ func TestResizeGoldenAndRoundTrip(t *testing.T) {
394 }394 }
395 395
396 func TestDetachGoldenAndRoundTrip(t *testing.T) {396 func TestDetachGoldenAndRoundTrip(t *testing.T) {
397- got := MarshalDetach(Detach{})397+ got := MarshalDetach(protocol.Detach{})
398 if len(got) != 0 {398 if len(got) != 0 {
399 t.Fatalf("MarshalDetach() = %#v, want empty", got)399 t.Fatalf("MarshalDetach() = %#v, want empty", got)
400 }400 }
@@ -402,14 +402,14 @@ func TestDetachGoldenAndRoundTrip(t *testing.T) {
402 if err != nil {402 if err != nil {
403 t.Fatalf("UnmarshalDetach() error = %v", err)403 t.Fatalf("UnmarshalDetach() error = %v", err)
404 }404 }
405- if back != (Detach{}) {405+ if back != (protocol.Detach{}) {
406- t.Fatalf("round trip = %#v, want %#v", back, Detach{})406+ t.Fatalf("round trip = %#v, want %#v", back, protocol.Detach{})
407 }407 }
408 assertTrailingGarbageFails(t, got, UnmarshalDetach)408 assertTrailingGarbageFails(t, got, UnmarshalDetach)
409 }409 }
410 410
411 func TestPingGoldenAndRoundTrip(t *testing.T) {411 func TestPingGoldenAndRoundTrip(t *testing.T) {
412- got := MarshalPing(Ping{})412+ got := MarshalPing(protocol.Ping{})
413 if len(got) != 0 {413 if len(got) != 0 {
414 t.Fatalf("MarshalPing() = %#v, want empty", got)414 t.Fatalf("MarshalPing() = %#v, want empty", got)
415 }415 }
@@ -417,14 +417,14 @@ func TestPingGoldenAndRoundTrip(t *testing.T) {
417 if err != nil {417 if err != nil {
418 t.Fatalf("UnmarshalPing() error = %v", err)418 t.Fatalf("UnmarshalPing() error = %v", err)
419 }419 }
420- if back != (Ping{}) {420+ if back != (protocol.Ping{}) {
421- t.Fatalf("round trip = %#v, want %#v", back, Ping{})421+ t.Fatalf("round trip = %#v, want %#v", back, protocol.Ping{})
422 }422 }
423 assertTrailingGarbageFails(t, got, UnmarshalPing)423 assertTrailingGarbageFails(t, got, UnmarshalPing)
424 }424 }
425 425
426 func TestPongGoldenAndRoundTrip(t *testing.T) {426 func TestPongGoldenAndRoundTrip(t *testing.T) {
427- got := MarshalPong(Pong{})427+ got := MarshalPong(protocol.Pong{})
428 if len(got) != 0 {428 if len(got) != 0 {
429 t.Fatalf("MarshalPong() = %#v, want empty", got)429 t.Fatalf("MarshalPong() = %#v, want empty", got)
430 }430 }
@@ -432,14 +432,14 @@ func TestPongGoldenAndRoundTrip(t *testing.T) {
432 if err != nil {432 if err != nil {
433 t.Fatalf("UnmarshalPong() error = %v", err)433 t.Fatalf("UnmarshalPong() error = %v", err)
434 }434 }
435- if back != (Pong{}) {435+ if back != (protocol.Pong{}) {
436- t.Fatalf("round trip = %#v, want %#v", back, Pong{})436+ t.Fatalf("round trip = %#v, want %#v", back, protocol.Pong{})
437 }437 }
438 assertTrailingGarbageFails(t, got, UnmarshalPong)438 assertTrailingGarbageFails(t, got, UnmarshalPong)
439 }439 }
440 440
441 func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {441 func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {
442- got := MarshalWelcome(Welcome{442+ got := MarshalWelcome(protocol.Welcome{
443 SessionName: "work",443 SessionName: "work",
444 CommittedIdentity: &protocol.CommittedRouteIdentity{444 CommittedIdentity: &protocol.CommittedRouteIdentity{
445 Target: protocol.ExactSessionTarget{SessionName: "different"},445 Target: protocol.ExactSessionTarget{SessionName: "different"},
@@ -449,7 +449,7 @@ func TestMarshalWelcomeRejectsInvalidCommittedIdentity(t *testing.T) {
449 }449 }
450 450
451 func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {451 func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
452- base := Welcome{452+ base := protocol.Welcome{
453 SessionID: "session",453 SessionID: "session",
454 SessionName: "work",454 SessionName: "work",
455 Ephemeral: true,455 Ephemeral: true,
@@ -458,9 +458,9 @@ func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
458 Ephemeral: true,458 Ephemeral: true,
459 },459 },
460 }460 }
461- for name, mutate := range map[string]func(*Welcome){461+ for name, mutate := range map[string]func(*protocol.Welcome){
462- "session name": func(w *Welcome) { w.SessionName = "other" },462+ "session name": func(w *protocol.Welcome) { w.SessionName = "other" },
463- "ephemeral": func(w *Welcome) { w.Ephemeral = false },463+ "ephemeral": func(w *protocol.Welcome) { w.Ephemeral = false },
464 } {464 } {
465 t.Run(name, func(t *testing.T) {465 t.Run(name, func(t *testing.T) {
466 got := base466 got := base
@@ -474,17 +474,17 @@ func TestMarshalWelcomeRejectsCommittedIdentityMetadataMismatch(t *testing.T) {
474 func TestWelcomeGoldenAndRoundTrip(t *testing.T) {474 func TestWelcomeGoldenAndRoundTrip(t *testing.T) {
475 tests := []struct {475 tests := []struct {
476 name string476 name string
477- msg Welcome477+ msg protocol.Welcome
478 want []byte478 want []byte
479 }{479 }{
480 {480 {
481 name: "ephemeral",481 name: "ephemeral",
482- msg: Welcome{SessionID: "sess-1", SessionName: "main", Ephemeral: true, ResumeToken: 0x0102030405060708, Capabilities: CapabilityResume | CapabilityPredict},482+ msg: protocol.Welcome{SessionID: "sess-1", SessionName: "main", Ephemeral: true, ResumeToken: 0x0102030405060708, Capabilities: protocol.CapabilityResume | protocol.CapabilityPredict},
483 want: []byte{0x00, 0x06, 0x73, 0x65, 0x73, 0x73, 0x2d, 0x31, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x05, 0x00},483 want: []byte{0x00, 0x06, 0x73, 0x65, 0x73, 0x73, 0x2d, 0x31, 0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e, 0x01, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x00, 0x00, 0x00, 0x05, 0x00},
484 },484 },
485 {485 {
486 name: "non-ephemeral empty name",486 name: "non-ephemeral empty name",
487- msg: Welcome{SessionID: "abc", SessionName: "", Ephemeral: false},487+ msg: protocol.Welcome{SessionID: "abc", SessionName: "", Ephemeral: false},
488 want: []byte{0x00, 0x03, 0x61, 0x62, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},488 want: []byte{0x00, 0x03, 0x61, 0x62, 0x63, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00},
489 },489 },
490 }490 }
@@ -511,22 +511,22 @@ func TestWelcomeGoldenAndRoundTrip(t *testing.T) {
511 }511 }
512 512
513 func TestCommandErrorCodes(t *testing.T) {513 func TestCommandErrorCodes(t *testing.T) {
514- require.Equal(t, uint16(6), ErrUnknownCommand)514+ require.Equal(t, uint16(6), protocol.ErrUnknownCommand)
515- require.Equal(t, uint16(7), ErrNotScriptable)515+ require.Equal(t, uint16(7), protocol.ErrNotScriptable)
516- require.Equal(t, uint16(8), ErrInvalidCommandArgs)516+ require.Equal(t, uint16(8), protocol.ErrInvalidCommandArgs)
517- require.Equal(t, uint16(9), ErrNoSuchTarget)517+ require.Equal(t, uint16(9), protocol.ErrNoSuchTarget)
518- require.Equal(t, uint16(10), ErrAmbiguousTarget)518+ require.Equal(t, uint16(10), protocol.ErrAmbiguousTarget)
519 }519 }
520 520
521 func TestCommandRequestGoldenAndRoundTrip(t *testing.T) {521 func TestCommandRequestGoldenAndRoundTrip(t *testing.T) {
522 tests := []struct {522 tests := []struct {
523 name string523 name string
524- msg CommandRequest524+ msg protocol.CommandRequest
525 want []byte525 want []byte
526 }{526 }{
527 {527 {
528 name: "minimal",528 name: "minimal",
529- msg: CommandRequest{Version: ProtocolVersion, Slug: "split-right"},529+ msg: protocol.CommandRequest{Version: protocol.Version, Slug: "split-right"},
530 want: []byte{530 want: []byte{
531 0x00, 0x25,531 0x00, 0x25,
532 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID532 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID
@@ -540,8 +540,8 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) {
540 },540 },
541 {541 {
542 name: "full",542 name: "full",
543- msg: CommandRequest{543+ msg: protocol.CommandRequest{
544- Version: ProtocolVersion,544+ Version: protocol.Version,
545 Self: true,545 Self: true,
546 Slug: "toast",546 Slug: "toast",
547 Args: []string{"-l", "warn", "tests KO"},547 Args: []string{"-l", "warn", "tests KO"},
@@ -590,8 +590,8 @@ func TestCommandRequestGoldenAndRoundTrip(t *testing.T) {
590 }590 }
591 591
592 func TestMarshalCommandRequestRejectsTooManyArguments(t *testing.T) {592 func TestMarshalCommandRequestRejectsTooManyArguments(t *testing.T) {
593- payload, err := MarshalCommandRequest(CommandRequest{593+ payload, err := MarshalCommandRequest(protocol.CommandRequest{
594- Version: ProtocolVersion,594+ Version: protocol.Version,
595 Slug: "toast",595 Slug: "toast",
596 Args: make([]string, math.MaxUint16+1),596 Args: make([]string, math.MaxUint16+1),
597 })597 })
@@ -601,7 +601,7 @@ func TestMarshalCommandRequestRejectsTooManyArguments(t *testing.T) {
601 }601 }
602 602
603 func TestCommandRequestRejectsImpossibleArgumentCount(t *testing.T) {603 func TestCommandRequestRejectsImpossibleArgumentCount(t *testing.T) {
604- payload, err := MarshalCommandRequest(CommandRequest{Version: ProtocolVersion, Slug: "toast"})604+ payload, err := MarshalCommandRequest(protocol.CommandRequest{Version: protocol.Version, Slug: "toast"})
605 require.NoError(t, err)605 require.NoError(t, err)
606 argCountOffset := 2 + 8 + 1 + 1 + 2 + len("toast")606 argCountOffset := 2 + 8 + 1 + 1 + 2 + len("toast")
607 payload[argCountOffset] = 0xff607 payload[argCountOffset] = 0xff
@@ -615,12 +615,12 @@ func TestCommandRequestRejectsImpossibleArgumentCount(t *testing.T) {
615 func TestCommandResultGoldenAndRoundTrip(t *testing.T) {615 func TestCommandResultGoldenAndRoundTrip(t *testing.T) {
616 tests := []struct {616 tests := []struct {
617 name string617 name string
618- msg CommandResult618+ msg protocol.CommandResult
619 want []byte619 want []byte
620 }{620 }{
621 {621 {
622 name: "error",622 name: "error",
623- msg: CommandResult{Code: ErrNoSuchTarget, Text: "no such session"},623+ msg: protocol.CommandResult{Code: protocol.ErrNoSuchTarget, Text: "no such session"},
624 want: []byte{624 want: []byte{
625 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID625 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID
626 0x00,626 0x00,
@@ -631,7 +631,7 @@ func TestCommandResultGoldenAndRoundTrip(t *testing.T) {
631 },631 },
632 {632 {
633 name: "success with output",633 name: "success with output",
634- msg: CommandResult{OK: true, Text: "listed", Output: "ID\tFOCUSED\n"},634+ msg: protocol.CommandResult{OK: true, Text: "listed", Output: "ID\tFOCUSED\n"},
635 want: []byte{635 want: []byte{
636 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID636 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // request ID
637 0x01,637 0x01,
@@ -656,7 +656,7 @@ func TestCommandResultGoldenAndRoundTrip(t *testing.T) {
656 }656 }
657 657
658 func TestErrorMsgGoldenAndRoundTrip(t *testing.T) {658 func TestErrorMsgGoldenAndRoundTrip(t *testing.T) {
659- msg := ErrorMsg{Code: ErrNameTaken, Text: "name taken"}659+ msg := protocol.ErrorMsg{Code: protocol.ErrNameTaken, Text: "name taken"}
660 want := []byte{0x00, 0x03, 0x00, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x74, 0x61, 0x6b, 0x65, 0x6e}660 want := []byte{0x00, 0x03, 0x00, 0x0a, 0x6e, 0x61, 0x6d, 0x65, 0x20, 0x74, 0x61, 0x6b, 0x65, 0x6e}
661 661
662 got := MarshalErrorMsg(msg)662 got := MarshalErrorMsg(msg)
@@ -722,7 +722,7 @@ func TestAckGoldenAndRoundTrip(t *testing.T) {
722 }722 }
723 723
724 func TestDetachedGoldenAndRoundTrip(t *testing.T) {724 func TestDetachedGoldenAndRoundTrip(t *testing.T) {
725- msg := Detached{Reason: ReasonReplaced}725+ msg := protocol.Detached{Reason: protocol.ReasonReplaced}
726 want := []byte{0x03}726 want := []byte{0x03}
727 727
728 got := MarshalDetached(msg)728 got := MarshalDetached(msg)
@@ -744,13 +744,13 @@ func TestDetachedGoldenAndRoundTrip(t *testing.T) {
744 func TestClientNoticeGoldenAndStrict(t *testing.T) {744 func TestClientNoticeGoldenAndStrict(t *testing.T) {
745 tests := []struct {745 tests := []struct {
746 name string746 name string
747- msg ClientNotice747+ msg protocol.ClientNotice
748 want []byte748 want []byte
749 }{749 }{
750- {name: "clipboard fallback", msg: ClientNotice{Action: ClientNoticeClipboardFallback}, want: []byte{0x01}},750+ {name: "clipboard fallback", msg: protocol.ClientNotice{Action: protocol.ClientNoticeClipboardFallback}, want: []byte{0x01}},
751- {name: "clipboard too large", msg: ClientNotice{Action: ClientNoticeClipboardTooLarge}, want: []byte{0x02}},751+ {name: "clipboard too large", msg: protocol.ClientNotice{Action: protocol.ClientNoticeClipboardTooLarge}, want: []byte{0x02}},
752- {name: "link degraded", msg: ClientNotice{Action: ClientNoticeLinkDegraded}, want: []byte{0x03}},752+ {name: "link degraded", msg: protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded}, want: []byte{0x03}},
753- {name: "link connected", msg: ClientNotice{Action: ClientNoticeLinkConnected}, want: []byte{0x04}},753+ {name: "link connected", msg: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}, want: []byte{0x04}},
754 }754 }
755 for _, tt := range tests {755 for _, tt := range tests {
756 t.Run(tt.name, func(t *testing.T) {756 t.Run(tt.name, func(t *testing.T) {
@@ -768,7 +768,7 @@ func TestClientNoticeGoldenAndStrict(t *testing.T) {
768 }768 }
769 769
770 func TestListGoldenAndRoundTrip(t *testing.T) {770 func TestListGoldenAndRoundTrip(t *testing.T) {
771- got := MarshalList(List{})771+ got := MarshalList(protocol.List{})
772 if len(got) != 0 {772 if len(got) != 0 {
773 t.Fatalf("MarshalList() = %#v, want empty", got)773 t.Fatalf("MarshalList() = %#v, want empty", got)
774 }774 }
@@ -776,8 +776,8 @@ func TestListGoldenAndRoundTrip(t *testing.T) {
776 if err != nil {776 if err != nil {
777 t.Fatalf("UnmarshalList() error = %v", err)777 t.Fatalf("UnmarshalList() error = %v", err)
778 }778 }
779- if back != (List{}) {779+ if back != (protocol.List{}) {
780- t.Fatalf("round trip = %#v, want %#v", back, List{})780+ t.Fatalf("round trip = %#v, want %#v", back, protocol.List{})
781 }781 }
782 assertTrailingGarbageFails(t, got, UnmarshalList)782 assertTrailingGarbageFails(t, got, UnmarshalList)
783 }783 }
@@ -785,12 +785,12 @@ func TestListGoldenAndRoundTrip(t *testing.T) {
785 func TestKillGoldenAndRoundTrip(t *testing.T) {785 func TestKillGoldenAndRoundTrip(t *testing.T) {
786 tests := []struct {786 tests := []struct {
787 name string787 name string
788- msg Kill788+ msg protocol.Kill
789 want []byte789 want []byte
790 }{790 }{
791- {name: "named", msg: Kill{Name: "main"}, want: []byte{0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e}},791+ {name: "named", msg: protocol.Kill{Name: "main"}, want: []byte{0x00, 0x04, 0x6d, 0x61, 0x69, 0x6e}},
792- {name: "empty", msg: Kill{Name: ""}, want: []byte{0x00, 0x00}},792+ {name: "empty", msg: protocol.Kill{Name: ""}, want: []byte{0x00, 0x00}},
793- {name: "all", msg: Kill{All: true}, want: []byte{0x00, 0x00, 0x01}},793+ {name: "all", msg: protocol.Kill{All: true}, want: []byte{0x00, 0x00, 0x01}},
794 }794 }
795 795
796 for _, tt := range tests {796 for _, tt := range tests {
@@ -814,7 +814,7 @@ func TestKillGoldenAndRoundTrip(t *testing.T) {
814 if err != nil {814 if err != nil {
815 t.Fatalf("UnmarshalKill(legacy) error = %v", err)815 t.Fatalf("UnmarshalKill(legacy) error = %v", err)
816 }816 }
817- if !reflect.DeepEqual(back, Kill{Name: "main"}) {817+ if !reflect.DeepEqual(back, protocol.Kill{Name: "main"}) {
818 t.Fatalf("legacy round trip = %#v, want named kill", back)818 t.Fatalf("legacy round trip = %#v, want named kill", back)
819 }819 }
820 assertAllPrefixesFail(t, legacy, UnmarshalKill)820 assertAllPrefixesFail(t, legacy, UnmarshalKill)
@@ -824,16 +824,16 @@ func TestKillGoldenAndRoundTrip(t *testing.T) {
824 func TestSessionInfoRecoveryState(t *testing.T) {824 func TestSessionInfoRecoveryState(t *testing.T) {
825 tests := []struct {825 tests := []struct {
826 name string826 name string
827- state SessionState827+ state protocol.SessionState
828 want []byte828 want []byte
829 }{829 }{
830- {name: "up", state: SessionUp, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},830+ {name: "up", state: protocol.SessionUp, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0}},
831- {name: "down", state: SessionDown, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},831+ {name: "down", state: protocol.SessionDown, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1}},
832- {name: "broken", state: SessionBroken, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}},832+ {name: "broken", state: protocol.SessionBroken, want: []byte{0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 2}},
833 }833 }
834 for _, tt := range tests {834 for _, tt := range tests {
835 t.Run(tt.name, func(t *testing.T) {835 t.Run(tt.name, func(t *testing.T) {
836- payload := MarshalSessions(Sessions{Sessions: []SessionInfo{{State: tt.state}}})836+ payload := MarshalSessions(protocol.Sessions{Sessions: []protocol.SessionInfo{{State: tt.state}}})
837 require.Equal(t, tt.want, payload)837 require.Equal(t, tt.want, payload)
838 got, err := UnmarshalSessions(payload)838 got, err := UnmarshalSessions(payload)
839 require.NoError(t, err)839 require.NoError(t, err)
@@ -853,19 +853,19 @@ func TestSessionInfoRecoveryState(t *testing.T) {
853 func TestSessionsGoldenAndRoundTrip(t *testing.T) {853 func TestSessionsGoldenAndRoundTrip(t *testing.T) {
854 tests := []struct {854 tests := []struct {
855 name string855 name string
856- msg Sessions856+ msg protocol.Sessions
857 want []byte857 want []byte
858 }{858 }{
859 {859 {
860 name: "empty",860 name: "empty",
861- msg: Sessions{},861+ msg: protocol.Sessions{},
862 want: []byte{0x00, 0x00},862 want: []byte{0x00, 0x00},
863 },863 },
864 {864 {
865 name: "two",865 name: "two",
866- msg: Sessions{Sessions: []SessionInfo{866+ msg: protocol.Sessions{Sessions: []protocol.SessionInfo{
867- {SessionID: "0", Name: "0", State: SessionUp, Ephemeral: true, Tabs: 1, Attached: false},867+ {SessionID: "0", Name: "0", State: protocol.SessionUp, Ephemeral: true, Tabs: 1, Attached: false},
868- {SessionID: "work", Name: "proj", State: SessionDown, Ephemeral: false, Tabs: 5, Attached: true},868+ {SessionID: "work", Name: "proj", State: protocol.SessionDown, Ephemeral: false, Tabs: 5, Attached: true},
869 }},869 }},
870 want: []byte{870 want: []byte{
871 0x00, 0x02,871 0x00, 0x02,
@@ -907,10 +907,10 @@ func TestSessionsGoldenAndRoundTrip(t *testing.T) {
907 func TestMsgTypeConstantsDistinct(t *testing.T) {907 func TestMsgTypeConstantsDistinct(t *testing.T) {
908 t.Run("intents", func(t *testing.T) {908 t.Run("intents", func(t *testing.T) {
909 vals := map[uint8]string{909 vals := map[uint8]string{
910- IntentEphemeral: "IntentEphemeral",910+ protocol.IntentEphemeral: "IntentEphemeral",
911- IntentNew: "IntentNew",911+ protocol.IntentNew: "IntentNew",
912- IntentAttach: "IntentAttach",912+ protocol.IntentAttach: "IntentAttach",
913- IntentResume: "IntentResume",913+ protocol.IntentResume: "IntentResume",
914 }914 }
915 if len(vals) != 4 {915 if len(vals) != 4 {
916 t.Fatalf("expected 4 distinct intent values, got %d", len(vals))916 t.Fatalf("expected 4 distinct intent values, got %d", len(vals))
@@ -919,12 +919,12 @@ func TestMsgTypeConstantsDistinct(t *testing.T) {
919 919
920 t.Run("error codes", func(t *testing.T) {920 t.Run("error codes", func(t *testing.T) {
921 vals := map[uint16]string{921 vals := map[uint16]string{
922- ErrVersionMismatch: "ErrVersionMismatch",922+ protocol.ErrVersionMismatch: "ErrVersionMismatch",
923- ErrNoSuchSession: "ErrNoSuchSession",923+ protocol.ErrNoSuchSession: "ErrNoSuchSession",
924- ErrNameTaken: "ErrNameTaken",924+ protocol.ErrNameTaken: "ErrNameTaken",
925- ErrServerShutdown: "ErrServerShutdown",925+ protocol.ErrServerShutdown: "ErrServerShutdown",
926- ErrInvalidSessionName: "ErrInvalidSessionName",926+ protocol.ErrInvalidSessionName: "ErrInvalidSessionName",
927- ErrInternal: "ErrInternal",927+ protocol.ErrInternal: "ErrInternal",
928 }928 }
929 if len(vals) != 6 {929 if len(vals) != 6 {
930 t.Fatalf("expected 6 distinct error codes, got %d", len(vals))930 t.Fatalf("expected 6 distinct error codes, got %d", len(vals))
@@ -933,10 +933,10 @@ func TestMsgTypeConstantsDistinct(t *testing.T) {
933 933
934 t.Run("detached reasons", func(t *testing.T) {934 t.Run("detached reasons", func(t *testing.T) {
935 vals := map[uint8]string{935 vals := map[uint8]string{
936- ReasonDetach: "ReasonDetach",936+ protocol.ReasonDetach: "ReasonDetach",
937- ReasonSessionKilled: "ReasonSessionKilled",937+ protocol.ReasonSessionKilled: "ReasonSessionKilled",
938- ReasonServerShutdown: "ReasonServerShutdown",938+ protocol.ReasonServerShutdown: "ReasonServerShutdown",
939- ReasonReplaced: "ReasonReplaced",939+ protocol.ReasonReplaced: "ReasonReplaced",
940 }940 }
941 if len(vals) != 4 {941 if len(vals) != 4 {
942 t.Fatalf("expected 4 distinct detached reasons, got %d", len(vals))942 t.Fatalf("expected 4 distinct detached reasons, got %d", len(vals))
@@ -947,7 +947,7 @@ func TestMsgTypeConstantsDistinct(t *testing.T) {
947 func TestHelloOutputWindowByteExactValues(t *testing.T) {947 func TestHelloOutputWindowByteExactValues(t *testing.T) {
948 for _, window := range []uint8{0, 1, 8} {948 for _, window := range []uint8{0, 1, 8} {
949 t.Run(fmt.Sprintf("window_%d", window), func(t *testing.T) {949 t.Run(fmt.Sprintf("window_%d", window), func(t *testing.T) {
950- hello := Hello{Version: 14, Size: domain.Size{Cols: 1, Rows: 1}, MaxOutputInFlight: window}950+ hello := protocol.Hello{Version: 14, Size: domain.Size{Cols: 1, Rows: 1}, MaxOutputInFlight: window}
951 // The empty Hello has a fixed 1x1 size and zero pixel geometry before the negotiated output window.951 // The empty Hello has a fixed 1x1 size and zero pixel geometry before the negotiated output window.
952 want := append(make([]byte, 42), window, 0, 0, 0, 0, 0, 0)952 want := append(make([]byte, 42), window, 0, 0, 0, 0, 0, 0)
953 want = appendNoNavigationTail(want)953 want = appendNoNavigationTail(want)
@@ -973,12 +973,12 @@ func requireBytesEqual(t *testing.T, want, got []byte) {
973 func TestCommandCorrelationGoldenAndStrict(t *testing.T) {973 func TestCommandCorrelationGoldenAndStrict(t *testing.T) {
974 tests := []struct {974 tests := []struct {
975 name string975 name string
976- msg CommandRequest976+ msg protocol.CommandRequest
977 want []byte977 want []byte
978 }{978 }{
979 {979 {
980 name: "attached",980 name: "attached",
981- msg: CommandRequest{Version: ProtocolVersion, RequestID: 0x0102030405060708, Attached: true, Self: true, Slug: "split-right", Args: []string{"--vertical"}},981+ msg: protocol.CommandRequest{Version: protocol.Version, RequestID: 0x0102030405060708, Attached: true, Self: true, Slug: "split-right", Args: []string{"--vertical"}},
982 want: []byte{982 want: []byte{
983 0x00, 0x25, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01,983 0x00, 0x25, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x01, 0x01,
984 0x00, 0x0b, 's', 'p', 'l', 'i', 't', '-', 'r', 'i', 'g', 'h', 't',984 0x00, 0x0b, 's', 'p', 'l', 'i', 't', '-', 'r', 'i', 'g', 'h', 't',
@@ -988,7 +988,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) {
988 },988 },
989 {989 {
990 name: "control",990 name: "control",
991- msg: CommandRequest{Version: ProtocolVersion, Slug: "ls"},991+ msg: protocol.CommandRequest{Version: protocol.Version, Slug: "ls"},
992 want: []byte{992 want: []byte{
993 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,993 0x00, 0x25, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
994 0x00, 0x02, 'l', 's', 0x00, 0x00,994 0x00, 0x02, 'l', 's', 0x00, 0x00,
@@ -1020,7 +1020,7 @@ func TestCommandCorrelationGoldenAndStrict(t *testing.T) {
1020 }1020 }
1021 1021
1022 func TestCommandResultCorrelationGoldenAndStrict(t *testing.T) {1022 func TestCommandResultCorrelationGoldenAndStrict(t *testing.T) {
1023- msg := CommandResult{RequestID: 0x0102030405060708, OK: true, Code: 7, Text: "ok", Output: "result"}1023+ msg := protocol.CommandResult{RequestID: 0x0102030405060708, OK: true, Code: 7, Text: "ok", Output: "result"}
1024 want := []byte{1024 want := []byte{
1025 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,1025 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,
1026 0x01, 0x00, 0x07, 0x00, 0x02, 'o', 'k', 0x00, 0x00, 0x00, 0x06, 'r', 'e', 's', 'u', 'l', 't',1026 0x01, 0x00, 0x07, 0x00, 0x02, 'o', 'k', 0x00, 0x00, 0x00, 0x06, 'r', 'e', 's', 'u', 'l', 't',
@@ -1040,10 +1040,10 @@ func TestCommandResultCorrelationGoldenAndStrict(t *testing.T) {
1040 }1040 }
1041 1041
1042 func TestOutputResetRequestStrict(t *testing.T) {1042 func TestOutputResetRequestStrict(t *testing.T) {
1043- got := MarshalOutputResetRequest(OutputResetRequest{})1043+ got := MarshalOutputResetRequest(protocol.OutputResetRequest{})
1044 require.Empty(t, got)1044 require.Empty(t, got)
1045 back, err := UnmarshalOutputResetRequest(got)1045 back, err := UnmarshalOutputResetRequest(got)
1046 require.NoError(t, err)1046 require.NoError(t, err)
1047- require.Equal(t, OutputResetRequest{}, back)1047+ require.Equal(t, protocol.OutputResetRequest{}, back)
1048 assertTrailingGarbageFails(t, got, UnmarshalOutputResetRequest)1048 assertTrailingGarbageFails(t, got, UnmarshalOutputResetRequest)
1049 }1049 }
A internal/protocol/command.go +25 -0
@@ -0,0 +1,25 @@
1+package protocol
2+
3+// CommandRequest asks the daemon to run one control command. Version must stay
4+// first so a future payload layout can still be rejected cleanly.
5+type CommandRequest struct {
6+ Version uint16
7+ RequestID uint64
8+ Attached bool
9+ Self bool
10+ Slug string
11+ Args []string
12+ TargetSession string
13+ TargetTab string
14+ TargetPane string
15+ JSON bool
16+}
17+
18+// CommandResult reports a control command's outcome.
19+type CommandResult struct {
20+ RequestID uint64
21+ OK bool
22+ Code uint16
23+ Text string
24+ Output string
25+}
M internal/protocol/navigation.go +10 -0
@@ -86,6 +86,16 @@ func ValidateNavigation(capabilities NavigationCapabilities, overlay StartupOver
86 return nil86 return nil
87 }87 }
88 88
89+func validateHelloNavigation(h Hello) error {
90+ if h.Intent != IntentAttach && h.Intent != IntentResume {
91+ if h.NavigationCapabilities != 0 || h.StartupOverlay != StartupOverlayNone {
92+ return ErrInvalidNavigation
93+ }
94+ return nil
95+ }
96+ return ValidateNavigation(h.NavigationCapabilities, h.StartupOverlay, h.RemoteTarget != nil || h.EnvironmentPolicy == EnvironmentPolicyDaemonOwned)
97+}
98+
89 func ValidateParkedRouteRequest(request ParkedRouteRequest) error {99 func ValidateParkedRouteRequest(request ParkedRouteRequest) error {
90 if request.RequestID == 0 || request.LeaseID.IsZero() {100 if request.RequestID == 0 || request.LeaseID.IsZero() {
91 return ErrInvalidNavigation101 return ErrInvalidNavigation
M internal/protocol/session.go +304 -4
@@ -6,22 +6,122 @@ import (
6 "errors"6 "errors"
7 "fmt"7 "fmt"
8 "math"8 "math"
9+ "time"
9 10
10 renderer "github.com/bnema/vev-vt"11 renderer "github.com/bnema/vev-vt"
11 "github.com/bnema/vev/internal/domain"12 "github.com/bnema/vev/internal/domain"
12 )13 )
13 14
15+// Version is the negotiated vev session protocol version.
16+const Version uint16 = 37
17+
18+// HandshakeTimeout bounds every transport handshake from connect through the
19+// first committed publication.
20+const HandshakeTimeout = 15 * time.Second
21+
14 // MaxOutputDataLen is the largest decoded terminal byte stream accepted by one22 // MaxOutputDataLen is the largest decoded terminal byte stream accepted by one
15 // output publication in protocol version 37.23 // output publication in protocol version 37.
16 const MaxOutputDataLen = (16 << 20) - 5524 const MaxOutputDataLen = (16 << 20) - 55
17 25
26+// ConnectionCapabilities describes transport behavior relevant to session
27+// flow without naming a concrete carriage.
28+type ConnectionCapabilities struct {
29+ OutputDataLimit int
30+ PreferredOutputWindow uint8
31+ AsyncSend bool
32+ OwnedSynchronousSend bool
33+ LinkState bool
34+}
35+
18 var (36 var (
19- ErrInvalidSize = errors.New("ports: invalid terminal size")37+ ErrInvalidSize = errors.New("ports: invalid terminal size")
20- ErrInvalidOutput = errors.New("ports: invalid output")38+ ErrInvalidHello = errors.New("ports: invalid hello")
21- ErrInvalidAck = errors.New("ports: invalid ack")39+ ErrInvalidOutput = errors.New("ports: invalid output")
22- ErrInvalidNavigation = errors.New("ports: invalid navigation")40+ ErrInvalidAck = errors.New("ports: invalid ack")
41+ ErrInvalidAttachTarget = errors.New("ports: invalid attach target")
42+ ErrInvalidNavigation = errors.New("ports: invalid navigation")
43+ ErrInvalidEnvironmentPolicy = errors.New("ports: invalid environment policy")
44+ ErrInvalidClientNotice = errors.New("ports: invalid client notice")
45+ ErrInvalidDetached = errors.New("ports: invalid detached reason")
46+)
47+
48+const (
49+ IntentEphemeral uint8 = 0
50+ IntentNew uint8 = 1
51+ IntentAttach uint8 = 2
52+ IntentResume uint8 = 3
53+)
54+
55+type EnvironmentPolicy uint8
56+
57+const (
58+ EnvironmentPolicyClientOwned EnvironmentPolicy = iota
59+ EnvironmentPolicyDaemonOwned
60+)
61+
62+const (
63+ CapabilityResume uint32 = 1 << 0
64+ CapabilityUDP uint32 = 1 << 1
65+ CapabilityPredict uint32 = 1 << 2
66+)
67+
68+const (
69+ ErrVersionMismatch uint16 = 1
70+ ErrNoSuchSession uint16 = 2
71+ ErrNameTaken uint16 = 3
72+ ErrServerShutdown uint16 = 4
73+ ErrInvalidSessionName uint16 = 5
74+ ErrUnknownCommand uint16 = 6
75+ ErrNotScriptable uint16 = 7
76+ ErrInvalidCommandArgs uint16 = 8
77+ ErrNoSuchTarget uint16 = 9
78+ ErrAmbiguousTarget uint16 = 10
79+ ErrInternal uint16 = 255
23 )80 )
24 81
82+const (
83+ ReasonDetach uint8 = 0
84+ ReasonSessionKilled uint8 = 1
85+ ReasonServerShutdown uint8 = 2
86+ ReasonReplaced uint8 = 3
87+)
88+
89+// Hello is sent by the client immediately after connecting.
90+type Hello struct {
91+ Version uint16
92+ Intent uint8
93+ ClientID [16]byte
94+ ResumeToken uint64
95+ Name string
96+ Size domain.Size
97+ PixelWidth int
98+ PixelHeight int
99+ TermEnv string
100+ Cwd string
101+ TrueColor bool
102+ // KittyDirectGraphics is an explicit declaration that the client has
103+ // probed its direct outer terminal and it accepts Kitty graphics output.
104+ KittyDirectGraphics bool
105+ MaxOutputInFlight uint8
106+ Env []string
107+ RemoteTarget *domain.RemoteSessionTarget
108+ ExactTarget *ExactSessionTarget
109+ PreferredTabID domain.TabStableID
110+ EnvironmentPolicy EnvironmentPolicy
111+ NavigationCapabilities NavigationCapabilities
112+ StartupOverlay StartupOverlay
113+ Remote bool
114+}
115+
116+func (h Hello) Geometry() domain.Geometry {
117+ return domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}.NormalizePixels()
118+}
119+
120+type Input struct {
121+ InputSeq uint64
122+ Data []byte
123+}
124+
25 type Resize struct {125 type Resize struct {
26 Size domain.Size126 Size domain.Size
27 PixelWidth int127 PixelWidth int
@@ -50,11 +150,38 @@ type ImagePush struct {
50 Data []byte150 Data []byte
51 }151 }
52 152
153+type ClientNotice struct{ Action uint8 }
154+
155+const (
156+ ClientNoticeClipboardFallback uint8 = 1
157+ ClientNoticeClipboardTooLarge uint8 = 2
158+ ClientNoticeLinkDegraded uint8 = 3
159+ ClientNoticeLinkConnected uint8 = 4
160+)
161+
162+type Detach struct{}
163+type Ping struct{}
164+type Pong struct{}
165+
53 type Ack struct {166 type Ack struct {
54 Epoch uint64167 Epoch uint64
55 State uint64168 State uint64
56 }169 }
57 170
171+type Welcome struct {
172+ SessionID string
173+ SessionName string
174+ Ephemeral bool
175+ ResumeToken uint64
176+ Capabilities uint32
177+ CommittedIdentity *CommittedRouteIdentity
178+}
179+
180+type ErrorMsg struct {
181+ Code uint16
182+ Text string
183+}
184+
58 type Output struct {185 type Output struct {
59 Epoch uint64186 Epoch uint64
60 Base uint64187 Base uint64
@@ -66,6 +193,145 @@ type Output struct {
66 Data []byte193 Data []byte
67 }194 }
68 195
196+type AttachTarget struct {
197+ Endpoint string
198+ Session string
199+ Intent uint8
200+ RemoteTarget *domain.RemoteSessionTarget
201+ ExactTarget *ExactSessionTarget
202+ EnvironmentPolicy EnvironmentPolicy
203+ SamePeer bool
204+}
205+
206+type Detached struct{ Reason uint8 }
207+type List struct{}
208+
209+type Kill struct {
210+ Name string
211+ All bool
212+}
213+
214+type SessionState uint8
215+
216+const (
217+ SessionUp SessionState = iota
218+ SessionDown
219+ SessionBroken
220+)
221+
222+type SessionInfo struct {
223+ SessionID string
224+ Name string
225+ State SessionState
226+ Ephemeral bool
227+ Tabs uint16
228+ Attached bool
229+}
230+
231+type Sessions struct{ Sessions []SessionInfo }
232+type OutputResetRequest struct{}
233+
234+func validEnvironmentPolicy(policy EnvironmentPolicy) bool {
235+ return policy == EnvironmentPolicyClientOwned || policy == EnvironmentPolicyDaemonOwned
236+}
237+
238+func ValidateEnvironmentPolicy(policy EnvironmentPolicy) error {
239+ if !validEnvironmentPolicy(policy) {
240+ return ErrInvalidEnvironmentPolicy
241+ }
242+ return nil
243+}
244+
245+func validClientNoticeAction(action uint8) bool {
246+ switch action {
247+ case ClientNoticeClipboardFallback, ClientNoticeClipboardTooLarge, ClientNoticeLinkDegraded, ClientNoticeLinkConnected:
248+ return true
249+ default:
250+ return false
251+ }
252+}
253+
254+func validDetachedReason(reason uint8) bool {
255+ switch reason {
256+ case ReasonDetach, ReasonSessionKilled, ReasonServerShutdown, ReasonReplaced:
257+ return true
258+ default:
259+ return false
260+ }
261+}
262+
263+func ValidateClientNotice(m ClientNotice) error {
264+ if !validClientNoticeAction(m.Action) {
265+ return ErrInvalidClientNotice
266+ }
267+ return nil
268+}
269+
270+func ValidateDetached(m Detached) error {
271+ if !validDetachedReason(m.Reason) {
272+ return ErrInvalidDetached
273+ }
274+ return nil
275+}
276+
277+func ValidateHello(h Hello) error {
278+ if h.Intent != IntentEphemeral && h.Intent != IntentNew && h.Intent != IntentAttach && h.Intent != IntentResume {
279+ return ErrInvalidHello
280+ }
281+ if err := ValidateGeometry(domain.Geometry{Size: h.Size, PixelWidth: h.PixelWidth, PixelHeight: h.PixelHeight}); err != nil {
282+ return fmt.Errorf("%w: geometry", ErrInvalidHello)
283+ }
284+ if !validEnvironmentPolicy(h.EnvironmentPolicy) {
285+ return ErrInvalidHello
286+ }
287+ if err := validateHelloNavigation(h); err != nil {
288+ return fmt.Errorf("%w: navigation: %w", ErrInvalidHello, err)
289+ }
290+ if len(h.Name) > math.MaxUint16 || len(h.TermEnv) > math.MaxUint16 || len(h.Cwd) > math.MaxUint16 || len(h.Env) > math.MaxUint32 {
291+ return ErrInvalidHello
292+ }
293+ for _, entry := range h.Env {
294+ if uint64(len(entry)) > math.MaxUint32 {
295+ return ErrInvalidHello
296+ }
297+ }
298+ if h.PreferredTabID != "" {
299+ if h.Intent != IntentAttach && h.Intent != IntentResume {
300+ return ErrInvalidHello
301+ }
302+ if err := domain.ValidateTabStableID(h.PreferredTabID); err != nil {
303+ return fmt.Errorf("%w: preferred tab: %v", ErrInvalidHello, err)
304+ }
305+ }
306+ if h.ExactTarget != nil {
307+ if h.Intent != IntentAttach && h.Intent != IntentResume {
308+ return ErrInvalidHello
309+ }
310+ if err := h.ExactTarget.Validate(); err != nil {
311+ return fmt.Errorf("%w: exact target: %v", ErrInvalidHello, err)
312+ }
313+ if h.Name != h.ExactTarget.SessionName {
314+ return ErrInvalidHello
315+ }
316+ if h.RemoteTarget != nil && (h.RemoteTarget.LifecycleID != h.ExactTarget.LifecycleID || h.RemoteTarget.SessionName != h.ExactTarget.SessionName) {
317+ return ErrInvalidHello
318+ }
319+ }
320+ if h.RemoteTarget == nil {
321+ return nil
322+ }
323+ if h.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || (h.Intent != IntentAttach && h.Intent != IntentResume) {
324+ return ErrInvalidHello
325+ }
326+ if err := validateRemoteTarget(*h.RemoteTarget); err != nil {
327+ return fmt.Errorf("%w: remote target: %v", ErrInvalidHello, err)
328+ }
329+ if h.Name != h.RemoteTarget.SessionName {
330+ return ErrInvalidHello
331+ }
332+ return nil
333+}
334+
69 func ValidateOutput(m Output) error {335 func ValidateOutput(m Output) error {
70 if m.Epoch == 0 {336 if m.Epoch == 0 {
71 return ErrInvalidOutput337 return ErrInvalidOutput
@@ -93,6 +359,40 @@ func ValidateAck(m Ack) error {
93 return nil359 return nil
94 }360 }
95 361
362+func ValidateAttachTarget(m AttachTarget) error {
363+ if m.Session == "" || len(m.Endpoint) > math.MaxUint16 || len(m.Session) > math.MaxUint16 {
364+ return ErrInvalidAttachTarget
365+ }
366+ if err := domain.ValidateSessionName(m.Session); err != nil {
367+ return ErrInvalidAttachTarget
368+ }
369+ if m.Intent != IntentEphemeral && m.Intent != IntentNew && m.Intent != IntentAttach && m.Intent != IntentResume {
370+ return ErrInvalidAttachTarget
371+ }
372+ if m.Intent == IntentResume || !validEnvironmentPolicy(m.EnvironmentPolicy) {
373+ return ErrInvalidAttachTarget
374+ }
375+ if m.ExactTarget != nil && (m.ExactTarget.SessionName != m.Session || m.ExactTarget.Validate() != nil) {
376+ return ErrInvalidAttachTarget
377+ }
378+ if m.SamePeer && (m.Endpoint != "" || m.RemoteTarget != nil || m.ExactTarget == nil) {
379+ return ErrInvalidAttachTarget
380+ }
381+ if m.RemoteTarget == nil {
382+ return nil
383+ }
384+ if m.EnvironmentPolicy != EnvironmentPolicyDaemonOwned || m.Intent != IntentAttach {
385+ return ErrInvalidAttachTarget
386+ }
387+ if err := validateRemoteTarget(*m.RemoteTarget); err != nil {
388+ return fmt.Errorf("%w: remote target: %v", ErrInvalidAttachTarget, err)
389+ }
390+ if m.Endpoint != m.RemoteTarget.Endpoint || m.Session != m.RemoteTarget.SessionName {
391+ return ErrInvalidAttachTarget
392+ }
393+ return nil
394+}
395+
96 func validateRemoteTarget(target domain.RemoteSessionTarget) error {396 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 {397 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")398 return errors.New("remote target string too long")
M internal/protocol/session_test.go +36 -1
@@ -9,6 +9,40 @@ import (
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
10 )10 )
11 11
12+func TestVersionRemainsWireCompatible(t *testing.T) {
13+ require.Equal(t, uint16(37), Version)
14+ require.Equal(t, (16<<20)-55, MaxOutputDataLen)
15+}
16+
17+func TestHelloSemanticValidation(t *testing.T) {
18+ base := Hello{Version: Version, Intent: IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}}
19+ tests := []struct {
20+ name string
21+ mutate func(*Hello)
22+ want error
23+ }{
24+ {name: "valid", mutate: func(*Hello) {}},
25+ {name: "unknown intent", mutate: func(h *Hello) { h.Intent = 99 }, want: ErrInvalidHello},
26+ {name: "invalid geometry", mutate: func(h *Hello) { h.Size.Rows = 0 }, want: ErrInvalidHello},
27+ {name: "unsafe navigation", mutate: func(h *Hello) { h.NavigationCapabilities = NavigationCapabilityBack }, want: ErrInvalidHello},
28+ {name: "exact target name mismatch", mutate: func(h *Hello) {
29+ h.ExactTarget = &ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "other"}
30+ }, want: ErrInvalidHello},
31+ }
32+ for _, tt := range tests {
33+ t.Run(tt.name, func(t *testing.T) {
34+ hello := base
35+ tt.mutate(&hello)
36+ err := ValidateHello(hello)
37+ if tt.want == nil {
38+ require.NoError(t, err)
39+ return
40+ }
41+ require.ErrorIs(t, err, tt.want)
42+ })
43+ }
44+}
45+
12 func TestOutputSemanticValidation(t *testing.T) {46 func TestOutputSemanticValidation(t *testing.T) {
13 valid := Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}47 valid := Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}
14 require.NoError(t, ValidateOutput(valid))48 require.NoError(t, ValidateOutput(valid))
@@ -43,7 +77,8 @@ func TestRouteAndPreviewSemanticValidation(t *testing.T) {
43 require.ErrorIs(t, ValidateRemotePreview(preview), ErrInvalidRemotePreview)77 require.ErrorIs(t, ValidateRemotePreview(preview), ErrInvalidRemotePreview)
44 }78 }
45 79
46-func TestPresentationErrorsPreserveClassification(t *testing.T) {80+func TestSemanticErrorsPreserveClassification(t *testing.T) {
47 require.True(t, errors.Is(ValidateAck(Ack{}), ErrInvalidAck))81 require.True(t, errors.Is(ValidateAck(Ack{}), ErrInvalidAck))
82+ require.True(t, errors.Is(ValidateAttachTarget(AttachTarget{}), ErrInvalidAttachTarget))
48 require.True(t, errors.Is(ValidateParkedRouteRequest(ParkedRouteRequest{}), ErrInvalidNavigation))83 require.True(t, errors.Is(ValidateParkedRouteRequest(ParkedRouteRequest{}), ErrInvalidNavigation))
49 }84 }
M internal/usecase/client/client.go +41 -41
@@ -133,7 +133,7 @@ const sendQueueDepth = 64
133 133
134 // preWelcomeTimeout remains the focused-test name for the shared handshake134 // preWelcomeTimeout remains the focused-test name for the shared handshake
135 // budget, which now also covers connect and initial publication.135 // budget, which now also covers connect and initial publication.
136-const preWelcomeTimeout = ports.HandshakeTimeout136+const preWelcomeTimeout = protocol.HandshakeTimeout
137 137
138 // remoteHostLearnerShutdownTimeout bounds how long Run waits for async remote138 // remoteHostLearnerShutdownTimeout bounds how long Run waits for async remote
139 // host learning after terminal restoration. Learning is best-effort; a stalled139 // host learning after terminal restoration. Learning is best-effort; a stalled
@@ -151,7 +151,7 @@ const (
151 )151 )
152 152
153 // Dependencies supplies the collaborators required by a Runner.153 // Dependencies supplies the collaborators required by a Runner.
154-type AttachHandoffFunc func(ports.AttachTarget) (ports.Dialer, AttachRequest, error)154+type AttachHandoffFunc func(protocol.AttachTarget) (ports.Dialer, AttachRequest, error)
155 155
156 type Dependencies struct {156 type Dependencies struct {
157 Dialer ports.Dialer157 Dialer ports.Dialer
@@ -189,7 +189,7 @@ type AttachRequest struct {
189 ExactTarget *protocol.ExactSessionTarget189 ExactTarget *protocol.ExactSessionTarget
190 PreferredTabID domain.TabStableID190 PreferredTabID domain.TabStableID
191 HostLabel string191 HostLabel string
192- EnvironmentPolicy ports.EnvironmentPolicy192+ EnvironmentPolicy protocol.EnvironmentPolicy
193 NavigationCapabilities protocol.NavigationCapabilities193 NavigationCapabilities protocol.NavigationCapabilities
194 StartupOverlay protocol.StartupOverlay194 StartupOverlay protocol.StartupOverlay
195 }195 }
@@ -379,14 +379,14 @@ func validateAttachRequest(request AttachRequest) error {
379 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 {
380 return fmt.Errorf("vev: invalid session target: %w", err)380 return fmt.Errorf("vev: invalid session target: %w", err)
381 }381 }
382- if err := protocol.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 == protocol.EnvironmentPolicyDaemonOwned); err != nil {
383 return fmt.Errorf("vev: invalid navigation route: %w", err)383 return fmt.Errorf("vev: invalid navigation route: %w", err)
384 }384 }
385 if request.RemoteTarget == nil {385 if request.RemoteTarget == nil {
386- if request.EnvironmentPolicy == ports.EnvironmentPolicyClientOwned {386+ if request.EnvironmentPolicy == protocol.EnvironmentPolicyClientOwned {
387 return nil387 return nil
388 }388 }
389- if request.Remote && request.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned {389+ if request.Remote && request.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned {
390 return nil390 return nil
391 }391 }
392 return errors.New("vev: attach without remote target requires a matching environment policy")392 return errors.New("vev: attach without remote target requires a matching environment policy")
@@ -397,10 +397,10 @@ func validateAttachRequest(request AttachRequest) error {
397 if request.SessionName != request.RemoteTarget.SessionName {397 if request.SessionName != request.RemoteTarget.SessionName {
398 return errors.New("vev: remote attach target session mismatch")398 return errors.New("vev: remote attach target session mismatch")
399 }399 }
400- if request.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {400+ if request.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
401 return errors.New("vev: remote attach target requires daemon-owned environment")401 return errors.New("vev: remote attach target requires daemon-owned environment")
402 }402 }
403- if request.Intent != ports.IntentAttach && request.Intent != ports.IntentResume {403+ if request.Intent != protocol.IntentAttach && request.Intent != protocol.IntentResume {
404 return errors.New("vev: remote attach target requires attach or resume")404 return errors.New("vev: remote attach target requires attach or resume")
405 }405 }
406 return nil406 return nil
@@ -510,9 +510,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
510 dialer = route.dialer510 dialer = route.dialer
511 attemptRequest = route.request511 attemptRequest = route.request
512 if route.resumeToken != 0 {512 if route.resumeToken != 0 {
513- attemptRequest.Intent = ports.IntentResume513+ attemptRequest.Intent = protocol.IntentResume
514 } else {514 } else {
515- attemptRequest.Intent = ports.IntentAttach515+ attemptRequest.Intent = protocol.IntentAttach
516 }516 }
517 attemptRequest.StartupOverlay = protocol.StartupOverlayNone517 attemptRequest.StartupOverlay = protocol.StartupOverlayNone
518 attemptRequest.NavigationCapabilities &^= protocol.NavigationCapabilityBack | protocol.NavigationCapabilityHomePicker518 attemptRequest.NavigationCapabilities &^= protocol.NavigationCapabilityBack | protocol.NavigationCapabilityHomePicker
@@ -524,11 +524,11 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
524 enterHomePicker := func() {524 enterHomePicker := func() {
525 dialer = homeRoute.dialer525 dialer = homeRoute.dialer
526 attemptRequest = homeRoute.request526 attemptRequest = homeRoute.request
527- attemptRequest.Intent = ports.IntentAttach527+ attemptRequest.Intent = protocol.IntentAttach
528 attemptRequest.NavigationCapabilities = protocol.NavigationCapabilityBack528 attemptRequest.NavigationCapabilities = protocol.NavigationCapabilityBack
529 attemptRequest.StartupOverlay = protocol.StartupOverlaySessionPicker529 attemptRequest.StartupOverlay = protocol.StartupOverlaySessionPicker
530 attemptRequest.RemoteTarget = nil530 attemptRequest.RemoteTarget = nil
531- attemptRequest.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned531+ attemptRequest.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned
532 attemptRequest.Remote = homeRoute.request.Remote || r.remote532 attemptRequest.Remote = homeRoute.request.Remote || r.remote
533 resumeToken = 0533 resumeToken = 0
534 remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote)534 remote = syncReconnectRemote(reconnect, homeRoute.request.Remote || r.remote)
@@ -556,11 +556,11 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
556 return attachResult{err: errors.New("vev: home route unavailable")}556 return attachResult{err: errors.New("vev: home route unavailable")}
557 }557 }
558 request := cloneAttachRequest(homeRoute.request)558 request := cloneAttachRequest(homeRoute.request)
559- request.Intent = ports.IntentAttach559+ request.Intent = protocol.IntentAttach
560 request.NavigationCapabilities = protocol.NavigationCapabilityBack560 request.NavigationCapabilities = protocol.NavigationCapabilityBack
561 request.StartupOverlay = protocol.StartupOverlaySessionPicker561 request.StartupOverlay = protocol.StartupOverlaySessionPicker
562 request.RemoteTarget = nil562 request.RemoteTarget = nil
563- request.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned563+ request.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned
564 request.Remote = false564 request.Remote = false
565 if err := validateAttachRequest(request); err != nil {565 if err := validateAttachRequest(request); err != nil {
566 return attachResult{err: err}566 return attachResult{err: err}
@@ -627,7 +627,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
627 return ctx.Err()627 return ctx.Err()
628 }628 }
629 backoff = nextReconnectBackoff(backoff, defaultReconnectBackoff.max)629 backoff = nextReconnectBackoff(backoff, defaultReconnectBackoff.max)
630- attemptRequest.Intent = ports.IntentResume630+ attemptRequest.Intent = protocol.IntentResume
631 continue631 continue
632 }632 }
633 ms.dialed = true633 ms.dialed = true
@@ -733,9 +733,9 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
733 dialer = selection.selected.dialer733 dialer = selection.selected.dialer
734 attemptRequest = cloneAttachRequest(selection.selected.request)734 attemptRequest = cloneAttachRequest(selection.selected.request)
735 if selection.selected.resumeToken != 0 {735 if selection.selected.resumeToken != 0 {
736- attemptRequest.Intent = ports.IntentResume736+ attemptRequest.Intent = protocol.IntentResume
737 } else {737 } else {
738- attemptRequest.Intent = ports.IntentAttach738+ attemptRequest.Intent = protocol.IntentAttach
739 }739 }
740 // Back is transient to the picker overlay. Re-derive the home-picker740 // Back is transient to the picker overlay. Re-derive the home-picker
741 // capability from the selected route instead of trusting request741 // capability from the selected route instead of trusting request
@@ -795,7 +795,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
795 if result.sessionName != "" {795 if result.sessionName != "" {
796 routeRequest.SessionName = result.sessionName796 routeRequest.SessionName = result.sessionName
797 }797 }
798- routeRequest.Intent = ports.IntentAttach798+ routeRequest.Intent = protocol.IntentAttach
799 routeRequest.NavigationCapabilities = 0799 routeRequest.NavigationCapabilities = 0
800 routeRequest.StartupOverlay = protocol.StartupOverlayNone800 routeRequest.StartupOverlay = protocol.StartupOverlayNone
801 homeRoute = &attachRoute{dialer: dialer, request: routeRequest}801 homeRoute = &attachRoute{dialer: dialer, request: routeRequest}
@@ -832,7 +832,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
832 if result.target.ExactTarget != nil {832 if result.target.ExactTarget != nil {
833 nextRequest.ExactTarget = result.target.ExactTarget833 nextRequest.ExactTarget = result.target.ExactTarget
834 }834 }
835- if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned) {835+ if homeRoute != nil && (nextRequest.RemoteTarget != nil || nextRequest.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned) {
836 nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker836 nextRequest.NavigationCapabilities |= protocol.NavigationCapabilityHomePicker
837 }837 }
838 if attemptRequest.StartupOverlay == protocol.StartupOverlaySessionPicker {838 if attemptRequest.StartupOverlay == protocol.StartupOverlaySessionPicker {
@@ -875,7 +875,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
875 continue875 continue
876 }876 }
877 if routeNavigationPending && routeNavigationResumeFallback && resumeNeedsExactAttach(result.err) {877 if routeNavigationPending && routeNavigationResumeFallback && resumeNeedsExactAttach(result.err) {
878- attemptRequest.Intent = ports.IntentAttach878+ attemptRequest.Intent = protocol.IntentAttach
879 resumeToken = 0879 resumeToken = 0
880 routeNavigationResumeFallback = false880 routeNavigationResumeFallback = false
881 continue881 continue
@@ -896,7 +896,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
896 continue896 continue
897 }897 }
898 if returnResumeFallback && resumeNeedsExactAttach(result.err) {898 if returnResumeFallback && resumeNeedsExactAttach(result.err) {
899- attemptRequest.Intent = ports.IntentAttach899+ attemptRequest.Intent = protocol.IntentAttach
900 resumeToken = 0900 resumeToken = 0
901 returnResumeFallback = false901 returnResumeFallback = false
902 continue902 continue
@@ -928,7 +928,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
928 return ctx.Err()928 return ctx.Err()
929 }929 }
930 backoff = nextReconnectBackoff(backoff, defaultReconnectBackoff.max)930 backoff = nextReconnectBackoff(backoff, defaultReconnectBackoff.max)
931- attemptRequest.Intent = ports.IntentResume931+ attemptRequest.Intent = protocol.IntentResume
932 }932 }
933 }933 }
934 934
@@ -1089,7 +1089,7 @@ type attachResult struct {
1089 routePosition *protocol.RoutePosition1089 routePosition *protocol.RoutePosition
1090 welcomed bool1090 welcomed bool
1091 transportClosed bool1091 transportClosed bool
1092- target *ports.AttachTarget1092+ target *protocol.AttachTarget
1093 action protocol.NavigationAction1093 action protocol.NavigationAction
1094 homePickerTarget bool1094 homePickerTarget bool
1095 routeAction *protocol.RouteNavigationAction1095 routeAction *protocol.RouteNavigationAction
@@ -1099,7 +1099,7 @@ type attachResult struct {
1099 // AttachTargetError requests that the composition root replace the current1099 // AttachTargetError requests that the composition root replace the current
1100 // transport with the selected endpoint. The daemon never opens that endpoint.1100 // transport with the selected endpoint. The daemon never opens that endpoint.
1101 type AttachTargetError struct {1101 type AttachTargetError struct {
1102- Target ports.AttachTarget1102+ Target protocol.AttachTarget
1103 }1103 }
1104 1104
1105 func (e *AttachTargetError) Error() string {1105 func (e *AttachTargetError) Error() string {
@@ -1219,8 +1219,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1219 SessionName: request.RemoteTarget.SessionName,1219 SessionName: request.RemoteTarget.SessionName,
1220 }1220 }
1221 }1221 }
1222- hello := ports.Hello{1222+ hello := protocol.Hello{
1223- Version: ports.ProtocolVersion,1223+ Version: protocol.Version,
1224 Intent: intent,1224 Intent: intent,
1225 ClientID: clientID,1225 ClientID: clientID,
1226 ResumeToken: resumeToken,1226 ResumeToken: resumeToken,
@@ -1490,7 +1490,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1490 requestID uint641490 requestID uint64
1491 action protocol.ParkedRouteAction1491 action protocol.ParkedRouteAction
1492 leaseID protocol.ParkedRouteLeaseID1492 leaseID protocol.ParkedRouteLeaseID
1493- fallbackTarget *ports.AttachTarget1493+ fallbackTarget *protocol.AttachTarget
1494 timer ports.Timer1494 timer ports.Timer
1495 }1495 }
1496 var parkedPending *parkedRoutePending1496 var parkedPending *parkedRoutePending
@@ -1600,7 +1600,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1600 awaitParkedFull := func() {1600 awaitParkedFull := func() {
1601 clearParkedFull()1601 clearParkedFull()
1602 parkedWaitingFull = true1602 parkedWaitingFull = true
1603- parkedFullTimer = clk.NewTimer(ports.HandshakeTimeout)1603+ parkedFullTimer = clk.NewTimer(protocol.HandshakeTimeout)
1604 }1604 }
1605 parkedFullC := func() <-chan time.Time {1605 parkedFullC := func() <-chan time.Time {
1606 if parkedFullTimer == nil {1606 if parkedFullTimer == nil {
@@ -1654,7 +1654,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1654 case controlCh <- ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: payload}:1654 case controlCh <- ports.Frame{Type: ports.MsgParkedRouteRequest, Payload: payload}:
1655 parkedPending = &parkedRoutePending{1655 parkedPending = &parkedRoutePending{
1656 requestID: request.RequestID, action: action, leaseID: leaseID,1656 requestID: request.RequestID, action: action, leaseID: leaseID,
1657- timer: clk.NewTimer(ports.HandshakeTimeout),1657+ timer: clk.NewTimer(protocol.HandshakeTimeout),
1658 }1658 }
1659 return nil1659 return nil
1660 case <-loopCtx.Done():1660 case <-loopCtx.Done():
@@ -1827,7 +1827,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1827 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))1827 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))
1828 }1828 }
1829 select {1829 select {
1830- case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(ports.ClientNotice{Action: ports.ClientNoticeLinkConnected})}:1830+ case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}:
1831 case <-loopCtx.Done():1831 case <-loopCtx.Done():
1832 return welcomedResult(nil)1832 return welcomedResult(nil)
1833 }1833 }
@@ -1836,7 +1836,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1836 if ev.State == ports.LinkStateDegraded {1836 if ev.State == ports.LinkStateDegraded {
1837 log.Warn("UDP link degraded")1837 log.Warn("UDP link degraded")
1838 select {1838 select {
1839- case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(ports.ClientNotice{Action: ports.ClientNoticeLinkDegraded})}:1839+ case sendCh <- ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})}:
1840 case <-loopCtx.Done():1840 case <-loopCtx.Done():
1841 return welcomedResult(nil)1841 return welcomedResult(nil)
1842 }1842 }
@@ -1896,7 +1896,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1896 // repeated gaps while that reset is in flight.1896 // repeated gaps while that reset is in flight.
1897 if !outputResetRequested {1897 if !outputResetRequested {
1898 select {1898 select {
1899- case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}:1899+ case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
1900 outputResetRequested = true1900 outputResetRequested = true
1901 case <-loopCtx.Done():1901 case <-loopCtx.Done():
1902 return loopCanceledResult()1902 return loopCanceledResult()
@@ -2111,7 +2111,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2111 transitionWaitingFull = true2111 transitionWaitingFull = true
2112 if !outputResetRequested {2112 if !outputResetRequested {
2113 select {2113 select {
2114- case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}:2114+ case sendCh <- ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
2115 outputResetRequested = true2115 outputResetRequested = true
2116 case <-loopCtx.Done():2116 case <-loopCtx.Done():
2117 return loopCanceledResult()2117 return loopCanceledResult()
@@ -2145,7 +2145,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2145 return welcomedResult(fmt.Errorf("vev: decoding detached: %w", derr))2145 return welcomedResult(fmt.Errorf("vev: decoding detached: %w", derr))
2146 }2146 }
2147 ms.detached = true2147 ms.detached = true
2148- if d.Reason == ports.ReasonDetach {2148+ if d.Reason == protocol.ReasonDetach {
2149 log.Info("detached cleanly", "reason", d.Reason)2149 log.Info("detached cleanly", "reason", d.Reason)
2150 } else {2150 } else {
2151 log.Warn("detached by daemon", "reason", d.Reason)2151 log.Warn("detached by daemon", "reason", d.Reason)
@@ -2785,7 +2785,7 @@ func (p *stdinPump) run() {
2785 return2785 return
2786 }2786 }
2787 inputSeq++2787 inputSeq++
2788- if !send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {2788+ if !send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {
2789 undeliveredMu.Lock()2789 undeliveredMu.Lock()
2790 undelivered = append(undelivered, data...)2790 undelivered = append(undelivered, data...)
2791 undeliveredMu.Unlock()2791 undeliveredMu.Unlock()
@@ -2799,7 +2799,7 @@ func (p *stdinPump) run() {
2799 reader: p.clipboard,2799 reader: p.clipboard,
2800 log: p.logger,2800 log: p.logger,
2801 sendNotice: func(action uint8) {2801 sendNotice: func(action uint8) {
2802- send(ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(ports.ClientNotice{Action: action})})2802+ send(ports.Frame{Type: ports.MsgClientNotice, Payload: ports.MarshalClientNotice(protocol.ClientNotice{Action: action})})
2803 },2803 },
2804 sendImage: func(mime string, data []byte) {2804 sendImage: func(mime string, data []byte) {
2805 inputSeq++2805 inputSeq++
@@ -2945,7 +2945,7 @@ func (p *stdinPump) run() {
2945 disarmMarkerDeadline()2945 disarmMarkerDeadline()
2946 markers.flush(sink)2946 markers.flush(sink)
2947 select {2947 select {
2948- case p.out <- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}:2948+ case p.out <- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})}:
2949 case <-p.ctx.Done():2949 case <-p.ctx.Done():
2950 }2950 }
2951 p.cancel()2951 p.cancel()
@@ -3002,13 +3002,13 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- po
3002 // the user learns why the session went away.3002 // the user learns why the session went away.
3003 func detachedResult(reason uint8) error {3003 func detachedResult(reason uint8) error {
3004 switch reason {3004 switch reason {
3005- case ports.ReasonDetach:3005+ case protocol.ReasonDetach:
3006 return nil3006 return nil
3007- case ports.ReasonSessionKilled:3007+ case protocol.ReasonSessionKilled:
3008 return &DetachedError{Reason: reason, Text: "session was killed"}3008 return &DetachedError{Reason: reason, Text: "session was killed"}
3009- case ports.ReasonServerShutdown:3009+ case protocol.ReasonServerShutdown:
3010 return &DetachedError{Reason: reason, Text: "daemon shut down"}3010 return &DetachedError{Reason: reason, Text: "daemon shut down"}
3011- case ports.ReasonReplaced:3011+ case protocol.ReasonReplaced:
3012 return &DetachedError{Reason: reason, Text: "session taken over by another client"}3012 return &DetachedError{Reason: reason, Text: "session taken over by another client"}
3013 default:3013 default:
3014 return &DetachedError{Reason: reason, Text: "detached by daemon"}3014 return &DetachedError{Reason: reason, Text: "detached by daemon"}
M internal/usecase/client/client_test.go +153 -153
@@ -265,7 +265,7 @@ func attachTestDependencies(transport ports.Transport, terminal ports.Terminal,
265 return testDependencies(transportDialer{transport: transport}, terminal, clock, nil, nil)265 return testDependencies(transportDialer{transport: transport}, terminal, clock, nil, nil)
266 }266 }
267 267
268-const testPreWelcomeTimeout = ports.HandshakeTimeout268+const testPreWelcomeTimeout = protocol.HandshakeTimeout
269 269
270 func newHandshakeClock(t *testing.T, timerCount int) (*portsmocks.MockClock, <-chan chan time.Time) {270 func newHandshakeClock(t *testing.T, timerCount int) (*portsmocks.MockClock, <-chan chan time.Time) {
271 t.Helper()271 t.Helper()
@@ -294,7 +294,7 @@ func TestRunBoundsBlockedDial(t *testing.T) {
294 294
295 errCh := make(chan error, 1)295 errCh := make(chan error, 1)
296 go func() {296 go func() {
297- errCh <- runTestClient(context.Background(), testDependencies(dialer, nil, clk, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main"})297+ errCh <- runTestClient(context.Background(), testDependencies(dialer, nil, clk, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"})
298 }()298 }()
299 timerC := <-createdTimers299 timerC := <-createdTimers
300 <-started300 <-started
@@ -353,7 +353,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
353 353
354 errCh := make(chan error, 1)354 errCh := make(chan error, 1)
355 go func() {355 go func() {
356- errCh <- runTestClient(ctx, testDependencies(dialer, term, clk, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})356+ errCh <- runTestClient(ctx, testDependencies(dialer, term, clk, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
357 }()357 }()
358 358
359 var timerC chan time.Time359 var timerC chan time.Time
@@ -399,7 +399,7 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) {
399 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)399 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
400 defer in.unblock()400 defer in.unblock()
401 401
402- gotHello := make(chan ports.Hello, 1)402+ gotHello := make(chan protocol.Hello, 1)
403 tr := portsmocks.NewMockTransport(t)403 tr := portsmocks.NewMockTransport(t)
404 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()404 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
405 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()405 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
@@ -410,13 +410,13 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) {
410 return nil410 return nil
411 }).Once()411 }).Once()
412 unblock := scriptRecv(tr,412 unblock := scriptRecv(tr,
413- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},413+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
414- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},414+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
415 )415 )
416 defer unblock()416 defer unblock()
417 tr.EXPECT().Close().Return(nil).Once()417 tr.EXPECT().Close().Return(nil).Once()
418 418
419- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})419+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
420 require.NoError(t, err)420 require.NoError(t, err)
421 421
422 select {422 select {
@@ -434,18 +434,18 @@ func TestAttachPublishesCommittedRouteSnapshot(t *testing.T) {
434 defer term.in.unblock()434 defer term.in.unblock()
435 lifecycle := domain.SessionLifecycleID{9, 8, 7}435 lifecycle := domain.SessionLifecycleID{9, 8, 7}
436 tr := &recordingTransport{recvs: []recvItem{436 tr := &recordingTransport{recvs: []recvItem{
437- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{437+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
438 SessionID: "s1",438 SessionID: "s1",
439 SessionName: "main",439 SessionName: "main",
440 CommittedIdentity: &protocol.CommittedRouteIdentity{440 CommittedIdentity: &protocol.CommittedRouteIdentity{
441 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "main"},441 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "main"},
442 },442 },
443 }))},443 }))},
444- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},444+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
445 }}445 }}
446 446
447 err := runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), client.AttachRequest{447 err := runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), client.AttachRequest{
448- Intent: ports.IntentAttach,448+ Intent: protocol.IntentAttach,
449 SessionName: "main",449 SessionName: "main",
450 Origin: protocol.RouteOriginLocal,450 Origin: protocol.RouteOriginLocal,
451 OriginKey: "local",451 OriginKey: "local",
@@ -485,8 +485,8 @@ func TestLocalOnlyHandoffBetweenLocalSessionsKeepsClientRunning(t *testing.T) {
485 firstLifecycle := domain.SessionLifecycleID{1}485 firstLifecycle := domain.SessionLifecycleID{1}
486 secondLifecycle := domain.SessionLifecycleID{2}486 secondLifecycle := domain.SessionLifecycleID{2}
487 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {487 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
488- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{488+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
489- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,489+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
490 CommittedIdentity: &protocol.CommittedRouteIdentity{490 CommittedIdentity: &protocol.CommittedRouteIdentity{
491 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},491 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
492 },492 },
@@ -495,19 +495,19 @@ func TestLocalOnlyHandoffBetweenLocalSessionsKeepsClientRunning(t *testing.T) {
495 secondTarget := protocol.ExactSessionTarget{LifecycleID: secondLifecycle, SessionName: "second"}495 secondTarget := protocol.ExactSessionTarget{LifecycleID: secondLifecycle, SessionName: "second"}
496 first := &recordingTransport{recvs: []recvItem{496 first := &recordingTransport{recvs: []recvItem{
497 {f: welcome("first", firstLifecycle)},497 {f: welcome("first", firstLifecycle)},
498- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{498+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
499- Session: "second", Intent: ports.IntentAttach, ExactTarget: &secondTarget,499+ Session: "second", Intent: protocol.IntentAttach, ExactTarget: &secondTarget,
500- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned, SamePeer: true,500+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, SamePeer: true,
501 }))},501 }))},
502 {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: secondTarget}))},502 {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: secondTarget}))},
503- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},503+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
504 }}504 }}
505 dialer := &sequenceDialer{trs: []ports.Transport{first}}505 dialer := &sequenceDialer{trs: []ports.Transport{first}}
506 506
507 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{
508- Intent: ports.IntentAttach, SessionName: "first",508+ Intent: protocol.IntentAttach, SessionName: "first",
509 Origin: protocol.RouteOriginLocal, OriginKey: "local",509 Origin: protocol.RouteOriginLocal, OriginKey: "local",
510- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,510+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
511 })511 })
512 512
513 require.NoError(t, err)513 require.NoError(t, err)
@@ -523,14 +523,14 @@ func TestAttachHelloPreservesCompleteAttachRequest(t *testing.T) {
523 SessionName: "work", LiveTabID: "tab-1",523 SessionName: "work", LiveTabID: "tab-1",
524 }524 }
525 request := client.AttachRequest{525 request := client.AttachRequest{
526- Intent: ports.IntentAttach, SessionName: "work", Remote: true,526+ Intent: protocol.IntentAttach, SessionName: "work", Remote: true,
527- RemoteTarget: target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,527+ RemoteTarget: target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
528 NavigationCapabilities: protocol.NavigationCapabilityHomePicker,528 NavigationCapabilities: protocol.NavigationCapabilityHomePicker,
529 StartupOverlay: protocol.StartupOverlayNone,529 StartupOverlay: protocol.StartupOverlayNone,
530 }530 }
531 tr := &recordingTransport{recvs: []recvItem{531 tr := &recordingTransport{recvs: []recvItem{
532 {f: welcomeFrame(0)},532 {f: welcomeFrame(0)},
533- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},533+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
534 }}534 }}
535 535
536 require.NoError(t, runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), request))536 require.NoError(t, runTestClient(context.Background(), attachTestDependencies(tr, term, realClock{}), request))
@@ -551,8 +551,8 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
551 targetLifecycle := domain.SessionLifecycleID{2}551 targetLifecycle := domain.SessionLifecycleID{2}
552 target := protocol.ExactSessionTarget{LifecycleID: targetLifecycle, SessionName: "stopped"}552 target := protocol.ExactSessionTarget{LifecycleID: targetLifecycle, SessionName: "stopped"}
553 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {553 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
554- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{554+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
555- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,555+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
556 CommittedIdentity: &protocol.CommittedRouteIdentity{556 CommittedIdentity: &protocol.CommittedRouteIdentity{
557 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},557 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
558 },558 },
@@ -560,21 +560,21 @@ func TestStoppedLocalHandoffDialsReplacementTransport(t *testing.T) {
560 }560 }
561 first := &recordingTransport{recvs: []recvItem{561 first := &recordingTransport{recvs: []recvItem{
562 {f: welcome("source", sourceLifecycle)},562 {f: welcome("source", sourceLifecycle)},
563- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{563+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
564- Session: "stopped", Intent: ports.IntentAttach, ExactTarget: &target,564+ Session: "stopped", Intent: protocol.IntentAttach, ExactTarget: &target,
565- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,565+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
566 }))},566 }))},
567 }}567 }}
568 second := &recordingTransport{recvs: []recvItem{568 second := &recordingTransport{recvs: []recvItem{
569 {f: welcome("stopped", targetLifecycle)},569 {f: welcome("stopped", targetLifecycle)},
570- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},570+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
571 }}571 }}
572 dialer := &sequenceDialer{trs: []ports.Transport{first, second}}572 dialer := &sequenceDialer{trs: []ports.Transport{first, second}}
573 573
574 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{
575- Intent: ports.IntentAttach, SessionName: "source",575+ Intent: protocol.IntentAttach, SessionName: "source",
576 Origin: protocol.RouteOriginLocal, OriginKey: "local",576 Origin: protocol.RouteOriginLocal, OriginKey: "local",
577- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,577+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
578 })578 })
579 579
580 require.NoError(t, err)580 require.NoError(t, err)
@@ -588,8 +588,8 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
588 localLifecycle := domain.SessionLifecycleID{1}588 localLifecycle := domain.SessionLifecycleID{1}
589 remoteLifecycle := domain.SessionLifecycleID{2}589 remoteLifecycle := domain.SessionLifecycleID{2}
590 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {590 welcome := func(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
591- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{591+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
592- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,592+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
593 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},593 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
594 }))594 }))
595 }595 }
@@ -599,9 +599,9 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
599 }599 }
600 local1 := &recordingTransport{recvs: []recvItem{600 local1 := &recordingTransport{recvs: []recvItem{
601 {f: welcome("local", localLifecycle)},601 {f: welcome("local", localLifecycle)},
602- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{602+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
603- Endpoint: "remote", Session: "work", Intent: ports.IntentAttach,603+ Endpoint: "remote", Session: "work", Intent: protocol.IntentAttach,
604- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,604+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
605 }))},605 }))},
606 }}606 }}
607 remote := &recordingTransport{recvs: []recvItem{607 remote := &recordingTransport{recvs: []recvItem{
@@ -610,31 +610,31 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
610 }}610 }}
611 local2 := &recordingTransport{recvs: []recvItem{611 local2 := &recordingTransport{recvs: []recvItem{
612 {f: welcome("local", localLifecycle)},612 {f: welcome("local", localLifecycle)},
613- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},613+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
614 }}614 }}
615 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2}}615 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2}}
616 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}616 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}
617 deps := testDependencies(localDialer, term, realClock{}, nil, nil)617 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
618- deps.AttachHandoff = func(ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {618+ deps.AttachHandoff = func(protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
619 target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}619 target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}
620 return remoteDialer, client.AttachRequest{620 return remoteDialer, client.AttachRequest{
621- Intent: ports.IntentAttach, SessionName: "work", Remote: true,621+ Intent: protocol.IntentAttach, SessionName: "work", Remote: true,
622 Origin: protocol.RouteOriginRemote, OriginKey: "remote", ExactTarget: &target,622 Origin: protocol.RouteOriginRemote, OriginKey: "remote", ExactTarget: &target,
623- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,623+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
624 // Deliberately omit the copied Home capability. The accepted server624 // Deliberately omit the copied Home capability. The accepted server
625 // action remains valid because Runner retained the concrete home route.625 // action remains valid because Runner retained the concrete home route.
626 }, nil626 }, nil
627 }627 }
628 628
629 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{629 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
630- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",630+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
631 }))631 }))
632 require.Equal(t, protocol.StartupOverlaySessionPicker, helloFromSend(t, local2).StartupOverlay)632 require.Equal(t, protocol.StartupOverlaySessionPicker, helloFromSend(t, local2).StartupOverlay)
633 }633 }
634 634
635 func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.Frame {635 func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.Frame {
636- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{636+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
637- SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: ports.CapabilityResume,637+ SessionID: name, SessionName: name, ResumeToken: 1, Capabilities: protocol.CapabilityResume,
638 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},638 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name}},
639 }))639 }))
640 }640 }
@@ -642,16 +642,16 @@ func hybridWelcomeFrame(name string, lifecycle domain.SessionLifecycleID) ports.
642 func hybridLocalBootstrap(lifecycle domain.SessionLifecycleID, target domain.RemoteSessionTarget) *recordingTransport {642 func hybridLocalBootstrap(lifecycle domain.SessionLifecycleID, target domain.RemoteSessionTarget) *recordingTransport {
643 return &recordingTransport{recvs: []recvItem{643 return &recordingTransport{recvs: []recvItem{
644 {f: hybridWelcomeFrame("local", lifecycle)},644 {f: hybridWelcomeFrame("local", lifecycle)},
645- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{645+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
646- Endpoint: target.Endpoint, Session: target.SessionName, Intent: ports.IntentAttach,646+ Endpoint: target.Endpoint, Session: target.SessionName, Intent: protocol.IntentAttach,
647- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,647+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
648 }))},648 }))},
649 }}649 }}
650 }650 }
651 651
652 func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clock ports.Clock, remoteByEndpoint map[string]ports.Dialer) client.Dependencies {652 func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clock ports.Clock, remoteByEndpoint map[string]ports.Dialer) client.Dependencies {
653 deps := testDependencies(localDialer, term, clock, nil, nil)653 deps := testDependencies(localDialer, term, clock, nil, nil)
654- deps.AttachHandoff = func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {654+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
655 if target.RemoteTarget == nil {655 if target.RemoteTarget == nil {
656 return nil, client.AttachRequest{}, errors.New("hybrid picker target is not remote")656 return nil, client.AttachRequest{}, errors.New("hybrid picker target is not remote")
657 }657 }
@@ -661,9 +661,9 @@ func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clo
661 }661 }
662 selection := *target.RemoteTarget662 selection := *target.RemoteTarget
663 return dialer, client.AttachRequest{663 return dialer, client.AttachRequest{
664- Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,664+ Intent: protocol.IntentAttach, SessionName: target.Session, Remote: true,
665 Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,665 Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
666- RemoteTarget: &selection, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,666+ RemoteTarget: &selection, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
667 }, nil667 }, nil
668 }668 }
669 return deps669 return deps
@@ -721,7 +721,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
721 {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: remoteTargetLifecycle, SessionName: "target"}})), wait: switchSent},721 {f: frameOf(ports.MsgCommittedRouteIdentity, mustMarshalCommittedIdentity(protocol.CommittedRouteIdentity{Target: protocol.ExactSessionTarget{LifecycleID: remoteTargetLifecycle, SessionName: "target"}})), wait: switchSent},
722 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteSwitched}))},722 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteSwitched}))},
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.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 2, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("target frame")}))},
724- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},724+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
725 }}725 }}
726 remote.onSend = hybridParkedRequestHandler(parkedRequests, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{726 remote.onSend = hybridParkedRequestHandler(parkedRequests, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
727 protocol.ParkedRoutePrepare: parkSent,727 protocol.ParkedRoutePrepare: parkSent,
@@ -729,9 +729,9 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
729 })729 })
730 localPicker := &recordingTransport{recvs: []recvItem{730 localPicker := &recordingTransport{recvs: []recvItem{
731 {f: hybridWelcomeFrame("local", localLifecycle)},731 {f: hybridWelcomeFrame("local", localLifecycle)},
732- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{732+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
733- Endpoint: "remote", Session: "target", Intent: ports.IntentAttach,733+ Endpoint: "remote", Session: "target", Intent: protocol.IntentAttach,
734- RemoteTarget: &targetTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,734+ RemoteTarget: &targetTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
735 }))},735 }))},
736 }}736 }}
737 var resizeOnce sync.Once737 var resizeOnce sync.Once
@@ -745,7 +745,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
745 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})745 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
746 746
747 err := runTestClient(context.Background(), deps, client.AttachRequest{747 err := runTestClient(context.Background(), deps, client.AttachRequest{
748- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",748+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
749 })749 })
750 750
751 require.NoError(t, err)751 require.NoError(t, err)
@@ -812,14 +812,14 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
812 })812 })
813 localPicker := &recordingTransport{recvs: []recvItem{813 localPicker := &recordingTransport{recvs: []recvItem{
814 {f: hybridWelcomeFrame("local", localLifecycle)},814 {f: hybridWelcomeFrame("local", localLifecycle)},
815- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{815+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
816- Endpoint: "remote", Session: "target", Intent: ports.IntentAttach,816+ Endpoint: "remote", Session: "target", Intent: protocol.IntentAttach,
817- RemoteTarget: &targetTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,817+ RemoteTarget: &targetTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
818 }))},818 }))},
819 }}819 }}
820 targetRemote := &recordingTransport{recvs: []recvItem{820 targetRemote := &recordingTransport{recvs: []recvItem{
821 {f: hybridWelcomeFrame("target", targetLifecycle)},821 {f: hybridWelcomeFrame("target", targetLifecycle)},
822- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},822+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
823 }}823 }}
824 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}824 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
825 remoteDialer := &sequenceDialer{trs: []ports.Transport{825 remoteDialer := &sequenceDialer{trs: []ports.Transport{
@@ -828,7 +828,7 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
828 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})828 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
829 829
830 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{830 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
831- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",831+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
832 }))832 }))
833 select {833 select {
834 case requestErr := <-parkedRequestErrors:834 case requestErr := <-parkedRequestErrors:
@@ -855,7 +855,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
855 ch := make(chan time.Time, 1)855 ch := make(chan time.Time, 1)
856 timer.EXPECT().C().Return(ch).Maybe()856 timer.EXPECT().C().Return(ch).Maybe()
857 timer.EXPECT().Reset(mock.Anything).Return(true).Maybe()857 timer.EXPECT().Reset(mock.Anything).Return(true).Maybe()
858- if duration == ports.HandshakeTimeout {858+ if duration == protocol.HandshakeTimeout {
859 record := &deadlineTimer{ch: ch}859 record := &deadlineTimer{ch: ch}
860 timer.EXPECT().Stop().Run(func() { record.stopped.Store(true) }).Return(true).Maybe()860 timer.EXPECT().Stop().Run(func() { record.stopped.Store(true) }).Return(true).Maybe()
861 deadlineMu.Lock()861 deadlineMu.Lock()
@@ -900,7 +900,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
900 result := make(chan error, 1)900 result := make(chan error, 1)
901 go func() {901 go func() {
902 result <- runTestClient(ctx, deps, client.AttachRequest{902 result <- runTestClient(ctx, deps, client.AttachRequest{
903- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",903+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
904 })904 })
905 }()905 }()
906 select {906 select {
@@ -954,7 +954,7 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
954 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},954 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady})), wait: parkSent},
955 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed})), wait: resumeSent},955 {f: frameOf(ports.MsgParkedRouteResponse, ports.MarshalParkedRouteResponse(protocol.ParkedRouteResponse{RequestID: 2, Status: protocol.ParkedRouteResumed})), wait: resumeSent},
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.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, New: 1, Size: domain.Size{Cols: 80, Rows: 24}, Full: true, Data: []byte("source frame")}))},
957- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},957+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
958 }}958 }}
959 remote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{959 remote.onSend = hybridParkedRequestHandler(nil, parkedRequestErrors, map[protocol.ParkedRouteAction]chan struct{}{
960 protocol.ParkedRoutePrepare: parkSent,960 protocol.ParkedRoutePrepare: parkSent,
@@ -969,7 +969,7 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
969 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})969 deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
970 970
971 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{971 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
972- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",972+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
973 }))973 }))
974 select {974 select {
975 case requestErr := <-parkedRequestErrors:975 case requestErr := <-parkedRequestErrors:
@@ -1010,14 +1010,14 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
1010 })1010 })
1011 localPicker := &recordingTransport{recvs: []recvItem{1011 localPicker := &recordingTransport{recvs: []recvItem{
1012 {f: hybridWelcomeFrame("local", localLifecycle)},1012 {f: hybridWelcomeFrame("local", localLifecycle)},
1013- {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(ports.AttachTarget{1013+ {f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(protocol.AttachTarget{
1014- Endpoint: targetTarget.Endpoint, Session: targetTarget.SessionName, Intent: ports.IntentAttach,1014+ Endpoint: targetTarget.Endpoint, Session: targetTarget.SessionName, Intent: protocol.IntentAttach,
1015- RemoteTarget: &targetTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,1015+ RemoteTarget: &targetTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
1016 }))},1016 }))},
1017 }}1017 }}
1018 targetRemote := &recordingTransport{recvs: []recvItem{1018 targetRemote := &recordingTransport{recvs: []recvItem{
1019 {f: hybridWelcomeFrame("target", targetLifecycle)},1019 {f: hybridWelcomeFrame("target", targetLifecycle)},
1020- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1020+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1021 }}1021 }}
1022 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}1022 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
1023 sourceDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: sourceRemote}}}1023 sourceDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: sourceRemote}}}
@@ -1028,7 +1028,7 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
1028 })1028 })
1029 1029
1030 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{1030 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
1031- Intent: ports.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",1031+ Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1032 }))1032 }))
1033 select {1033 select {
1034 case requestErr := <-parkedRequestErrors:1034 case requestErr := <-parkedRequestErrors:
@@ -1049,11 +1049,11 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
1049 newLifecycle := domain.SessionLifecycleID{3, 3, 3}1049 newLifecycle := domain.SessionLifecycleID{3, 3, 3}
1050 remoteNewLifecycle := domain.SessionLifecycleID{4, 4, 4}1050 remoteNewLifecycle := domain.SessionLifecycleID{4, 4, 4}
1051 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {1051 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {
1052- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{1052+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
1053 SessionID: name + "-id",1053 SessionID: name + "-id",
1054 SessionName: name,1054 SessionName: name,
1055 ResumeToken: token,1055 ResumeToken: token,
1056- Capabilities: ports.CapabilityResume,1056+ Capabilities: protocol.CapabilityResume,
1057 CommittedIdentity: &protocol.CommittedRouteIdentity{1057 CommittedIdentity: &protocol.CommittedRouteIdentity{
1058 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},1058 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
1059 },1059 },
@@ -1066,9 +1066,9 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
1066 SessionName: "remote-manual",1066 SessionName: "remote-manual",
1067 LiveTabID: "tab-1",1067 LiveTabID: "tab-1",
1068 }1068 }
1069- remoteHandoff := ports.AttachTarget{1069+ remoteHandoff := protocol.AttachTarget{
1070- Endpoint: "remote", Session: "remote-manual", Intent: ports.IntentAttach,1070+ Endpoint: "remote", Session: "remote-manual", Intent: protocol.IntentAttach,
1071- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,1071+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
1072 }1072 }
1073 1073
1074 local1 := &recordingTransport{recvs: []recvItem{1074 local1 := &recordingTransport{recvs: []recvItem{
@@ -1102,23 +1102,23 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
1102 }}1102 }}
1103 remote3 := &recordingTransport{recvs: []recvItem{1103 remote3 := &recordingTransport{recvs: []recvItem{
1104 {f: welcome("remote-new", remoteNewLifecycle, 22)},1104 {f: welcome("remote-new", remoteNewLifecycle, 22)},
1105- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1105+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1106 }}1106 }}
1107 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2, local3}}1107 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2, local3}}
1108 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2, remote3}}1108 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2, remote3}}
1109 1109
1110 deps := testDependencies(localDialer, term, realClock{}, nil, nil)1110 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
1111- deps.AttachHandoff = func(target ports.AttachTarget) (ports.Dialer, client.AttachRequest, error) {1111+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
1112 require.Equal(t, remoteHandoff, target)1112 require.Equal(t, remoteHandoff, target)
1113 return remoteDialer, client.AttachRequest{1113 return remoteDialer, client.AttachRequest{
1114- Intent: ports.IntentAttach, SessionName: target.Session, Remote: true,1114+ Intent: protocol.IntentAttach, SessionName: target.Session, Remote: true,
1115 Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,1115 Origin: protocol.RouteOriginDiscovery, OriginKey: target.Endpoint,
1116- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,1116+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
1117 }, nil1117 }, nil
1118 }1118 }
1119 1119
1120 err := runTestClient(context.Background(), deps, client.AttachRequest{1120 err := runTestClient(context.Background(), deps, client.AttachRequest{
1121- Intent: ports.IntentAttach, SessionName: "test", Origin: protocol.RouteOriginLocal, OriginKey: "local",1121+ Intent: protocol.IntentAttach, SessionName: "test", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1122 })1122 })
1123 remoteHello := helloFromSend(t, remote2)1123 remoteHello := helloFromSend(t, remote2)
1124 require.NoError(t, err)1124 require.NoError(t, err)
@@ -1168,18 +1168,18 @@ func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.
1168 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)1168 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
1169 defer in.unblock()1169 defer in.unblock()
1170 1170
1171- target := ports.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: ports.IntentAttach}1171+ target := protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}
1172 tr := portsmocks.NewMockTransport(t)1172 tr := portsmocks.NewMockTransport(t)
1173 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()1173 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
1174 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1174 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1175 unblock := scriptRecv(tr,1175 unblock := scriptRecv(tr,
1176- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "local"}))},1176+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "local"}))},
1177 recvItem{f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(target))},1177 recvItem{f: frameOf(ports.MsgAttachTarget, ports.MarshalAttachTarget(target))},
1178 )1178 )
1179 defer unblock()1179 defer unblock()
1180 tr.EXPECT().Close().Return(nil).Once()1180 tr.EXPECT().Close().Return(nil).Once()
1181 1181
1182- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"})1182+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"})
1183 var handoff *client.AttachTargetError1183 var handoff *client.AttachTargetError
1184 require.ErrorAs(t, err, &handoff)1184 require.ErrorAs(t, err, &handoff)
1185 require.Equal(t, target, handoff.Target)1185 require.Equal(t, target, handoff.Target)
@@ -1195,7 +1195,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) {
1195 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)1195 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
1196 defer in.unblock()1196 defer in.unblock()
1197 1197
1198- gotHello := make(chan ports.Hello, 1)1198+ gotHello := make(chan protocol.Hello, 1)
1199 tr := portsmocks.NewMockTransport(t)1199 tr := portsmocks.NewMockTransport(t)
1200 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()1200 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
1201 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()1201 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
@@ -1208,13 +1208,13 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) {
1208 return nil1208 return nil
1209 }).Once()1209 }).Once()
1210 unblock := scriptRecv(tr,1210 unblock := scriptRecv(tr,
1211- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1211+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1212- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1212+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1213 )1213 )
1214 defer unblock()1214 defer unblock()
1215 tr.EXPECT().Close().Return(nil).Once()1215 tr.EXPECT().Close().Return(nil).Once()
1216 1216
1217- require.NoError(t, runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral}))1217+ require.NoError(t, runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral}))
1218 require.Equal(t, os.Environ(), (<-gotHello).Env)1218 require.Equal(t, os.Environ(), (<-gotHello).Env)
1219 }1219 }
1220 1220
@@ -1235,13 +1235,13 @@ func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) {
1235 return nil1235 return nil
1236 }).Once()1236 }).Once()
1237 unblock := scriptRecv(tr,1237 unblock := scriptRecv(tr,
1238- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1238+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1239- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1239+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1240 )1240 )
1241 defer unblock()1241 defer unblock()
1242 tr.EXPECT().Close().Return(nil).Once()1242 tr.EXPECT().Close().Return(nil).Once()
1243 1243
1244- require.NoError(t, runTestClient(context.Background(), attachTestDependencies(markedDatagramTransport{tr}, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral}))1244+ require.NoError(t, runTestClient(context.Background(), attachTestDependencies(markedDatagramTransport{tr}, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral}))
1245 }1245 }
1246 1246
1247 func TestAttachHappyPath(t *testing.T) {1247 func TestAttachHappyPath(t *testing.T) {
@@ -1256,14 +1256,14 @@ func TestAttachHappyPath(t *testing.T) {
1256 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()1256 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
1257 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1257 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1258 unblock := scriptRecv(tr,1258 unblock := scriptRecv(tr,
1259- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"}))},1259+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: "main"}))},
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.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Data: []byte("hello world")}))},
1261- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1261+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1262 )1262 )
1263 defer unblock()1263 defer unblock()
1264 tr.EXPECT().Close().Return(nil).Once()1264 tr.EXPECT().Close().Return(nil).Once()
1265 1265
1266- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})1266+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
1267 require.NoError(t, err)1267 require.NoError(t, err)
1268 require.Equal(t, "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07\x1b[?2031$phello world", out.String())1268 require.Equal(t, "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07\x1b[?2031$phello world", out.String())
1269 require.Equal(t, int32(1), restoreCount.Load(), "restore must run exactly once")1269 require.Equal(t, int32(1), restoreCount.Load(), "restore must run exactly once")
@@ -1277,16 +1277,16 @@ func TestAttachVersionMismatch(t *testing.T) {
1277 tr := portsmocks.NewMockTransport(t)1277 tr := portsmocks.NewMockTransport(t)
1278 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1278 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1279 tr.EXPECT().Recv().Return(1279 tr.EXPECT().Recv().Return(
1280- frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"})),1280+ frameOf(ports.MsgError, ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
1281 nil,1281 nil,
1282 ).Once()1282 ).Once()
1283 tr.EXPECT().Close().Return(nil).Once()1283 tr.EXPECT().Close().Return(nil).Once()
1284 1284
1285- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main"})1285+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"})
1286 require.Error(t, err)1286 require.Error(t, err)
1287 var pe *client.ProtocolError1287 var pe *client.ProtocolError
1288 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)1288 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)
1289- require.Equal(t, ports.ErrVersionMismatch, pe.Code)1289+ require.Equal(t, protocol.ErrVersionMismatch, pe.Code)
1290 tr.AssertNotCalled(t, "Send", isType(ports.MsgTheme))1290 tr.AssertNotCalled(t, "Send", isType(ports.MsgTheme))
1291 }1291 }
1292 1292
@@ -1303,13 +1303,13 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
1303 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1303 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1304 boom := errors.New("connection reset")1304 boom := errors.New("connection reset")
1305 unblock := scriptRecv(tr,1305 unblock := scriptRecv(tr,
1306- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1306+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1307 recvItem{err: boom},1307 recvItem{err: boom},
1308 )1308 )
1309 defer unblock()1309 defer unblock()
1310 tr.EXPECT().Close().Return(nil).Once()1310 tr.EXPECT().Close().Return(nil).Once()
1311 1311
1312- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})1312+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
1313 require.Error(t, err)1313 require.Error(t, err)
1314 require.Equal(t, int32(1), restoreCount.Load(), "restore must run after mid-stream Recv error")1314 require.Equal(t, int32(1), restoreCount.Load(), "restore must run after mid-stream Recv error")
1315 }1315 }
@@ -1326,13 +1326,13 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) {
1326 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()1326 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
1327 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1327 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1328 unblock := scriptRecv(tr,1328 unblock := scriptRecv(tr,
1329- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1329+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1330 recvItem{err: io.EOF},1330 recvItem{err: io.EOF},
1331 )1331 )
1332 defer unblock()1332 defer unblock()
1333 tr.EXPECT().Close().Return(nil).Once()1333 tr.EXPECT().Close().Return(nil).Once()
1334 1334
1335- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})1335+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
1336 require.Error(t, err)1336 require.Error(t, err)
1337 require.ErrorIs(t, err, io.EOF)1337 require.ErrorIs(t, err, io.EOF)
1338 require.Equal(t, int32(1), restoreCount.Load())1338 require.Equal(t, int32(1), restoreCount.Load())
@@ -1366,8 +1366,8 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) {
1366 close(allowDetach)1366 close(allowDetach)
1367 return nil1367 return nil
1368 }).Once()1368 }).Once()
1369- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))1369+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
1370- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))1370+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
1371 recvCh := make(chan recvItem, 1)1371 recvCh := make(chan recvItem, 1)
1372 recvCh <- recvItem{f: welcome}1372 recvCh <- recvItem{f: welcome}
1373 closed := make(chan struct{})1373 closed := make(chan struct{})
@@ -1389,7 +1389,7 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) {
1389 }).Maybe()1389 }).Maybe()
1390 tr.EXPECT().Close().Return(nil).Once()1390 tr.EXPECT().Close().Return(nil).Once()
1391 1391
1392- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})1392+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
1393 require.NoError(t, err)1393 require.NoError(t, err)
1394 select {1394 select {
1395 case got := <-gotInput:1395 case got := <-gotInput:
@@ -1428,8 +1428,8 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) {
1428 close(allowDetach)1428 close(allowDetach)
1429 return nil1429 return nil
1430 }).Once()1430 }).Once()
1431- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))1431+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
1432- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))1432+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
1433 recvCh := make(chan recvItem, 1)1433 recvCh := make(chan recvItem, 1)
1434 recvCh <- recvItem{f: welcome}1434 recvCh <- recvItem{f: welcome}
1435 closed := make(chan struct{})1435 closed := make(chan struct{})
@@ -1451,7 +1451,7 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) {
1451 }).Maybe()1451 }).Maybe()
1452 tr.EXPECT().Close().Return(nil).Once()1452 tr.EXPECT().Close().Return(nil).Once()
1453 1453
1454- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})1454+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
1455 require.NoError(t, err)1455 require.NoError(t, err)
1456 select {1456 select {
1457 case got := <-gotInput:1457 case got := <-gotInput:
@@ -1485,8 +1485,8 @@ func TestAttachForwardsResize(t *testing.T) {
1485 return nil1485 return nil
1486 }).Once()1486 }).Once()
1487 1487
1488- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))1488+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
1489- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))1489+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
1490 recvCh := make(chan recvItem, 2)1490 recvCh := make(chan recvItem, 2)
1491 recvCh <- recvItem{f: welcome}1491 recvCh <- recvItem{f: welcome}
1492 done := make(chan struct{})1492 done := make(chan struct{})
@@ -1516,7 +1516,7 @@ func TestAttachForwardsResize(t *testing.T) {
1516 resizeCh <- domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}}1516 resizeCh <- domain.Geometry{Size: domain.Size{Cols: 120, Rows: 40}}
1517 }()1517 }()
1518 1518
1519- err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: ""})1519+ err := runTestClient(context.Background(), attachTestDependencies(tr, tm, realClock{}), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: ""})
1520 require.NoError(t, err)1520 require.NoError(t, err)
1521 select {1521 select {
1522 case r := <-gotResize:1522 case r := <-gotResize:
@@ -1539,7 +1539,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
1539 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1539 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1540 boom := errors.New("connection reset")1540 boom := errors.New("connection reset")
1541 unblock := scriptRecv(tr,1541 unblock := scriptRecv(tr,
1542- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1542+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1543 recvItem{err: boom},1543 recvItem{err: boom},
1544 )1544 )
1545 defer unblock()1545 defer unblock()
@@ -1547,7 +1547,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
1547 d := portsmocks.NewMockDialer(t)1547 d := portsmocks.NewMockDialer(t)
1548 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1548 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1549 1549
1550- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: "", Remote: false})1550+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: false})
1551 require.Error(t, err)1551 require.Error(t, err)
1552 require.Equal(t, int32(1), restoreCount.Load(), "Run must restore raw mode after attach attempt errors")1552 require.Equal(t, int32(1), restoreCount.Load(), "Run must restore raw mode after attach attempt errors")
1553 }1553 }
@@ -1560,14 +1560,14 @@ func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) {
1560 tr := portsmocks.NewMockTransport(t)1560 tr := portsmocks.NewMockTransport(t)
1561 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()1561 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
1562 tr.EXPECT().Recv().Return(1562 tr.EXPECT().Recv().Return(
1563- frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"})),1563+ frameOf(ports.MsgError, ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
1564 nil,1564 nil,
1565 ).Once()1565 ).Once()
1566 tr.EXPECT().Close().Return(nil).Once()1566 tr.EXPECT().Close().Return(nil).Once()
1567 d := portsmocks.NewMockDialer(t)1567 d := portsmocks.NewMockDialer(t)
1568 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1568 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1569 1569
1570- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})1570+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
1571 require.Error(t, err)1571 require.Error(t, err)
1572 var pe *client.ProtocolError1572 var pe *client.ProtocolError
1573 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)1573 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)
@@ -1580,7 +1580,7 @@ func TestRunPhaseASingleAttempt(t *testing.T) {
1580 d.EXPECT().Dial(mock.Anything).Return(nil, dialErr).Once()1580 d.EXPECT().Dial(mock.Anything).Return(nil, dialErr).Once()
1581 tm := portsmocks.NewMockTerminal(t)1581 tm := portsmocks.NewMockTerminal(t)
1582 1582
1583- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentEphemeral, SessionName: "", Remote: false})1583+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: false})
1584 require.ErrorIs(t, err, dialErr)1584 require.ErrorIs(t, err, dialErr)
1585 }1585 }
1586 1586
@@ -1688,7 +1688,7 @@ func (t *runTerminal) In() io.Reader { return t.in }
1688 func (t *runTerminal) Out() io.Writer { return &t.out }1688 func (t *runTerminal) Out() io.Writer { return &t.out }
1689 func (t *runTerminal) Flush() error { return nil }1689 func (t *runTerminal) Flush() error { return nil }
1690 1690
1691-func helloFromSend(t *testing.T, tr *recordingTransport) ports.Hello {1691+func helloFromSend(t *testing.T, tr *recordingTransport) protocol.Hello {
1692 t.Helper()1692 t.Helper()
1693 sends := tr.Sends()1693 sends := tr.Sends()
1694 require.NotEmpty(t, sends)1694 require.NotEmpty(t, sends)
@@ -1699,7 +1699,7 @@ func helloFromSend(t *testing.T, tr *recordingTransport) ports.Hello {
1699 }1699 }
1700 1700
1701 func welcomeFrame(token uint64) ports.Frame {1701 func welcomeFrame(token uint64) ports.Frame {
1702- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main", ResumeToken: token, Capabilities: ports.CapabilityResume}))1702+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: "main", ResumeToken: token, Capabilities: protocol.CapabilityResume}))
1703 }1703 }
1704 1704
1705 func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) {1705 func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) {
@@ -1707,12 +1707,12 @@ func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) {
1707 defer term.in.unblock()1707 defer term.in.unblock()
1708 tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}}1708 tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}}
1709 tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {err: io.EOF}}}1709 tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {err: io.EOF}}}
1710- tr3 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(33)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))}}}1710+ tr3 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(33)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}}}
1711 d := &sequenceDialer{trs: []ports.Transport{tr1, tr2, tr3}}1711 d := &sequenceDialer{trs: []ports.Transport{tr1, tr2, tr3}}
1712 clock := &reconnectTestClock{}1712 clock := &reconnectTestClock{}
1713 result := make(chan error, 1)1713 result := make(chan error, 1)
1714 go func() {1714 go func() {
1715- result <- runTestClient(context.Background(), testDependencies(d, term, clock, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})1715+ result <- runTestClient(context.Background(), testDependencies(d, term, clock, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
1716 }()1716 }()
1717 clock.fireReconnect(t)1717 clock.fireReconnect(t)
1718 clock.fireReconnect(t)1718 clock.fireReconnect(t)
@@ -1724,11 +1724,11 @@ func TestRunReconnectsWithRotatedTokenAndSameClientID(t *testing.T) {
1724 h1 := helloFromSend(t, tr1)1724 h1 := helloFromSend(t, tr1)
1725 h2 := helloFromSend(t, tr2)1725 h2 := helloFromSend(t, tr2)
1726 h3 := helloFromSend(t, tr3)1726 h3 := helloFromSend(t, tr3)
1727- require.Equal(t, ports.IntentAttach, h1.Intent)1727+ require.Equal(t, protocol.IntentAttach, h1.Intent)
1728 require.Zero(t, h1.ResumeToken)1728 require.Zero(t, h1.ResumeToken)
1729- require.Equal(t, ports.IntentResume, h2.Intent)1729+ require.Equal(t, protocol.IntentResume, h2.Intent)
1730 require.Equal(t, uint64(11), h2.ResumeToken)1730 require.Equal(t, uint64(11), h2.ResumeToken)
1731- require.Equal(t, ports.IntentResume, h3.Intent)1731+ require.Equal(t, protocol.IntentResume, h3.Intent)
1732 require.Equal(t, uint64(22), h3.ResumeToken)1732 require.Equal(t, uint64(22), h3.ResumeToken)
1733 require.Equal(t, h1.ClientID, h2.ClientID)1733 require.Equal(t, h1.ClientID, h2.ClientID)
1734 require.Equal(t, h1.ClientID, h3.ClientID)1734 require.Equal(t, h1.ClientID, h3.ClientID)
@@ -1742,8 +1742,8 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
1742 lifecycle := domain.SessionLifecycleID{7, 8, 9}1742 lifecycle := domain.SessionLifecycleID{7, 8, 9}
1743 target := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}1743 target := protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: "work"}
1744 welcome := func(token uint64) ports.Frame {1744 welcome := func(token uint64) ports.Frame {
1745- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{1745+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
1746- SessionID: "work", SessionName: "work", ResumeToken: token, Capabilities: ports.CapabilityResume,1746+ SessionID: "work", SessionName: "work", ResumeToken: token, Capabilities: protocol.CapabilityResume,
1747 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: target},1747 CommittedIdentity: &protocol.CommittedRouteIdentity{Target: target},
1748 }))1748 }))
1749 }1749 }
@@ -1754,14 +1754,14 @@ func TestReconnectRestoresLastPublishedRouteTab(t *testing.T) {
1754 }}1754 }}
1755 tr2 := &recordingTransport{recvs: []recvItem{1755 tr2 := &recordingTransport{recvs: []recvItem{
1756 {f: welcome(22)},1756 {f: welcome(22)},
1757- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1757+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1758 }}1758 }}
1759 dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}1759 dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}
1760 clock := &reconnectTestClock{}1760 clock := &reconnectTestClock{}
1761 result := make(chan error, 1)1761 result := make(chan error, 1)
1762 go func() {1762 go func() {
1763 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{
1764- Intent: ports.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local",1764+ Intent: protocol.IntentAttach, SessionName: "work", Origin: protocol.RouteOriginLocal, OriginKey: "local",
1765 })1765 })
1766 }()1766 }()
1767 clock.fireReconnect(t)1767 clock.fireReconnect(t)
@@ -1781,8 +1781,8 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
1781 SessionName: "old", LiveTabID: "tab-1",1781 SessionName: "old", LiveTabID: "tab-1",
1782 }1782 }
1783 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {1783 welcome := func(name string, lifecycle domain.SessionLifecycleID, token uint64) ports.Frame {
1784- return frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{1784+ return frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{
1785- SessionID: name, SessionName: name, ResumeToken: token, Capabilities: ports.CapabilityResume,1785+ SessionID: name, SessionName: name, ResumeToken: token, Capabilities: protocol.CapabilityResume,
1786 CommittedIdentity: &protocol.CommittedRouteIdentity{1786 CommittedIdentity: &protocol.CommittedRouteIdentity{
1787 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},1787 Target: protocol.ExactSessionTarget{LifecycleID: lifecycle, SessionName: name},
1788 },1788 },
@@ -1797,23 +1797,23 @@ func TestRemoteReconnectDropsStalePickerTargetAfterDaemonSessionSwitch(t *testin
1797 }}1797 }}
1798 tr2 := &recordingTransport{recvs: []recvItem{1798 tr2 := &recordingTransport{recvs: []recvItem{
1799 {f: welcome("new", newLifecycle, 22)},1799 {f: welcome("new", newLifecycle, 22)},
1800- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1800+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1801 }}1801 }}
1802 dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}1802 dialer := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}
1803 clock := &reconnectTestClock{}1803 clock := &reconnectTestClock{}
1804 result := make(chan error, 1)1804 result := make(chan error, 1)
1805 go func() {1805 go func() {
1806 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{
1807- Intent: ports.IntentAttach, SessionName: "old", Remote: true,1807+ Intent: protocol.IntentAttach, SessionName: "old", Remote: true,
1808 Origin: protocol.RouteOriginDiscovery, OriginKey: "remote",1808 Origin: protocol.RouteOriginDiscovery, OriginKey: "remote",
1809- RemoteTarget: remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,1809+ RemoteTarget: remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
1810 })1810 })
1811 }()1811 }()
1812 clock.fireReconnect(t)1812 clock.fireReconnect(t)
1813 require.NoError(t, <-result)1813 require.NoError(t, <-result)
1814 1814
1815 reconnectHello := helloFromSend(t, tr2)1815 reconnectHello := helloFromSend(t, tr2)
1816- require.Equal(t, ports.IntentResume, reconnectHello.Intent)1816+ require.Equal(t, protocol.IntentResume, reconnectHello.Intent)
1817 require.Equal(t, "new", reconnectHello.Name)1817 require.Equal(t, "new", reconnectHello.Name)
1818 require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, reconnectHello.ExactTarget)1818 require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: newLifecycle, SessionName: "new"}, reconnectHello.ExactTarget)
1819 require.Nil(t, reconnectHello.RemoteTarget)1819 require.Nil(t, reconnectHello.RemoteTarget)
@@ -1832,35 +1832,35 @@ func TestAttachRememberRemoteHost(t *testing.T) {
1832 {1832 {
1833 name: "remote success",1833 name: "remote success",
1834 request: client.AttachRequest{1834 request: client.AttachRequest{
1835- Intent: ports.IntentAttach,1835+ Intent: protocol.IntentAttach,
1836 SessionName: "main",1836 SessionName: "main",
1837 Remote: true,1837 Remote: true,
1838 },1838 },
1839 remember: func() error { return nil },1839 remember: func() error { return nil },
1840 recv: []recvItem{1840 recv: []recvItem{
1841- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1841+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1842- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1842+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1843 },1843 },
1844 wantCalled: true,1844 wantCalled: true,
1845 },1845 },
1846 {1846 {
1847 name: "callback failure does not fail attach",1847 name: "callback failure does not fail attach",
1848 request: client.AttachRequest{1848 request: client.AttachRequest{
1849- Intent: ports.IntentAttach,1849+ Intent: protocol.IntentAttach,
1850 SessionName: "main",1850 SessionName: "main",
1851 Remote: true,1851 Remote: true,
1852 },1852 },
1853 remember: func() error { return errors.New("disk full") },1853 remember: func() error { return errors.New("disk full") },
1854 recv: []recvItem{1854 recv: []recvItem{
1855- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1855+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1856- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1856+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1857 },1857 },
1858 wantCalled: true,1858 wantCalled: true,
1859 },1859 },
1860 {1860 {
1861 name: "local attach does not call callback",1861 name: "local attach does not call callback",
1862 request: client.AttachRequest{1862 request: client.AttachRequest{
1863- Intent: ports.IntentAttach,1863+ Intent: protocol.IntentAttach,
1864 SessionName: "main",1864 SessionName: "main",
1865 Remote: false,1865 Remote: false,
1866 },1866 },
@@ -1869,14 +1869,14 @@ func TestAttachRememberRemoteHost(t *testing.T) {
1869 return nil1869 return nil
1870 },1870 },
1871 recv: []recvItem{1871 recv: []recvItem{
1872- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1872+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1873- {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},1873+ {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
1874 },1874 },
1875 },1875 },
1876 {1876 {
1877 name: "rejected before welcome does not call callback",1877 name: "rejected before welcome does not call callback",
1878 request: client.AttachRequest{1878 request: client.AttachRequest{
1879- Intent: ports.IntentAttach,1879+ Intent: protocol.IntentAttach,
1880 SessionName: "main",1880 SessionName: "main",
1881 Remote: true,1881 Remote: true,
1882 },1882 },
@@ -1885,7 +1885,7 @@ func TestAttachRememberRemoteHost(t *testing.T) {
1885 return nil1885 return nil
1886 },1886 },
1887 recv: []recvItem{1887 recv: []recvItem{
1888- {f: frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"}))},1888+ {f: frameOf(ports.MsgError, ports.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"}))},
1889 },1889 },
1890 preWelcomeReject: true,1890 preWelcomeReject: true,
1891 wantErr: true,1891 wantErr: true,
@@ -1893,13 +1893,13 @@ func TestAttachRememberRemoteHost(t *testing.T) {
1893 {1893 {
1894 name: "post-welcome error still uses happy terminal fixture",1894 name: "post-welcome error still uses happy terminal fixture",
1895 request: client.AttachRequest{1895 request: client.AttachRequest{
1896- Intent: ports.IntentAttach,1896+ Intent: protocol.IntentAttach,
1897 SessionName: "main",1897 SessionName: "main",
1898 Remote: true,1898 Remote: true,
1899 },1899 },
1900 remember: func() error { return nil },1900 remember: func() error { return nil },
1901 recv: []recvItem{1901 recv: []recvItem{
1902- {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},1902+ {f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
1903 {err: errors.New("connection reset")},1903 {err: errors.New("connection reset")},
1904 },1904 },
1905 wantCalled: true,1905 wantCalled: true,
@@ -1984,7 +1984,7 @@ func TestRunRememberRemoteHostAtMostOnceAcrossReconnects(t *testing.T) {
1984 learnerDone := make(chan struct{})1984 learnerDone := make(chan struct{})
1985 1985
1986 tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}}1986 tr1 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {err: io.EOF}}}
1987- tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))}}}1987+ tr2 := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(22)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))}}}
1988 d := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}1988 d := &sequenceDialer{trs: []ports.Transport{tr1, tr2}}
1989 1989
1990 deps := testDependencies(d, term, realClock{}, nil, nil)1990 deps := testDependencies(d, term, realClock{}, nil, nil)
@@ -1996,7 +1996,7 @@ func TestRunRememberRemoteHostAtMostOnceAcrossReconnects(t *testing.T) {
1996 }).Once()1996 }).Once()
1997 deps.RemoteHostLearner = learner1997 deps.RemoteHostLearner = learner
1998 err := runTestClient(context.Background(), deps, client.AttachRequest{1998 err := runTestClient(context.Background(), deps, client.AttachRequest{
1999- Intent: ports.IntentAttach,1999+ Intent: protocol.IntentAttach,
2000 SessionName: "main",2000 SessionName: "main",
2001 Remote: true,2001 Remote: true,
2002 })2002 })
@@ -2036,8 +2036,8 @@ func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) {
2036 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()2036 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
2037 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()2037 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
2038 unblock := scriptRecv(tr,2038 unblock := scriptRecv(tr,
2039- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},2039+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
2040- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},2040+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
2041 )2041 )
2042 defer unblock()2042 defer unblock()
2043 tr.EXPECT().Close().Return(nil).Once()2043 tr.EXPECT().Close().Return(nil).Once()
@@ -2055,7 +2055,7 @@ func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) {
2055 errCh := make(chan error, 1)2055 errCh := make(chan error, 1)
2056 go func() {2056 go func() {
2057 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{2057 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{
2058- Intent: ports.IntentAttach,2058+ Intent: protocol.IntentAttach,
2059 SessionName: "main",2059 SessionName: "main",
2060 Remote: true,2060 Remote: true,
2061 })2061 })
@@ -2117,8 +2117,8 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) {
2117 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()2117 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
2118 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()2118 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
2119 unblock := scriptRecv(tr,2119 unblock := scriptRecv(tr,
2120- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},2120+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
2121- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},2121+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
2122 )2122 )
2123 defer unblock()2123 defer unblock()
2124 tr.EXPECT().Close().Return(nil).Once()2124 tr.EXPECT().Close().Return(nil).Once()
@@ -2136,7 +2136,7 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) {
2136 errCh := make(chan error, 1)2136 errCh := make(chan error, 1)
2137 go func() {2137 go func() {
2138 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{2138 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{
2139- Intent: ports.IntentAttach,2139+ Intent: protocol.IntentAttach,
2140 SessionName: "main",2140 SessionName: "main",
2141 Remote: true,2141 Remote: true,
2142 })2142 })
@@ -2214,8 +2214,8 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
2214 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()2214 tr.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
2215 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()2215 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
2216 unblock := scriptRecv(tr,2216 unblock := scriptRecv(tr,
2217- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},2217+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
2218- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},2218+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
2219 )2219 )
2220 defer unblock()2220 defer unblock()
2221 tr.EXPECT().Close().Return(nil).Once()2221 tr.EXPECT().Close().Return(nil).Once()
@@ -2232,7 +2232,7 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
2232 errCh := make(chan error, 1)2232 errCh := make(chan error, 1)
2233 go func() {2233 go func() {
2234 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{2234 errCh <- runTestClient(context.Background(), deps, client.AttachRequest{
2235- Intent: ports.IntentAttach,2235+ Intent: protocol.IntentAttach,
2236 SessionName: "main",2236 SessionName: "main",
2237 Remote: true,2237 Remote: true,
2238 })2238 })
@@ -2267,10 +2267,10 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
2267 func TestRunDoesNotRetryTerminalDetachedError(t *testing.T) {2267 func TestRunDoesNotRetryTerminalDetachedError(t *testing.T) {
2268 term := newRunTerminal()2268 term := newRunTerminal()
2269 defer term.in.unblock()2269 defer term.in.unblock()
2270- tr := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonSessionKilled}))}}}2270+ tr := &recordingTransport{recvs: []recvItem{{f: welcomeFrame(11)}, {f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonSessionKilled}))}}}
2271 d := &sequenceDialer{trs: []ports.Transport{tr}}2271 d := &sequenceDialer{trs: []ports.Transport{tr}}
2272 2272
2273- err := runTestClient(context.Background(), testDependencies(d, term, realClock{}, nil, nil), client.AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: false})2273+ err := runTestClient(context.Background(), testDependencies(d, term, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
2274 require.Error(t, err)2274 require.Error(t, err)
2275 var de *client.DetachedError2275 var de *client.DetachedError
2276 require.True(t, errors.As(err, &de))2276 require.True(t, errors.As(err, &de))
M internal/usecase/client/clipboard_intercept.go +3 -2
@@ -7,6 +7,7 @@ import (
7 "log/slog"7 "log/slog"
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 // ctrlV is the byte a Ctrl+V keypress sends on the wire.13 // ctrlV is the byte a Ctrl+V keypress sends on the wire.
@@ -101,7 +102,7 @@ func (c *clipboardIntercept) handleCtrlV() {
101 if !errors.Is(err, ports.ErrNoClipboardImage) {102 if !errors.Is(err, ports.ErrNoClipboardImage) {
102 c.log.Warn("clipboard image read failed", "err", err)103 c.log.Warn("clipboard image read failed", "err", err)
103 if c.sendNotice != nil {104 if c.sendNotice != nil {
104- c.sendNotice(ports.ClientNoticeClipboardFallback)105+ c.sendNotice(protocol.ClientNoticeClipboardFallback)
105 }106 }
106 }107 }
107 c.next([]byte{ctrlV})108 c.next([]byte{ctrlV})
@@ -110,7 +111,7 @@ func (c *clipboardIntercept) handleCtrlV() {
110 if len(data) > maxClipboardImagePush {111 if len(data) > maxClipboardImagePush {
111 c.log.Warn("clipboard image too large to send, forwarding Ctrl+V instead", "size", len(data), "cap", maxClipboardImagePush)112 c.log.Warn("clipboard image too large to send, forwarding Ctrl+V instead", "size", len(data), "cap", maxClipboardImagePush)
112 if c.sendNotice != nil {113 if c.sendNotice != nil {
113- c.sendNotice(ports.ClientNoticeClipboardTooLarge)114+ c.sendNotice(protocol.ClientNoticeClipboardTooLarge)
114 }115 }
115 c.next([]byte{ctrlV})116 c.next([]byte{ctrlV})
116 return117 return
M internal/usecase/client/clipboard_intercept_internal_test.go +2 -2
@@ -86,8 +86,8 @@ func TestClipboardInterceptFailureNotifiesDaemonAndForwardsCtrlV(t *testing.T) {
86 reader ports.ClipboardReader86 reader ports.ClipboardReader
87 want uint887 want uint8
88 }{88 }{
89- {name: "image read failure", reader: &ciFakeReader{err: errors.New("boom")}, want: ports.ClientNoticeClipboardFallback},89+ {name: "image read failure", reader: &ciFakeReader{err: errors.New("boom")}, want: protocol.ClientNoticeClipboardFallback},
90- {name: "oversized image", reader: &ciFakeReader{mime: "image/png", data: make([]byte, maxClipboardImagePush+1)}, want: ports.ClientNoticeClipboardTooLarge},90+ {name: "oversized image", reader: &ciFakeReader{mime: "image/png", data: make([]byte, maxClipboardImagePush+1)}, want: protocol.ClientNoticeClipboardTooLarge},
91 }91 }
92 for _, tt := range tests {92 for _, tt := range tests {
93 t.Run(tt.name, func(t *testing.T) {93 t.Run(tt.name, func(t *testing.T) {
M internal/usecase/client/clipboard_intercept_test.go +7 -7
@@ -64,8 +64,8 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
64 }).Maybe()64 }).Maybe()
65 tr.EXPECT().Send(isType(ports.MsgClientNotice)).Return(nil).Maybe()65 tr.EXPECT().Send(isType(ports.MsgClientNotice)).Return(nil).Maybe()
66 66
67- welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))67+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
68- detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))68+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
69 recvCh := make(chan recvItem, 1)69 recvCh := make(chan recvItem, 1)
70 recvCh <- recvItem{f: welcome}70 recvCh <- recvItem{f: welcome}
71 closed := make(chan struct{})71 closed := make(chan struct{})
@@ -90,7 +90,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
90 d := portsmocks.NewMockDialer(t)90 d := portsmocks.NewMockDialer(t)
91 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()91 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
92 92
93- err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, clip, nil), client.AttachRequest{Intent: ports.IntentEphemeral, Remote: remote})93+ err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, clip, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, Remote: remote})
94 require.NoError(t, err)94 require.NoError(t, err)
95 return gotInput, gotImage95 return gotInput, gotImage
96 }96 }
@@ -176,11 +176,11 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
176 clipboard := portsmocks.NewMockClipboardReader(t)176 clipboard := portsmocks.NewMockClipboardReader(t)
177 clipboard.EXPECT().ReadImage(mock.Anything).Return("", nil, errors.New("read failed")).Once()177 clipboard.EXPECT().ReadImage(mock.Anything).Return("", nil, errors.New("read failed")).Once()
178 transport := &clipboardToastLifecycleTransport{recv: make(chan ports.Frame, 8), sends: make(chan ports.Frame, 16)}178 transport := &clipboardToastLifecycleTransport{recv: make(chan ports.Frame, 8), sends: make(chan ports.Frame, 16)}
179- transport.recv <- frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))179+ transport.recv <- frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))
180 180
181 result := make(chan error, 1)181 result := make(chan error, 1)
182 go func() {182 go func() {
183- result <- client.NewRunner(client.Dependencies{Dialer: clipboardToastLifecycleDialer{transport: transport}, Terminal: term, Clock: realClock{}, DisableCapabilityProbe: true, Clipboard: clipboard}).Run(context.Background(), client.AttachRequest{Intent: ports.IntentEphemeral, Remote: true})183+ result <- client.NewRunner(client.Dependencies{Dialer: clipboardToastLifecycleDialer{transport: transport}, Terminal: term, Clock: realClock{}, DisableCapabilityProbe: true, Clipboard: clipboard}).Run(context.Background(), client.AttachRequest{Intent: protocol.IntentEphemeral, Remote: true})
184 }()184 }()
185 185
186 var gotNotice bool186 var gotNotice bool
@@ -192,7 +192,7 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
192 }192 }
193 notice, err := ports.UnmarshalClientNotice(sent.Payload)193 notice, err := ports.UnmarshalClientNotice(sent.Payload)
194 require.NoError(t, err)194 require.NoError(t, err)
195- require.Equal(t, ports.ClientNoticeClipboardFallback, notice.Action)195+ require.Equal(t, protocol.ClientNoticeClipboardFallback, notice.Action)
196 gotNotice = true196 gotNotice = true
197 case <-time.After(time.Second):197 case <-time.After(time.Second):
198 t.Fatal("client did not notify daemon of clipboard failure")198 t.Fatal("client did not notify daemon of clipboard failure")
@@ -200,7 +200,7 @@ func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testi
200 }200 }
201 beforeOutput := out.String()201 beforeOutput := out.String()
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.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Base: 0, New: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("incremental")}))
203- transport.recv <- frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))203+ transport.recv <- frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))
204 204
205 select {205 select {
206 case err := <-result:206 case err := <-result:
M internal/usecase/client/handshake.go +3 -2
@@ -7,6 +7,7 @@ import (
7 "time"7 "time"
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 var errHandshakeTimeout = errors.New("handshake timed out")13 var errHandshakeTimeout = errors.New("handshake timed out")
@@ -33,9 +34,9 @@ func newHandshakeContext(parent context.Context, clock ports.Clock) (context.Con
33 if clock == nil {34 if clock == nil {
34 clock = systemClock{}35 clock = systemClock{}
35 }36 }
36- timer := clock.NewTimer(ports.HandshakeTimeout)37+ timer := clock.NewTimer(protocol.HandshakeTimeout)
37 if timer == nil {38 if timer == nil {
38- timer = systemClock{}.NewTimer(ports.HandshakeTimeout)39+ timer = systemClock{}.NewTimer(protocol.HandshakeTimeout)
39 }40 }
40 stop := make(chan struct{})41 stop := make(chan struct{})
41 done := make(chan struct{})42 done := make(chan struct{})
M internal/usecase/client/navigation_test.go +3 -4
@@ -4,7 +4,6 @@ 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"
8 "github.com/bnema/vev/internal/protocol"7 "github.com/bnema/vev/internal/protocol"
9 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
10 )9 )
@@ -38,10 +37,10 @@ func TestValidateAttachRequestNavigationTable(t *testing.T) {
38 valid bool37 valid bool
39 }{38 }{
40 {name: "ordinary route", request: AttachRequest{}, valid: true},39 {name: "ordinary route", request: AttachRequest{}, valid: true},
41- {name: "home picker without remote route", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "work", NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},40+ {name: "home picker without remote route", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: false},
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},41+ {name: "home picker on daemon-owned remote route", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", Remote: true, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}, valid: true},
43 {name: "back on client-owned route", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: true},42 {name: "back on client-owned route", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: true},
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},43+ {name: "back on remote-target route", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "work", RemoteTarget: remoteRoute, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned, StartupOverlay: protocol.StartupOverlaySessionPicker, NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
45 {name: "unknown capability", request: AttachRequest{NavigationCapabilities: 4}, valid: false},44 {name: "unknown capability", request: AttachRequest{NavigationCapabilities: 4}, valid: false},
46 {name: "back without startup picker", request: AttachRequest{NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},45 {name: "back without startup picker", request: AttachRequest{NavigationCapabilities: protocol.NavigationCapabilityBack}, valid: false},
47 {name: "startup picker without back", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},46 {name: "startup picker without back", request: AttachRequest{StartupOverlay: protocol.StartupOverlaySessionPicker}, valid: false},
M internal/usecase/client/palette_attach_generation_test.go +7 -7
@@ -43,7 +43,7 @@ func newAttachPaletteClock() *attachPaletteClock {
43 func (*attachPaletteClock) Now() time.Time { return time.Time{} }43 func (*attachPaletteClock) Now() time.Time { return time.Time{} }
44 func (c *attachPaletteClock) NewTimer(delay time.Duration) ports.Timer {44 func (c *attachPaletteClock) NewTimer(delay time.Duration) ports.Timer {
45 timer := &attachPaletteTimer{ch: make(chan time.Time, 1), stopped: make(chan struct{}), duration: delay}45 timer := &attachPaletteTimer{ch: make(chan time.Time, 1), stopped: make(chan struct{}), duration: delay}
46- if delay == ports.HandshakeTimeout {46+ if delay == protocol.HandshakeTimeout {
47 select {47 select {
48 case c.handshakeTimers <- timer:48 case c.handshakeTimers <- timer:
49 default:49 default:
@@ -118,7 +118,7 @@ type attachPaletteTransport struct {
118 frames []ports.Frame118 frames []ports.Frame
119 themes []protocol.Theme119 themes []protocol.Theme
120 themeCh chan protocol.Theme120 themeCh chan protocol.Theme
121- inputCh chan ports.Input121+ inputCh chan protocol.Input
122 detached chan ports.Frame122 detached chan ports.Frame
123 themeSendErr error123 themeSendErr error
124 welcomed bool124 welcomed bool
@@ -127,7 +127,7 @@ type attachPaletteTransport struct {
127 func newAttachPaletteTransport() *attachPaletteTransport {127 func newAttachPaletteTransport() *attachPaletteTransport {
128 return &attachPaletteTransport{128 return &attachPaletteTransport{
129 themeCh: make(chan protocol.Theme, 16),129 themeCh: make(chan protocol.Theme, 16),
130- inputCh: make(chan ports.Input, 16),130+ inputCh: make(chan protocol.Input, 16),
131 detached: make(chan ports.Frame, 1),131 detached: make(chan ports.Frame, 1),
132 }132 }
133 }133 }
@@ -163,7 +163,7 @@ func (t *attachPaletteTransport) Recv() (ports.Frame, error) {
163 if !t.welcomed {163 if !t.welcomed {
164 t.welcomed = true164 t.welcomed = true
165 t.mu.Unlock()165 t.mu.Unlock()
166- return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s"})}, nil166+ return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s"})}, nil
167 }167 }
168 t.mu.Unlock()168 t.mu.Unlock()
169 return <-t.detached, nil169 return <-t.detached, nil
@@ -205,7 +205,7 @@ func startAttachPaletteHarnessWithInputAndWriteError(state *terminalThemeState,
205 ms := &milestones{}205 ms := &milestones{}
206 runner := &Runner{term: term, clock: clock, logger: slog.New(slog.DiscardHandler)}206 runner := &Runner{term: term, clock: clock, logger: slog.New(slog.DiscardHandler)}
207 attempt := &attachAttempt{207 attempt := &attachAttempt{
208- runner: runner, transport: transport, request: AttachRequest{Intent: ports.IntentAttach},208+ runner: runner, transport: transport, request: AttachRequest{Intent: protocol.IntentAttach},
209 milestones: ms, themeState: state,209 milestones: ms, themeState: state,
210 enterRaw: func() error { ms.rawEntered = true; return nil },210 enterRaw: func() error { ms.rawEntered = true; return nil },
211 reconnect: &reconnectUI{term: term, rawEntered: new(bool)},211 reconnect: &reconnectUI{term: term, rawEntered: new(bool)},
@@ -239,7 +239,7 @@ func (h *attachPaletteHarness) nextTimer(t *testing.T) *attachPaletteTimer {
239 }239 }
240 func (h *attachPaletteHarness) detach(t *testing.T) {240 func (h *attachPaletteHarness) detach(t *testing.T) {
241 t.Helper()241 t.Helper()
242- h.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}242+ h.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})}
243 result := <-h.done243 result := <-h.done
244 require.NoError(t, result.err)244 require.NoError(t, result.err)
245 select {245 select {
@@ -325,7 +325,7 @@ func TestAttachReconnectPreservesStandaloneEscapeBeforeClaimRevocation(t *testin
325 325
326 // Detach starts reconnect teardown. The residual must already be owned by326 // Detach starts reconnect teardown. The residual must already be owned by
327 // the lifecycle reader at the precise point the old claim is revoked.327 // the lifecycle reader at the precise point the old claim is revoked.
328- first.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}328+ first.transport.detached <- ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})}
329 require.Equal(t, []byte("\x1b"), <-revoked)329 require.Equal(t, []byte("\x1b"), <-revoked)
330 release()330 release()
331 require.NoError(t, (<-first.done).err)331 require.NoError(t, (<-first.done).err)
M internal/usecase/client/palette_attach_test.go +3 -3
@@ -109,10 +109,10 @@ func (t *paletteAttachTransport) Recv() (ports.Frame, error) {
109 first := len(t.sent) == 1109 first := len(t.sent) == 1
110 t.mu.Unlock()110 t.mu.Unlock()
111 if first {111 if first {
112- return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s"})}, nil112+ return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s"})}, nil
113 }113 }
114 <-t.finalSet114 <-t.finalSet
115- return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach})}, nil115+ return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach})}, nil
116 }116 }
117 func (*paletteAttachTransport) Close() error { return nil }117 func (*paletteAttachTransport) Close() error { return nil }
118 118
@@ -142,7 +142,7 @@ func TestAttachPublishesOnlyClearedAndDefinitiveInitialPalette(t *testing.T) {
142 ms := &milestones{}142 ms := &milestones{}
143 runner := &Runner{term: term, clock: paletteAttachClock{}, logger: slog.New(slog.DiscardHandler)}143 runner := &Runner{term: term, clock: paletteAttachClock{}, logger: slog.New(slog.DiscardHandler)}
144 attempt := &attachAttempt{144 attempt := &attachAttempt{
145- runner: runner, transport: transport, request: AttachRequest{Intent: ports.IntentAttach},145+ runner: runner, transport: transport, request: AttachRequest{Intent: protocol.IntentAttach},
146 milestones: ms, themeState: &terminalThemeState{},146 milestones: ms, themeState: &terminalThemeState{},
147 enterRaw: func() error { ms.rawEntered = true; return nil },147 enterRaw: func() error { ms.rawEntered = true; return nil },
148 reconnect: &reconnectUI{term: term, rawEntered: new(bool)},148 reconnect: &reconnectUI{term: term, rawEntered: new(bool)},
M internal/usecase/client/reconnect.go +2 -1
@@ -5,6 +5,7 @@ import (
5 "time"5 "time"
6 6
7 "github.com/bnema/vev/internal/ports"7 "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
8 )9 )
9 10
10 type reconnectBackoff struct {11 type reconnectBackoff struct {
@@ -54,7 +55,7 @@ func resumeNeedsExactAttach(err error) bool {
54 return true55 return true
55 }56 }
56 protocolErr, ok := errors.AsType[*ProtocolError](err)57 protocolErr, ok := errors.AsType[*ProtocolError](err)
57- return ok && (protocolErr.Code == ports.ErrNoSuchSession || protocolErr.Code == ports.ErrNoSuchTarget)58+ return ok && (protocolErr.Code == protocol.ErrNoSuchSession || protocolErr.Code == protocol.ErrNoSuchTarget)
58 }59 }
59 60
60 func shouldReconnect(err error) bool {61 func shouldReconnect(err error) bool {
M internal/usecase/client/reconnect_toast_test.go +45 -45
@@ -134,11 +134,11 @@ func reconnectToastWelcome(token uint64) ports.Frame {
134 }134 }
135 135
136 func reconnectToastWelcomeNamed(name string, token uint64) ports.Frame {136 func reconnectToastWelcomeNamed(name string, token uint64) ports.Frame {
137- return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: name, ResumeToken: token, Capabilities: ports.CapabilityResume})}137+ return ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(protocol.Welcome{SessionID: "s1", SessionName: name, ResumeToken: token, Capabilities: protocol.CapabilityResume})}
138 }138 }
139 139
140 func reconnectToastDetach(reason uint8) ports.Frame {140 func reconnectToastDetach(reason uint8) ports.Frame {
141- return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: reason})}141+ return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: reason})}
142 }142 }
143 143
144 type reconnectToastRecv struct {144 type reconnectToastRecv struct {
@@ -205,7 +205,7 @@ func (s *reconnectToastSentFrames) find(match func(ports.Frame) bool) (ports.Fra
205 type reconnectToastLinkTransport struct {205 type reconnectToastLinkTransport struct {
206 recvCh chan reconnectToastRecv206 recvCh chan reconnectToastRecv
207 sends *reconnectToastSentFrames207 sends *reconnectToastSentFrames
208- clientNotices chan ports.ClientNotice208+ clientNotices chan protocol.ClientNotice
209 events chan ports.LinkEvent209 events chan ports.LinkEvent
210 state ports.LinkState210 state ports.LinkState
211 }211 }
@@ -214,7 +214,7 @@ func newReconnectToastLinkTransport() *reconnectToastLinkTransport {
214 return &reconnectToastLinkTransport{214 return &reconnectToastLinkTransport{
215 recvCh: make(chan reconnectToastRecv, 4),215 recvCh: make(chan reconnectToastRecv, 4),
216 sends: newReconnectToastSentFrames(),216 sends: newReconnectToastSentFrames(),
217- clientNotices: make(chan ports.ClientNotice, 2),217+ clientNotices: make(chan protocol.ClientNotice, 2),
218 events: make(chan ports.LinkEvent, 4),218 events: make(chan ports.LinkEvent, 4),
219 state: ports.LinkStateConnected,219 state: ports.LinkStateConnected,
220 }220 }
@@ -298,7 +298,7 @@ func assertReconnectToastAttemptPublishesOnlyClearedTheme(t *testing.T, tr *reco
298 require.True(t, tr.wasClosed())298 require.True(t, tr.wasClosed())
299 }299 }
300 300
301-func reconnectToastHelloFromSend(t *testing.T, tr *reconnectToastRecordingTransport) ports.Hello {301+func reconnectToastHelloFromSend(t *testing.T, tr *reconnectToastRecordingTransport) protocol.Hello {
302 t.Helper()302 t.Helper()
303 frames := tr.sentFrames()303 frames := tr.sentFrames()
304 require.NotEmpty(t, frames)304 require.NotEmpty(t, frames)
@@ -362,9 +362,9 @@ func TestResumeNeedsExactAttach(t *testing.T) {
362 want bool362 want bool
363 }{363 }{
364 {name: "moved resume token", err: errRouteTargetChanged, want: true},364 {name: "moved resume token", err: errRouteTargetChanged, want: true},
365- {name: "missing target", err: &ProtocolError{Code: ports.ErrNoSuchTarget}, want: true},365+ {name: "missing target", err: &ProtocolError{Code: protocol.ErrNoSuchTarget}, want: true},
366- {name: "missing session", err: &ProtocolError{Code: ports.ErrNoSuchSession}, want: true},366+ {name: "missing session", err: &ProtocolError{Code: protocol.ErrNoSuchSession}, want: true},
367- {name: "internal", err: &ProtocolError{Code: ports.ErrInternal}},367+ {name: "internal", err: &ProtocolError{Code: protocol.ErrInternal}},
368 {name: "transport", err: io.EOF},368 {name: "transport", err: io.EOF},
369 } {369 } {
370 t.Run(test.name, func(t *testing.T) {370 t.Run(test.name, func(t *testing.T) {
@@ -386,7 +386,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
386 386
387 resultCh := make(chan attachResult, 1)387 resultCh := make(chan attachResult, 1)
388 ms := milestones{}388 ms := milestones{}
389- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)389+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
390 go func() { resultCh <- attempt.run(context.Background()) }()390 go func() { resultCh <- attempt.run(context.Background()) }()
391 391
392 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}392 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -408,7 +408,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
408 require.Contains(t, redrawn, "probing UDP path")408 require.Contains(t, redrawn, "probing UDP path")
409 409
410 tr.events <- ports.LinkEvent{State: ports.LinkStateConnected}410 tr.events <- ports.LinkEvent{State: ports.LinkStateConnected}
411- require.Equal(t, ports.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)411+ require.Equal(t, protocol.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
412 require.Equal(t, term.size, requireResize(t, tr.sends).Size)412 require.Equal(t, term.size, requireResize(t, tr.sends).Size)
413 413
414 beforeAwaitingReset := out.String()414 beforeAwaitingReset := out.String()
@@ -461,7 +461,7 @@ func TestProbingToastReconcilesDaemonOutputAndDismissal(t *testing.T) {
461 }, time.Second, time.Millisecond)461 }, time.Second, time.Millisecond)
462 requireAckedState(t, tr.sends, 2, 5)462 requireAckedState(t, tr.sends, 2, 5)
463 463
464- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}464+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
465 result := requireAttachResult(t, resultCh)465 result := requireAttachResult(t, resultCh)
466 require.NoError(t, result.err)466 require.NoError(t, result.err)
467 }467 }
@@ -479,7 +479,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
479 479
480 resultCh := make(chan attachResult, 1)480 resultCh := make(chan attachResult, 1)
481 ms := milestones{}481 ms := milestones{}
482- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)482+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
483 go func() { resultCh <- attempt.run(context.Background()) }()483 go func() { resultCh <- attempt.run(context.Background()) }()
484 484
485 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}485 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -547,7 +547,7 @@ func TestActiveReconnectToastStageTransitionReconcilesBeforeRedraw(t *testing.T)
547 require.Eventually(t, func() bool { return flushes.Load() > beforeIncrementFlushes }, time.Second, time.Millisecond)547 require.Eventually(t, func() bool { return flushes.Load() > beforeIncrementFlushes }, time.Second, time.Millisecond)
548 requireAckedState(t, tr.sends, 2, 5)548 requireAckedState(t, tr.sends, 2, 5)
549 549
550- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}550+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
551 result := requireAttachResult(t, resultCh)551 result := requireAttachResult(t, resultCh)
552 require.NoError(t, result.err)552 require.NoError(t, result.err)
553 }553 }
@@ -693,7 +693,7 @@ func TestLocalReconnectStatusFlushesDaemonOutputBeforeObservationAndAck(t *testi
693 693
694 resultCh := make(chan attachResult, 1)694 resultCh := make(chan attachResult, 1)
695 ms := milestones{}695 ms := milestones{}
696- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)696+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
697 attempt.runner.runtimeObserver = observer697 attempt.runner.runtimeObserver = observer
698 go func() { resultCh <- attempt.run(context.Background()) }()698 go func() { resultCh <- attempt.run(context.Background()) }()
699 699
@@ -725,7 +725,7 @@ func TestLocalReconnectStatusFlushesDaemonOutputBeforeObservationAndAck(t *testi
725 requireAckedState(t, tr.sends, 1, 2)725 requireAckedState(t, tr.sends, 1, 2)
726 require.Greater(t, flushes.Load(), beforeOutput, "daemon output must be flushed before ACK")726 require.Greater(t, flushes.Load(), beforeOutput, "daemon output must be flushed before ACK")
727 727
728- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}728+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
729 require.NoError(t, requireAttachResult(t, resultCh).err)729 require.NoError(t, requireAttachResult(t, resultCh).err)
730 }730 }
731 731
@@ -748,14 +748,14 @@ func TestLocalReconnectStatusTransitionsDoNotRequestAuthoritativeReset(t *testin
748 748
749 resultCh := make(chan attachResult, 1)749 resultCh := make(chan attachResult, 1)
750 ms := milestones{}750 ms := milestones{}
751- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)751+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
752 go func() { resultCh <- attempt.run(context.Background()) }()752 go func() { resultCh <- attempt.run(context.Background()) }()
753 753
754 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}754 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
755 require.Eventually(t, func() bool { return strings.Contains(out.String(), statusReconnect) }, time.Second, time.Millisecond)755 require.Eventually(t, func() bool { return strings.Contains(out.String(), statusReconnect) }, time.Second, time.Millisecond)
756 tr.events <- ports.LinkEvent{State: tt.transition}756 tr.events <- ports.LinkEvent{State: tt.transition}
757 if tt.transition == ports.LinkStateConnected {757 if tt.transition == ports.LinkStateConnected {
758- require.Equal(t, ports.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)758+ require.Equal(t, protocol.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
759 }759 }
760 760
761 const output = "local status output remains visible"761 const output = "local status output remains visible"
@@ -772,7 +772,7 @@ func TestLocalReconnectStatusTransitionsDoNotRequestAuthoritativeReset(t *testin
772 _, sentResize := tr.sends.find(func(frame ports.Frame) bool { return frame.Type == ports.MsgResize })772 _, sentResize := tr.sends.find(func(frame ports.Frame) bool { return frame.Type == ports.MsgResize })
773 require.False(t, sentResize, "local reconnect status must not request a daemon reset")773 require.False(t, sentResize, "local reconnect status must not request a daemon reset")
774 774
775- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}775+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
776 require.NoError(t, requireAttachResult(t, resultCh).err)776 require.NoError(t, requireAttachResult(t, resultCh).err)
777 })777 })
778 }778 }
@@ -820,7 +820,7 @@ func TestReconnectOverlayRedrawFailureDoesNotObserveOrAckOutput(t *testing.T) {
820 820
821 resultCh := make(chan attachResult, 1)821 resultCh := make(chan attachResult, 1)
822 ms := milestones{}822 ms := milestones{}
823- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)823+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
824 attempt.runner.runtimeObserver = observer824 attempt.runner.runtimeObserver = observer
825 go func() { resultCh <- attempt.run(context.Background()) }()825 go func() { resultCh <- attempt.run(context.Background()) }()
826 826
@@ -852,20 +852,20 @@ func TestReconnectLinkEventsNotifyDaemonWithoutLocalTerminalWrites(t *testing.T)
852 852
853 resultCh := make(chan attachResult, 1)853 resultCh := make(chan attachResult, 1)
854 ms := milestones{}854 ms := milestones{}
855- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)855+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
856 go func() { resultCh <- attempt.run(context.Background()) }()856 go func() { resultCh <- attempt.run(context.Background()) }()
857 857
858 tr.events <- ports.LinkEvent{State: ports.LinkStateDegraded}858 tr.events <- ports.LinkEvent{State: ports.LinkStateDegraded}
859 tr.events <- ports.LinkEvent{State: ports.LinkStateConnected}859 tr.events <- ports.LinkEvent{State: ports.LinkStateConnected}
860- require.Equal(t, ports.ClientNoticeLinkDegraded, (<-tr.clientNotices).Action)860+ require.Equal(t, protocol.ClientNoticeLinkDegraded, (<-tr.clientNotices).Action)
861- require.Equal(t, ports.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)861+ require.Equal(t, protocol.ClientNoticeLinkConnected, (<-tr.clientNotices).Action)
862 select {862 select {
863 case bytes := <-out.completed:863 case bytes := <-out.completed:
864 t.Fatalf("link notices wrote client-local terminal bytes: %q", bytes)864 t.Fatalf("link notices wrote client-local terminal bytes: %q", bytes)
865 default:865 default:
866 }866 }
867 867
868- tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)}868+ tr.recvCh <- reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)}
869 result := requireAttachResult(t, resultCh)869 result := requireAttachResult(t, resultCh)
870 require.NoError(t, result.err)870 require.NoError(t, result.err)
871 require.True(t, result.welcomed)871 require.True(t, result.welcomed)
@@ -879,7 +879,7 @@ func TestAttachAttemptOfflineLinkEventReturnsReconnectableError(t *testing.T) {
879 879
880 resultCh := make(chan attachResult, 1)880 resultCh := make(chan attachResult, 1)
881 ms := milestones{}881 ms := milestones{}
882- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)882+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
883 go func() { resultCh <- attempt.run(context.Background()) }()883 go func() { resultCh <- attempt.run(context.Background()) }()
884 884
885 tr.events <- ports.LinkEvent{State: ports.LinkStateOffline}885 tr.events <- ports.LinkEvent{State: ports.LinkStateOffline}
@@ -988,7 +988,7 @@ func TestPreSenderReconnectResetIsBounded(t *testing.T) {
988 ctx, cancel := context.WithCancel(context.Background())988 ctx, cancel := context.WithCancel(context.Background())
989 defer cancel()989 defer cancel()
990 ms := milestones{}990 ms := milestones{}
991- attempt := newReconnectAttachAttempt(term.term, tr, clock, AttachRequest{Intent: ports.IntentResume, SessionName: "requested", Remote: true}, 12, &terminalThemeState{}, nil, &ms)991+ attempt := newReconnectAttachAttempt(term.term, tr, clock, AttachRequest{Intent: protocol.IntentResume, SessionName: "requested", Remote: true}, 12, &terminalThemeState{}, nil, &ms)
992 attempt.reconnect.showing = true992 attempt.reconnect.showing = true
993 resultCh := make(chan attachResult, 1)993 resultCh := make(chan attachResult, 1)
994 go func() { resultCh <- attempt.run(ctx) }()994 go func() { resultCh <- attempt.run(ctx) }()
@@ -1078,7 +1078,7 @@ func TestReconnectResetEnqueueCancellationExitsCleanly(t *testing.T) {
1078 1078
1079 resultCh := make(chan attachResult, 1)1079 resultCh := make(chan attachResult, 1)
1080 ms := milestones{}1080 ms := milestones{}
1081- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)1081+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
1082 go func() { resultCh <- attempt.run(ctx) }()1082 go func() { resultCh <- attempt.run(ctx) }()
1083 1083
1084 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}1084 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -1118,7 +1118,7 @@ func TestReconnectResetCancellationPreservesQueuedSenderError(t *testing.T) {
1118 1118
1119 resultCh := make(chan attachResult, 1)1119 resultCh := make(chan attachResult, 1)
1120 ms := milestones{}1120 ms := milestones{}
1121- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)1121+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
1122 go func() { resultCh <- attempt.run(context.Background()) }()1122 go func() { resultCh <- attempt.run(context.Background()) }()
1123 1123
1124 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}1124 tr.events <- ports.LinkEvent{State: ports.LinkStateProbing}
@@ -1157,7 +1157,7 @@ func TestAttachAttemptReturnsWhileSenderSendIsBlockedAfterCancellation(t *testin
1157 1157
1158 resultCh := make(chan attachResult, 1)1158 resultCh := make(chan attachResult, 1)
1159 ms := milestones{}1159 ms := milestones{}
1160- attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)1160+ attempt := newReconnectAttachAttempt(term.term, tr, newReconnectHandshakeClock(t), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true}, 0, &terminalThemeState{}, tr.LinkEvents(), &ms)
1161 go func() { resultCh <- attempt.run(context.Background()) }()1161 go func() { resultCh <- attempt.run(context.Background()) }()
1162 1162
1163 tr.armed.Store(true)1163 tr.armed.Store(true)
@@ -1188,8 +1188,8 @@ func TestDetachedResultLegacyReplacementCompatibilityAndCleanDetach(t *testing.T
1188 reason uint81188 reason uint8
1189 wantText string1189 wantText string
1190 }{1190 }{
1191- {name: "clean detach", reason: ports.ReasonDetach},1191+ {name: "clean detach", reason: protocol.ReasonDetach},
1192- {name: "legacy replacement", reason: ports.ReasonReplaced, wantText: "session taken over by another client"},1192+ {name: "legacy replacement", reason: protocol.ReasonReplaced, wantText: "session taken over by another client"},
1193 } {1193 } {
1194 t.Run(tt.name, func(t *testing.T) {1194 t.Run(tt.name, func(t *testing.T) {
1195 err := detachedResult(tt.reason)1195 err := detachedResult(tt.reason)
@@ -1376,7 +1376,7 @@ func TestReconnectRetryFailuresPreserveAttachError(t *testing.T) {
1376 )1376 )
1377 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}}1377 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}}
1378 1378
1379- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})1379+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
1380 require.ErrorIs(t, err, attachErr)1380 require.ErrorIs(t, err, attachErr)
1381 require.ErrorIs(t, err, retryErr)1381 require.ErrorIs(t, err, retryErr)
1382 })1382 })
@@ -1433,7 +1433,7 @@ func TestReconnectRetrySleepFailuresPreserveDialError(t *testing.T) {
1433 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1433 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1434 dialer.EXPECT().Dial(mock.Anything).Run(func(context.Context) { armed.Store(true) }).Return(nil, dialErr).Once()1434 dialer.EXPECT().Dial(mock.Anything).Run(func(context.Context) { armed.Store(true) }).Return(nil, dialErr).Once()
1435 1435
1436- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})1436+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
1437 require.ErrorIs(t, err, dialErr)1437 require.ErrorIs(t, err, dialErr)
1438 require.ErrorIs(t, err, retryErr)1438 require.ErrorIs(t, err, retryErr)
1439 })1439 })
@@ -1463,7 +1463,7 @@ func TestRemoteReconnectToastFailedDrawDoesNotBlankBounds(t *testing.T) {
1463 }}1463 }}
1464 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}}1464 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr}}
1465 1465
1466- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})1466+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
1467 require.ErrorContains(t, err, "reconnect toast write failed")1467 require.ErrorContains(t, err, "reconnect toast write failed")
1468 require.True(t, out.failed)1468 require.True(t, out.failed)
1469 require.NotContains(t, out.String(), strings.Repeat(" ", reconnectToastBoundsFor(term.size, reconnectStageMessage(reconnectStageSSH)).Width))1469 require.NotContains(t, out.String(), strings.Repeat(" ", reconnectToastBoundsFor(term.size, reconnectStageMessage(reconnectStageSSH)).Width))
@@ -1540,7 +1540,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T)
1540 dialer := portsmocks.NewMockDialer(t)1540 dialer := portsmocks.NewMockDialer(t)
1541 tt.configure(dialer, tr)1541 tt.configure(dialer, tr)
1542 1542
1543- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: ports.IntentAttach, SessionName: "main"})1543+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: protocol.IntentAttach, SessionName: "main"})
1544 require.ErrorIs(t, err, context.Canceled)1544 require.ErrorIs(t, err, context.Canceled)
1545 require.ErrorIs(t, err, clearErr)1545 require.ErrorIs(t, err, clearErr)
1546 })1546 })
@@ -1572,11 +1572,11 @@ func TestRemoteReconnectToastLifecycleWithWrappedTransportError(t *testing.T) {
1572 }}1572 }}
1573 tr2 := &reconnectToastRecordingTransport{recvs: []reconnectToastRecv{1573 tr2 := &reconnectToastRecordingTransport{recvs: []reconnectToastRecv{
1574 {frame: reconnectToastWelcome(55)},1574 {frame: reconnectToastWelcome(55)},
1575- {frame: reconnectToastDetach(ports.ReasonDetach)},1575+ {frame: reconnectToastDetach(protocol.ReasonDetach)},
1576 }}1576 }}
1577 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}}1577 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}}
1578 1578
1579- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})1579+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
1580 require.NoError(t, err)1580 require.NoError(t, err)
1581 require.True(t, tr1.wasClosed())1581 require.True(t, tr1.wasClosed())
1582 require.True(t, tr2.wasClosed())1582 require.True(t, tr2.wasClosed())
@@ -1584,9 +1584,9 @@ func TestRemoteReconnectToastLifecycleWithWrappedTransportError(t *testing.T) {
1584 1584
1585 firstHello := reconnectToastHelloFromSend(t, tr1)1585 firstHello := reconnectToastHelloFromSend(t, tr1)
1586 resumeHello := reconnectToastHelloFromSend(t, tr2)1586 resumeHello := reconnectToastHelloFromSend(t, tr2)
1587- require.Equal(t, ports.IntentAttach, firstHello.Intent)1587+ require.Equal(t, protocol.IntentAttach, firstHello.Intent)
1588 require.Zero(t, firstHello.ResumeToken)1588 require.Zero(t, firstHello.ResumeToken)
1589- require.Equal(t, ports.IntentResume, resumeHello.Intent)1589+ require.Equal(t, protocol.IntentResume, resumeHello.Intent)
1590 require.Equal(t, uint64(44), resumeHello.ResumeToken)1590 require.Equal(t, uint64(44), resumeHello.ResumeToken)
1591 require.Equal(t, firstHello.ClientID, resumeHello.ClientID)1591 require.Equal(t, firstHello.ClientID, resumeHello.ClientID)
1592 1592
@@ -1616,20 +1616,20 @@ func TestRemoteEphemeralReconnectUsesAssignedSessionName(t *testing.T) {
1616 }}1616 }}
1617 tr2 := &reconnectToastRecordingTransport{recvs: []reconnectToastRecv{1617 tr2 := &reconnectToastRecordingTransport{recvs: []reconnectToastRecv{
1618 {frame: reconnectToastWelcomeNamed("0", 55)},1618 {frame: reconnectToastWelcomeNamed("0", 55)},
1619- {frame: reconnectToastDetach(ports.ReasonDetach)},1619+ {frame: reconnectToastDetach(protocol.ReasonDetach)},
1620 }}1620 }}
1621 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}}1621 dialer := &reconnectToastSequenceDialer{transports: []ports.Transport{tr1, tr2}}
1622 1622
1623- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: ports.IntentEphemeral, SessionName: "", Remote: true})1623+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(context.Background(), AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: true})
1624 require.NoError(t, err)1624 require.NoError(t, err)
1625 require.Equal(t, 2, dialer.calls)1625 require.Equal(t, 2, dialer.calls)
1626 1626
1627 firstHello := reconnectToastHelloFromSend(t, tr1)1627 firstHello := reconnectToastHelloFromSend(t, tr1)
1628 resumeHello := reconnectToastHelloFromSend(t, tr2)1628 resumeHello := reconnectToastHelloFromSend(t, tr2)
1629- require.Equal(t, ports.IntentEphemeral, firstHello.Intent)1629+ require.Equal(t, protocol.IntentEphemeral, firstHello.Intent)
1630 require.Empty(t, firstHello.Name)1630 require.Empty(t, firstHello.Name)
1631 require.Zero(t, firstHello.ResumeToken)1631 require.Zero(t, firstHello.ResumeToken)
1632- require.Equal(t, ports.IntentResume, resumeHello.Intent)1632+ require.Equal(t, protocol.IntentResume, resumeHello.Intent)
1633 require.Equal(t, "0", resumeHello.Name)1633 require.Equal(t, "0", resumeHello.Name)
1634 require.Equal(t, uint64(44), resumeHello.ResumeToken)1634 require.Equal(t, uint64(44), resumeHello.ResumeToken)
1635 require.Equal(t, firstHello.ClientID, resumeHello.ClientID)1635 require.Equal(t, firstHello.ClientID, resumeHello.ClientID)
@@ -1647,7 +1647,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
1647 name: "clears on successful reconnect",1647 name: "clears on successful reconnect",
1648 configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {1648 configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {
1649 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})1649 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
1650- tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonDetach)})1650+ tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)})
1651 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()1651 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
1652 dialer.EXPECT().Dial(mock.Anything).Return(tr2, nil).Once()1652 dialer.EXPECT().Dial(mock.Anything).Return(tr2, nil).Once()
1653 return []*reconnectToastRecordingTransport{tr1, tr2}1653 return []*reconnectToastRecordingTransport{tr1, tr2}
@@ -1672,7 +1672,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
1672 name: "clears on final exit",1672 name: "clears on final exit",
1673 configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {1673 configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {
1674 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})1674 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
1675- tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(ports.ReasonSessionKilled)})1675+ tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonSessionKilled)})
1676 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()1676 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
1677 dialer.EXPECT().Dial(mock.Anything).Return(tr2, nil).Once()1677 dialer.EXPECT().Dial(mock.Anything).Return(tr2, nil).Once()
1678 return []*reconnectToastRecordingTransport{tr1, tr2}1678 return []*reconnectToastRecordingTransport{tr1, tr2}
@@ -1704,7 +1704,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
1704 dialer := portsmocks.NewMockDialer(t)1704 dialer := portsmocks.NewMockDialer(t)
1705 transports := tt.configure(t, dialer)1705 transports := tt.configure(t, dialer)
1706 1706
1707- err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: ports.IntentAttach, SessionName: "main", Remote: true})1707+ err := NewRunner(Dependencies{Dialer: dialer, Terminal: term.term, Clock: newReconnectHandshakeClock(t), DisableCapabilityProbe: true, Logger: slog.New(slog.DiscardHandler)}).Run(ctx, AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: true})
1708 tt.wantErr(t, err)1708 tt.wantErr(t, err)
1709 for _, transport := range transports {1709 for _, transport := range transports {
1710 assertReconnectToastAttemptPublishesOnlyClearedTheme(t, transport)1710 assertReconnectToastAttemptPublishesOnlyClearedTheme(t, transport)
M internal/usecase/client/remote_attach_validation_test.go +9 -10
@@ -7,7 +7,6 @@ import (
7 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
11 portsmocks "github.com/bnema/vev/internal/ports/mocks"10 portsmocks "github.com/bnema/vev/internal/ports/mocks"
12 "github.com/bnema/vev/internal/protocol"11 "github.com/bnema/vev/internal/protocol"
13 )12 )
@@ -17,10 +16,10 @@ func TestMalformedAttachRequestsNeverDial(t *testing.T) {
17 name string16 name string
18 request AttachRequest17 request AttachRequest
19 }{18 }{
20- {name: "missing session name", request: AttachRequest{Intent: ports.IntentAttach}},19+ {name: "missing session name", request: AttachRequest{Intent: protocol.IntentAttach}},
21- {name: "unsafe session name", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "bad name"}},20+ {name: "unsafe session name", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "bad name"}},
22- {name: "daemon-owned without remote", request: AttachRequest{Intent: ports.IntentAttach, SessionName: "main", EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}},21+ {name: "daemon-owned without remote", request: AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}},
23- {name: "ephemeral with navigation capability", request: AttachRequest{Intent: ports.IntentEphemeral, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}},22+ {name: "ephemeral with navigation capability", request: AttachRequest{Intent: protocol.IntentEphemeral, NavigationCapabilities: protocol.NavigationCapabilityHomePicker}},
24 }23 }
25 for _, tt := range tests {24 for _, tt := range tests {
26 t.Run(tt.name, func(t *testing.T) {25 t.Run(tt.name, func(t *testing.T) {
@@ -42,17 +41,17 @@ func TestValidateAttachRequestRequiresDaemonOwnedEnvironmentForRemoteTarget(t *t
42 41
43 tests := []struct {42 tests := []struct {
44 name string43 name string
45- policy ports.EnvironmentPolicy44+ policy protocol.EnvironmentPolicy
46 wantOK bool45 wantOK bool
47 }{46 }{
48- {name: "daemon owned", policy: ports.EnvironmentPolicyDaemonOwned, wantOK: true},47+ {name: "daemon owned", policy: protocol.EnvironmentPolicyDaemonOwned, wantOK: true},
49- {name: "client owned", policy: ports.EnvironmentPolicyClientOwned},48+ {name: "client owned", policy: protocol.EnvironmentPolicyClientOwned},
50- {name: "unknown", policy: ports.EnvironmentPolicy(99)},49+ {name: "unknown", policy: protocol.EnvironmentPolicy(99)},
51 }50 }
52 for _, test := range tests {51 for _, test := range tests {
53 t.Run(test.name, func(t *testing.T) {52 t.Run(test.name, func(t *testing.T) {
54 err := validateAttachRequest(AttachRequest{53 err := validateAttachRequest(AttachRequest{
55- Intent: ports.IntentAttach, SessionName: target.SessionName,54+ Intent: protocol.IntentAttach, SessionName: target.SessionName,
56 RemoteTarget: target, EnvironmentPolicy: test.policy,55 RemoteTarget: target, EnvironmentPolicy: test.policy,
57 })56 })
58 if test.wantOK {57 if test.wantOK {
M internal/usecase/client/routes.go +5 -5
@@ -199,7 +199,7 @@ func routeFailureCode(err error) protocol.RouteFailureCode {
199 var protocolErr *ProtocolError199 var protocolErr *ProtocolError
200 if errors.As(err, &protocolErr) {200 if errors.As(err, &protocolErr) {
201 switch protocolErr.Code {201 switch protocolErr.Code {
202- case ports.ErrNoSuchSession, ports.ErrNoSuchTarget:202+ case protocol.ErrNoSuchSession, protocol.ErrNoSuchTarget:
203 return protocol.RouteFailureNoSuchRoute203 return protocol.RouteFailureNoSuchRoute
204 }204 }
205 }205 }
@@ -545,12 +545,12 @@ func (l *routeLedger) snapshot() protocol.RecentRouteSnapshot {
545 // samePeerHandoff restores a previously committed route on the daemon already545 // samePeerHandoff restores a previously committed route on the daemon already
546 // connected to by the client. The daemon deliberately sends no tab state: the546 // connected to by the client. The daemon deliberately sends no tab state: the
547 // ledger is the sole owner of per-route view memory.547 // ledger is the sole owner of per-route view memory.
548-func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachTarget) AttachRequest {548+func (l *routeLedger) samePeerHandoff(active AttachRequest, target protocol.AttachTarget) AttachRequest {
549 origin := normalizeRouteOrigin(active.Origin, active.Remote)549 origin := normalizeRouteOrigin(active.Origin, active.Remote)
550 originKey := normalizeRouteOriginKey(active.OriginKey, origin)550 originKey := normalizeRouteOriginKey(active.OriginKey, origin)
551 environmentPolicy := target.EnvironmentPolicy551 environmentPolicy := target.EnvironmentPolicy
552 if origin == protocol.RouteOriginLocal {552 if origin == protocol.RouteOriginLocal {
553- environmentPolicy = ports.EnvironmentPolicyClientOwned553+ environmentPolicy = protocol.EnvironmentPolicyClientOwned
554 }554 }
555 555
556 l.mu.RLock()556 l.mu.RLock()
@@ -568,7 +568,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
568 // original discovery target would make its Hello request a second,568 // original discovery target would make its Hello request a second,
569 // incorrect remote handoff instead of a same-peer session switch.569 // incorrect remote handoff instead of a same-peer session switch.
570 request.RemoteTarget = nil570 request.RemoteTarget = nil
571- if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {571+ if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == protocol.EnvironmentPolicyClientOwned {
572 request.NavigationCapabilities = 0572 request.NavigationCapabilities = 0
573 request.StartupOverlay = protocol.StartupOverlayNone573 request.StartupOverlay = protocol.StartupOverlayNone
574 }574 }
@@ -586,7 +586,7 @@ func (l *routeLedger) samePeerHandoff(active AttachRequest, target ports.AttachT
586 request.ExactTarget = target.ExactTarget586 request.ExactTarget = target.ExactTarget
587 request.PreferredTabID = ""587 request.PreferredTabID = ""
588 request.RemoteTarget = nil588 request.RemoteTarget = nil
589- if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == ports.EnvironmentPolicyClientOwned {589+ if request.EnvironmentPolicy != environmentPolicy && environmentPolicy == protocol.EnvironmentPolicyClientOwned {
590 request.NavigationCapabilities = 0590 request.NavigationCapabilities = 0
591 request.StartupOverlay = protocol.StartupOverlayNone591 request.StartupOverlay = protocol.StartupOverlayNone
592 }592 }
M internal/usecase/client/routes_test.go +17 -18
@@ -7,7 +7,6 @@ import (
7 "testing"7 "testing"
8 8
9 "github.com/bnema/vev/internal/domain"9 "github.com/bnema/vev/internal/domain"
10- "github.com/bnema/vev/internal/ports"
11 "github.com/bnema/vev/internal/protocol"10 "github.com/bnema/vev/internal/protocol"
12 "github.com/stretchr/testify/require"11 "github.com/stretchr/testify/require"
13 )12 )
@@ -27,7 +26,7 @@ func routeTestCandidate(index byte, origin protocol.RouteOrigin) routeCandidate
27 reachability: protocol.RouteReachabilityReachable,26 reachability: protocol.RouteReachabilityReachable,
28 },27 },
29 request: AttachRequest{28 request: AttachRequest{
30- Intent: ports.IntentAttach,29+ Intent: protocol.IntentAttach,
31 SessionName: target.SessionName,30 SessionName: target.SessionName,
32 Origin: origin,31 Origin: origin,
33 },32 },
@@ -37,7 +36,7 @@ func routeTestCandidate(index byte, origin protocol.RouteOrigin) routeCandidate
37 func TestRouteCandidateRetainsHostLabelWithoutDiscoveryTarget(t *testing.T) {36 func TestRouteCandidateRetainsHostLabelWithoutDiscoveryTarget(t *testing.T) {
38 target := routeTestTarget(1)37 target := routeTestTarget(1)
39 candidate := routeCandidateForAttach(AttachRequest{38 candidate := routeCandidateForAttach(AttachRequest{
40- Intent: ports.IntentAttach, SessionName: target.SessionName, Remote: true,39+ Intent: protocol.IntentAttach, SessionName: target.SessionName, Remote: true,
41 Origin: protocol.RouteOriginRemote, OriginKey: "user@remote", HostLabel: "remote",40 Origin: protocol.RouteOriginRemote, OriginKey: "user@remote", HostLabel: "remote",
42 }, protocol.CommittedRouteIdentity{Target: target}, nil, 0)41 }, protocol.CommittedRouteIdentity{Target: target}, nil, 0)
43 42
@@ -139,16 +138,16 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
139 {name: "recreated session does not reuse remembered tab", target: recreated, wantExactTarget: &recreated},138 {name: "recreated session does not reuse remembered tab", target: recreated, wantExactTarget: &recreated},
140 } {139 } {
141 t.Run(tt.name, func(t *testing.T) {140 t.Run(tt.name, func(t *testing.T) {
142- request := ledger.samePeerHandoff(second.request, ports.AttachTarget{141+ request := ledger.samePeerHandoff(second.request, protocol.AttachTarget{
143 Session: first.target.SessionName,142 Session: first.target.SessionName,
144- Intent: ports.IntentAttach,143+ Intent: protocol.IntentAttach,
145 ExactTarget: &tt.target,144 ExactTarget: &tt.target,
146- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,145+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
147 })146 })
148 require.Equal(t, first.target.SessionName, request.SessionName)147 require.Equal(t, first.target.SessionName, request.SessionName)
149 require.Equal(t, tt.wantExactTarget, request.ExactTarget)148 require.Equal(t, tt.wantExactTarget, request.ExactTarget)
150 require.Equal(t, tt.wantTab, request.PreferredTabID)149 require.Equal(t, tt.wantTab, request.PreferredTabID)
151- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)150+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
152 })151 })
153 }152 }
154 }153 }
@@ -156,7 +155,7 @@ func TestRouteLedgerSamePeerHandoffRestoresRouteTab(t *testing.T) {
156 func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *testing.T) {155 func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *testing.T) {
157 active := routeTestCandidate(2, protocol.RouteOriginLocal)156 active := routeTestCandidate(2, protocol.RouteOriginLocal)
158 active.originKey = "local"157 active.originKey = "local"
159- active.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned158+ active.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
160 active.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker159 active.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
161 active.request.StartupOverlay = protocol.StartupOverlaySessionPicker160 active.request.StartupOverlay = protocol.StartupOverlaySessionPicker
162 161
@@ -173,19 +172,19 @@ func TestRouteLedgerSamePeerHandoffClearsDaemonOwnedNavigationForLocalRoutes(t *
173 if tt.matched {172 if tt.matched {
174 stored := routeTestCandidate(1, protocol.RouteOriginLocal)173 stored := routeTestCandidate(1, protocol.RouteOriginLocal)
175 stored.originKey = "local"174 stored.originKey = "local"
176- stored.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned175+ stored.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
177 stored.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker176 stored.request.NavigationCapabilities = protocol.NavigationCapabilityHomePicker
178 stored.request.StartupOverlay = protocol.StartupOverlaySessionPicker177 stored.request.StartupOverlay = protocol.StartupOverlaySessionPicker
179 _, err := ledger.commit(stored)178 _, err := ledger.commit(stored)
180 require.NoError(t, err)179 require.NoError(t, err)
181 }180 }
182 181
183- request := ledger.samePeerHandoff(active.request, ports.AttachTarget{182+ request := ledger.samePeerHandoff(active.request, protocol.AttachTarget{
184- Session: target.SessionName, Intent: ports.IntentAttach, ExactTarget: &target,183+ Session: target.SessionName, Intent: protocol.IntentAttach, ExactTarget: &target,
185- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,184+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
186 })185 })
187 186
188- require.Equal(t, ports.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)187+ require.Equal(t, protocol.EnvironmentPolicyClientOwned, request.EnvironmentPolicy)
189 require.Zero(t, request.NavigationCapabilities)188 require.Zero(t, request.NavigationCapabilities)
190 require.Equal(t, protocol.StartupOverlayNone, request.StartupOverlay)189 require.Equal(t, protocol.StartupOverlayNone, request.StartupOverlay)
191 require.NoError(t, validateAttachRequest(request))190 require.NoError(t, validateAttachRequest(request))
@@ -199,7 +198,7 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
199 work.originKey = "remote"198 work.originKey = "remote"
200 work.request.Remote = true199 work.request.Remote = true
201 work.request.OriginKey = "remote"200 work.request.OriginKey = "remote"
202- work.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned201+ work.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
203 work.request.RemoteTarget = &domain.RemoteSessionTarget{202 work.request.RemoteTarget = &domain.RemoteSessionTarget{
204 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: work.target.LifecycleID,203 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: work.target.LifecycleID,
205 SessionName: work.target.SessionName, LiveTabID: "tab-work",204 SessionName: work.target.SessionName, LiveTabID: "tab-work",
@@ -211,13 +210,13 @@ func TestRouteLedgerSamePeerHandoffDropsOriginalRemoteTarget(t *testing.T) {
211 agents.originKey = "remote"210 agents.originKey = "remote"
212 agents.request.Remote = true211 agents.request.Remote = true
213 agents.request.OriginKey = "remote"212 agents.request.OriginKey = "remote"
214- agents.request.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned213+ agents.request.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
215 214
216- request := ledger.samePeerHandoff(agents.request, ports.AttachTarget{215+ request := ledger.samePeerHandoff(agents.request, protocol.AttachTarget{
217 Session: work.target.SessionName,216 Session: work.target.SessionName,
218- Intent: ports.IntentAttach,217+ Intent: protocol.IntentAttach,
219 ExactTarget: &work.target,218 ExactTarget: &work.target,
220- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,219+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
221 })220 })
222 221
223 require.True(t, request.Remote)222 require.True(t, request.Remote)
M internal/usecase/client/same_peer_switch_test.go +4 -4
@@ -24,7 +24,7 @@ func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
24 term.term,24 term.term,
25 transport,25 transport,
26 newReconnectHandshakeClock(t),26 newReconnectHandshakeClock(t),
27- AttachRequest{Intent: ports.IntentAttach, SessionName: "source"},27+ AttachRequest{Intent: protocol.IntentAttach, SessionName: "source"},
28 0,28 0,
29 &terminalThemeState{},29 &terminalThemeState{},
30 nil,30 nil,
@@ -35,9 +35,9 @@ func TestCloseAndDialAttachTargetSkipsSamePeerSwitch(t *testing.T) {
35 go func() { resultCh <- attempt.run(ctx) }()35 go func() { resultCh <- attempt.run(ctx) }()
36 36
37 exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{7}, SessionName: "stopped"}37 exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{7}, SessionName: "stopped"}
38- target := ports.AttachTarget{38+ target := protocol.AttachTarget{
39- Session: "stopped", Intent: ports.IntentAttach, ExactTarget: &exact,39+ Session: "stopped", Intent: protocol.IntentAttach, ExactTarget: &exact,
40- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,40+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
41 }41 }
42 transport.recvCh <- reconnectToastRecv{frame: ports.Frame{42 transport.recvCh <- reconnectToastRecv{frame: ports.Frame{
43 Type: ports.MsgAttachTarget, Payload: ports.MarshalAttachTarget(target),43 Type: ports.MsgAttachTarget, Payload: ports.MarshalAttachTarget(target),
M internal/usecase/client/session_connection.go +3 -2
@@ -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 var (12 var (
@@ -22,12 +23,12 @@ type SessionTarget struct {
22 23
23 func (t SessionTarget) validate() error {24 func (t SessionTarget) validate() error {
24 switch t.Intent {25 switch t.Intent {
25- case ports.IntentEphemeral:26+ case protocol.IntentEphemeral:
26 if t.SessionName != "" {27 if t.SessionName != "" {
27 return ErrEphemeralSessionName28 return ErrEphemeralSessionName
28 }29 }
29 return nil30 return nil
30- case ports.IntentNew, ports.IntentAttach, ports.IntentResume:31+ case protocol.IntentNew, protocol.IntentAttach, protocol.IntentResume:
31 if t.SessionName == "" {32 if t.SessionName == "" {
32 return domain.ErrInvalidSessionName33 return domain.ErrInvalidSessionName
33 }34 }
M internal/usecase/client/session_connection_test.go +11 -11
@@ -3,14 +3,14 @@ package client_test
3 import (3 import (
4 "testing"4 "testing"
5 5
6- "github.com/bnema/vev/internal/ports"
7 portsmocks "github.com/bnema/vev/internal/ports/mocks"6 portsmocks "github.com/bnema/vev/internal/ports/mocks"
7+ "github.com/bnema/vev/internal/protocol"
8 "github.com/bnema/vev/internal/usecase/client"8 "github.com/bnema/vev/internal/usecase/client"
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
10 )10 )
11 11
12 func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testing.T) {12 func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testing.T) {
13- target := client.SessionTarget{Intent: ports.IntentAttach, SessionName: "work"}13+ target := client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "work"}
14 local := portsmocks.NewMockTransport(t)14 local := portsmocks.NewMockTransport(t)
15 remote := portsmocks.NewMockTransport(t)15 remote := portsmocks.NewMockTransport(t)
16 16
@@ -20,7 +20,7 @@ func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testi
20 require.NoError(t, err)20 require.NoError(t, err)
21 21
22 require.Equal(t, localConnection.AttachRequest(), remoteConnection.AttachRequest())22 require.Equal(t, localConnection.AttachRequest(), remoteConnection.AttachRequest())
23- require.Equal(t, client.AttachRequest{Intent: ports.IntentAttach, SessionName: "work"}, localConnection.AttachRequest())23+ require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}, localConnection.AttachRequest())
24 require.Same(t, local, localConnection.Transport())24 require.Same(t, local, localConnection.Transport())
25 require.Same(t, remote, remoteConnection.Transport())25 require.Same(t, remote, remoteConnection.Transport())
26 }26 }
@@ -32,13 +32,13 @@ func TestSessionTargetValidation(t *testing.T) {
32 wantErr bool32 wantErr bool
33 wantErrIs error33 wantErrIs error
34 }{34 }{
35- {name: "ephemeral", target: client.SessionTarget{Intent: ports.IntentEphemeral}},35+ {name: "ephemeral", target: client.SessionTarget{Intent: protocol.IntentEphemeral}},
36- {name: "named ephemeral", target: client.SessionTarget{Intent: ports.IntentEphemeral, SessionName: "work"}, wantErr: true, wantErrIs: client.ErrEphemeralSessionName},36+ {name: "named ephemeral", target: client.SessionTarget{Intent: protocol.IntentEphemeral, SessionName: "work"}, wantErr: true, wantErrIs: client.ErrEphemeralSessionName},
37- {name: "new", target: client.SessionTarget{Intent: ports.IntentNew, SessionName: "work"}},37+ {name: "new", target: client.SessionTarget{Intent: protocol.IntentNew, SessionName: "work"}},
38- {name: "attach", target: client.SessionTarget{Intent: ports.IntentAttach, SessionName: "work"}},38+ {name: "attach", target: client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "work"}},
39- {name: "resume", target: client.SessionTarget{Intent: ports.IntentResume, SessionName: "work"}},39+ {name: "resume", target: client.SessionTarget{Intent: protocol.IntentResume, SessionName: "work"}},
40- {name: "missing named session", target: client.SessionTarget{Intent: ports.IntentAttach}, wantErr: true},40+ {name: "missing named session", target: client.SessionTarget{Intent: protocol.IntentAttach}, wantErr: true},
41- {name: "unsafe session", target: client.SessionTarget{Intent: ports.IntentAttach, SessionName: "my work"}, wantErr: true},41+ {name: "unsafe session", target: client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "my work"}, wantErr: true},
42 {name: "unknown intent", target: client.SessionTarget{Intent: 99, SessionName: "work"}, wantErr: true},42 {name: "unknown intent", target: client.SessionTarget{Intent: 99, SessionName: "work"}, wantErr: true},
43 }43 }
44 for _, tt := range tests {44 for _, tt := range tests {
@@ -58,6 +58,6 @@ func TestSessionTargetValidation(t *testing.T) {
58 }58 }
59 59
60 func TestSessionConnectionRequiresTransport(t *testing.T) {60 func TestSessionConnectionRequiresTransport(t *testing.T) {
61- _, err := client.NewSessionConnection(nil, client.SessionTarget{Intent: ports.IntentEphemeral})61+ _, err := client.NewSessionConnection(nil, client.SessionTarget{Intent: protocol.IntentEphemeral})
62 require.Error(t, err)62 require.Error(t, err)
63 }63 }
M internal/usecase/client/transition_ui.go +3 -2
@@ -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 const (13 const (
@@ -16,7 +17,7 @@ const (
16 17
17 var transitionSpinnerFrames = [...]rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}18 var transitionSpinnerFrames = [...]rune{'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'}
18 19
19-func transitionMessage(target ports.AttachTarget) string {20+func transitionMessage(target protocol.AttachTarget) string {
20 origin := target.Endpoint21 origin := target.Endpoint
21 if target.RemoteTarget != nil && target.RemoteTarget.DisplayOrigin != "" {22 if target.RemoteTarget != nil && target.RemoteTarget.DisplayOrigin != "" {
22 origin = target.RemoteTarget.DisplayOrigin23 origin = target.RemoteTarget.DisplayOrigin
@@ -55,7 +56,7 @@ func newTransitionUI(term ports.Terminal, clock ports.Clock, rawEntered *bool) *
55 return &transitionUI{term: term, clock: clock, rawEntered: rawEntered}56 return &transitionUI{term: term, clock: clock, rawEntered: rawEntered}
56 }57 }
57 58
58-func (u *transitionUI) start(target ports.AttachTarget) {59+func (u *transitionUI) start(target protocol.AttachTarget) {
59 if u == nil {60 if u == nil {
60 return61 return
61 }62 }
M internal/usecase/client/transition_ui_test.go +4 -3
@@ -11,15 +11,16 @@ 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 )15 )
15 16
16 func TestTransitionMessageDescribesSwitchAndStoppedRestore(t *testing.T) {17 func TestTransitionMessageDescribesSwitchAndStoppedRestore(t *testing.T) {
17- live := ports.AttachTarget{Endpoint: "remote", Session: "work"}18+ live := protocol.AttachTarget{Endpoint: "remote", Session: "work"}
18 stoppedTarget := domain.RemoteSessionTarget{19 stoppedTarget := domain.RemoteSessionTarget{
19 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{1},20 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: domain.SessionLifecycleID{1},
20 SessionName: "work", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-1"),21 SessionName: "work", Stopped: true, StoppedTab: domain.NewStableTabSelector("tab-1"),
21 }22 }
22- stopped := ports.AttachTarget{Endpoint: "remote", Session: "work", RemoteTarget: &stoppedTarget}23+ stopped := protocol.AttachTarget{Endpoint: "remote", Session: "work", RemoteTarget: &stoppedTarget}
23 24
24 require.Equal(t, "Switching to work@remote…", transitionMessage(live))25 require.Equal(t, "Switching to work@remote…", transitionMessage(live))
25 require.Equal(t, "Starting work@remote…", transitionMessage(stopped))26 require.Equal(t, "Starting work@remote…", transitionMessage(stopped))
@@ -94,7 +95,7 @@ func TestTransitionUIStartsAfterDelayTicksResizesAndStops(t *testing.T) {
94 term := &transitionTestTerminal{}95 term := &transitionTestTerminal{}
95 raw := tt.raw96 raw := tt.raw
96 ui := newTransitionUI(term, clock, &raw)97 ui := newTransitionUI(term, clock, &raw)
97- ui.start(ports.AttachTarget{Endpoint: "remote", Session: "work"})98+ ui.start(protocol.AttachTarget{Endpoint: "remote", Session: "work"})
98 99
99 require.Equal(t, []time.Duration{transitionSpinnerDelay}, clock.delays)100 require.Equal(t, []time.Duration{transitionSpinnerDelay}, clock.delays)
100 require.NotNil(t, ui.tickC())101 require.NotNil(t, ui.tickC())
M internal/usecase/client/transport_observability_test.go +5 -5
@@ -50,7 +50,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
50 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()50 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
51 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()
52 unblock := scriptRecv(transport,52 unblock := scriptRecv(transport,
53- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},53+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
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 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}))},
55 )55 )
56 defer unblock()56 defer unblock()
@@ -59,7 +59,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
59 ctx, cancel := context.WithCancel(context.Background())59 ctx, cancel := context.WithCancel(context.Background())
60 result := make(chan error, 1)60 result := make(chan error, 1)
61 go func() {61 go func() {
62- result <- runTestClient(ctx, testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: ports.IntentEphemeral})62+ result <- runTestClient(ctx, testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: protocol.IntentEphemeral})
63 }()63 }()
64 awaitClientRuntime(t, observer.entered, "blocking observer")64 awaitClientRuntime(t, observer.entered, "blocking observer")
65 awaitClientRuntime(t, flushed, "terminal flush")65 awaitClientRuntime(t, flushed, "terminal flush")
@@ -90,15 +90,15 @@ func TestTerminalFlushBoundaryTransportObservability(t *testing.T) {
90 transport.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()90 transport.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
91 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()91 transport.EXPECT().Send(isType(ports.MsgTheme)).Return(nil).Maybe()
92 unblock := scriptRecv(transport,92 unblock := scriptRecv(transport,
93- recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},93+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(protocol.Welcome{SessionID: "s1"}))},
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.MsgOutput, mustMarshalOutput(protocol.Output{Epoch: 1, Size: domain.Size{Cols: 1, Rows: 1}, Full: true, Data: []byte("unchanged-by-observer"), New: 3}))},
95- recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},95+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(protocol.Detached{Reason: protocol.ReasonDetach}))},
96 )96 )
97 defer unblock()97 defer unblock()
98 transport.EXPECT().Send(isType(ports.MsgAck)).Return(nil).Maybe()98 transport.EXPECT().Send(isType(ports.MsgAck)).Return(nil).Maybe()
99 transport.EXPECT().Close().Return(nil).Once()99 transport.EXPECT().Close().Return(nil).Once()
100 100
101- err := runTestClient(context.Background(), testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: ports.IntentEphemeral})101+ err := runTestClient(context.Background(), testDependencies(transportDialer{transport: transport}, term, realClock{}, nil, reporter), client.AttachRequest{Intent: protocol.IntentEphemeral})
102 require.NoError(t, err)102 require.NoError(t, err)
103 reporter.Flush()103 reporter.Flush()
104 require.Equal(t, "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07\x1b[?2031$punchanged-by-observer", out.String(), "observer must not alter terminal bytes")104 require.Equal(t, "\x1b]10;?\x07\x1b]11;?\x07\x1b]4;0;?;1;?;2;?;3;?;4;?;5;?;6;?;7;?;8;?;9;?;10;?;11;?;12;?;13;?;14;?;15;?\x07\x1b[?2031$punchanged-by-observer", out.String(), "observer must not alter terminal bytes")
M internal/usecase/daemon/attachment_effect_gate_test.go +8 -8
@@ -250,7 +250,7 @@ func TestKillSessionAcquisitionTimeoutDoesNotPublishPartialInvalidation(t *testi
250 frozenByKill := make(chan *attachedClient, 3)250 frozenByKill := make(chan *attachedClient, 3)
251 d.afterAttachmentEffectGateFrozen = func(_ string, ac *attachedClient) { frozenByKill <- ac }251 d.afterAttachmentEffectGateFrozen = func(_ string, ac *attachedClient) { frozenByKill <- ac }
252 killDone := make(chan error, 1)252 killDone := make(chan error, 1)
253- go func() { killDone <- d.killSession(sess, ports.ReasonSessionKilled, false) }()253+ go func() { killDone <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
254 254
255 require.Same(t, first, awaitTestValue(t, frozenByKill, "teardown did not acquire the first attachment effect gate"))255 require.Same(t, first, awaitTestValue(t, frozenByKill, "teardown did not acquire the first attachment effect gate"))
256 deadline := awaitTestValue(t, clock.timers, "teardown did not arm the gate acquisition deadline")256 deadline := awaitTestValue(t, clock.timers, "teardown did not arm the gate acquisition deadline")
@@ -343,7 +343,7 @@ func TestKillSessionInterruptsOnlyExactParticipantBlockedSends(t *testing.T) {
343 awaitTestCompletion(t, activeTransport.sendEntered, "active send did not block")343 awaitTestCompletion(t, activeTransport.sendEntered, "active send did not block")
344 344
345 killDone := make(chan error, 1)345 killDone := make(chan error, 1)
346- go func() { killDone <- d.killSession(sess, ports.ReasonSessionKilled, false) }()346+ go func() { killDone <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
347 select {347 select {
348 case err := <-killDone:348 case err := <-killDone:
349 require.NoError(t, err)349 require.NoError(t, err)
@@ -380,7 +380,7 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test
380 ac.replaceTransport(fresh)380 ac.replaceTransport(fresh)
381 }381 }
382 382
383- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))383+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
384 awaitTestCompletion(t, sendDone, "captured incarnation attachment effect ticket did not retire")384 awaitTestCompletion(t, sendDone, "captured incarnation attachment effect ticket did not retire")
385 require.True(t, stale.Closed(), "exact captured in-flight transport was not interrupted")385 require.True(t, stale.Closed(), "exact captured in-flight transport was not interrupted")
386 require.False(t, fresh.Closed(), "newer transport incarnation was interrupted")386 require.False(t, fresh.Closed(), "newer transport incarnation was interrupted")
@@ -408,7 +408,7 @@ func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T
408 clock := &signalClock{timers: make(chan *signalTimer, 1)}408 clock := &signalClock{timers: make(chan *signalTimer, 1)}
409 d.clock = clock409 d.clock = clock
410 shutdownDone := make(chan bool, 1)410 shutdownDone := make(chan bool, 1)
411- go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }()411+ go func() { shutdownDone <- d.shutdownAll(protocol.ReasonServerShutdown) }()
412 412
413 deadline := awaitTestValue(t, clock.timers, "shutdown did not arm the gate acquisition deadline")413 deadline := awaitTestValue(t, clock.timers, "shutdown did not arm the gate acquisition deadline")
414 deadline.ch <- clock.Now()414 deadline.ch <- clock.Now()
@@ -437,7 +437,7 @@ func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) {
437 awaitTestCompletion(t, blocked.sendEntered, "active send did not block")437 awaitTestCompletion(t, blocked.sendEntered, "active send did not block")
438 438
439 shutdownDone := make(chan bool, 1)439 shutdownDone := make(chan bool, 1)
440- go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }()440+ go func() { shutdownDone <- d.shutdownAll(protocol.ReasonServerShutdown) }()
441 select {441 select {
442 case incomplete := <-shutdownDone:442 case incomplete := <-shutdownDone:
443 require.False(t, incomplete)443 require.False(t, incomplete)
@@ -726,8 +726,8 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
726 token := source.captureAttachmentCapability(ac, transport)726 token := source.captureAttachmentCapability(ac, transport)
727 token.lease = rc.attachmentLease(ac)727 token.lease = rc.attachmentLease(ac)
728 ac.installTestAttachmentCapability(token)728 ac.installTestAttachmentCapability(token)
729- payload, err := ports.MarshalCommandRequest(ports.CommandRequest{729+ payload, err := ports.MarshalCommandRequest(protocol.CommandRequest{
730- Version: ports.ProtocolVersion, RequestID: 1, Slug: "back-session", Attached: true,730+ Version: protocol.Version, RequestID: 1, Slug: "back-session", Attached: true,
731 })731 })
732 require.NoError(t, err)732 require.NoError(t, err)
733 733
@@ -735,7 +735,7 @@ func TestAttachedNavigationCommandSendsResultAfterLocalTransition(t *testing.T)
735 require.Same(t, source, ac.currentAttachmentSession())735 require.Same(t, source, ac.currentAttachmentSession())
736 frames := transport.Sends()736 frames := transport.Sends()
737 require.NotEmpty(t, frames)737 require.NotEmpty(t, frames)
738- var result ports.CommandResult738+ var result protocol.CommandResult
739 for _, frame := range frames {739 for _, frame := range frames {
740 if frame.Type != ports.MsgCommandResult {740 if frame.Type != ports.MsgCommandResult {
741 continue741 continue
M internal/usecase/daemon/attachment_output.go +1 -1
@@ -42,7 +42,7 @@ type attachmentOutput struct {
42 42
43 // reconfigureAttachmentOutput applies a replacement terminal's declared43 // reconfigureAttachmentOutput applies a replacement terminal's declared
44 // capabilities to attachment-owned output state. Caller holds d.mu.44 // capabilities to attachment-owned output state. Caller holds d.mu.
45-func (d *Daemon) reconfigureAttachmentOutput(sess *session, ac *attachedClient, h ports.Hello) {45+func (d *Daemon) reconfigureAttachmentOutput(sess *session, ac *attachedClient, h protocol.Hello) {
46 if d == nil || ac == nil || ac.output == nil {46 if d == nil || ac == nil || ac.output == nil {
47 return47 return
48 }48 }
M internal/usecase/daemon/attachment_parking_test.go +24 -24
@@ -24,10 +24,10 @@ func TestAttachmentLossParksOnlyThatAttachment(t *testing.T) {
24 d := newTestDaemon(t, newFactory(t, pty), stubClock{})24 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
25 25
26 firstTransport := &closeTrackingTransport{}26 firstTransport := &closeTrackingTransport{}
27- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)27+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
28 require.NoError(t, err)28 require.NoError(t, err)
29 secondTransport := &closeTrackingTransport{}29 secondTransport := &closeTrackingTransport{}
30- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)30+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
31 require.NoError(t, err)31 require.NoError(t, err)
32 32
33 d.clientGone(sess, first, firstTransport, false)33 d.clientGone(sess, first, firstTransport, false)
@@ -51,10 +51,10 @@ func TestTwoAttachmentsParkIndependently(t *testing.T) {
51 defer release()51 defer release()
52 d := newTestDaemon(t, newFactory(t, pty), stubClock{})52 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
53 firstTransport := &closeTrackingTransport{}53 firstTransport := &closeTrackingTransport{}
54- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)54+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
55 require.NoError(t, err)55 require.NoError(t, err)
56 secondTransport := &closeTrackingTransport{}56 secondTransport := &closeTrackingTransport{}
57- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)57+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
58 require.NoError(t, err)58 require.NoError(t, err)
59 firstToken, secondToken := first.resumeToken, second.resumeToken59 firstToken, secondToken := first.resumeToken, second.resumeToken
60 60
@@ -76,10 +76,10 @@ func TestParkExpiryRemovesOnlyOneAttachment(t *testing.T) {
76 defer release()76 defer release()
77 d := newTestDaemon(t, newFactory(t, pty), stubClock{})77 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
78 firstTransport := &closeTrackingTransport{}78 firstTransport := &closeTrackingTransport{}
79- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)79+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
80 require.NoError(t, err)80 require.NoError(t, err)
81 secondTransport := &closeTrackingTransport{}81 secondTransport := &closeTrackingTransport{}
82- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)82+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
83 require.NoError(t, err)83 require.NoError(t, err)
84 d.clientGone(sess, first, firstTransport, false)84 d.clientGone(sess, first, firstTransport, false)
85 d.clientGone(sess, second, secondTransport, false)85 d.clientGone(sess, second, secondTransport, false)
@@ -101,14 +101,14 @@ func TestResumeRotatesCredentialAndRejectsStaleTransport(t *testing.T) {
101 defer release()101 defer release()
102 d := newTestDaemon(t, newFactory(t, pty), stubClock{})102 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
103 oldTransport := &closeTrackingTransport{}103 oldTransport := &closeTrackingTransport{}
104- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)104+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
105 require.NoError(t, err)105 require.NoError(t, err)
106 oldToken := ac.resumeToken106 oldToken := ac.resumeToken
107 oldGeneration := ac.lifecycle.generationValue()107 oldGeneration := ac.lifecycle.generationValue()
108 d.clientGone(sess, ac, oldTransport, false)108 d.clientGone(sess, ac, oldTransport, false)
109 109
110 newTransport := &closeTrackingTransport{}110 newTransport := &closeTrackingTransport{}
111- resumed, same, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", oldToken), newTransport, defaultSize)111+ resumed, same, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", oldToken), newTransport, defaultSize)
112 require.NoError(t, err)112 require.NoError(t, err)
113 require.True(t, ok)113 require.True(t, ok)
114 require.Same(t, sess, resumed)114 require.Same(t, sess, resumed)
@@ -135,14 +135,14 @@ func TestSuccessfulResumeRejectsOldCredentialAfterWelcome(t *testing.T) {
135 defer release()135 defer release()
136 d := newTestDaemon(t, newFactory(t, pty), stubClock{})136 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
137 oldTransport := &closeTrackingTransport{}137 oldTransport := &closeTrackingTransport{}
138- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)138+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
139 require.NoError(t, err)139 require.NoError(t, err)
140 oldToken := ac.resumeToken140 oldToken := ac.resumeToken
141 ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})141 ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1})
142 d.clientGone(sess, ac, oldTransport, false)142 d.clientGone(sess, ac, oldTransport, false)
143 143
144 resumedTransport := &closeTrackingTransport{}144 resumedTransport := &closeTrackingTransport{}
145- d.handleHello(resumedTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, "work", oldToken))})145+ d.handleHello(resumedTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", oldToken))})
146 newToken := ac.resumeToken146 newToken := ac.resumeToken
147 require.NotEqual(t, oldToken, newToken)147 require.NotEqual(t, oldToken, newToken)
148 d.mu.Lock()148 d.mu.Lock()
@@ -160,13 +160,13 @@ func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
160 defer release()160 defer release()
161 d := newTestDaemon(t, newFactory(t, pty), stubClock{})161 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
162 oldTransport := &closeTrackingTransport{}162 oldTransport := &closeTrackingTransport{}
163- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)163+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
164 require.NoError(t, err)164 require.NoError(t, err)
165 token := ac.resumeToken165 token := ac.resumeToken
166 d.clientGone(sess, ac, oldTransport, false)166 d.clientGone(sess, ac, oldTransport, false)
167 167
168 failed := &resumeWelcomeFailureTransport{}168 failed := &resumeWelcomeFailureTransport{}
169- d.handleHello(failed, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, "work", token))})169+ d.handleHello(failed, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(protocol.IntentResume, "work", token))})
170 require.Nil(t, ac.transport())170 require.Nil(t, ac.transport())
171 d.mu.Lock()171 d.mu.Lock()
172 parked := d.parked[token]172 parked := d.parked[token]
@@ -176,7 +176,7 @@ func TestFailedResumeHandshakeKeepsParkedCredential(t *testing.T) {
176 require.False(t, claimed)176 require.False(t, claimed)
177 require.Equal(t, token, ac.resumeToken)177 require.Equal(t, token, ac.resumeToken)
178 178
179- _, resumed, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)179+ _, resumed, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
180 require.NoError(t, err)180 require.NoError(t, err)
181 require.True(t, ok)181 require.True(t, ok)
182 require.Same(t, ac, resumed)182 require.Same(t, ac, resumed)
@@ -187,7 +187,7 @@ func TestConcurrentResumeHasExactlyOneWinner(t *testing.T) {
187 defer release()187 defer release()
188 d := newTestDaemon(t, newFactory(t, pty), stubClock{})188 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
189 oldTransport := &closeTrackingTransport{}189 oldTransport := &closeTrackingTransport{}
190- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)190+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
191 require.NoError(t, err)191 require.NoError(t, err)
192 token := ac.resumeToken192 token := ac.resumeToken
193 d.clientGone(sess, ac, oldTransport, false)193 d.clientGone(sess, ac, oldTransport, false)
@@ -199,7 +199,7 @@ func TestConcurrentResumeHasExactlyOneWinner(t *testing.T) {
199 results := make(chan result, 2)199 results := make(chan result, 2)
200 for range 2 {200 for range 2 {
201 go func() {201 go func() {
202- _, _, ok, resumeErr := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)202+ _, _, ok, resumeErr := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
203 results <- result{ok: ok, err: resumeErr}203 results <- result{ok: ok, err: resumeErr}
204 }()204 }()
205 }205 }
@@ -219,18 +219,18 @@ func TestKillSessionClosesEveryAttachmentAndParkedCredential(t *testing.T) {
219 defer release()219 defer release()
220 d := newTestDaemon(t, newFactory(t, pty), stubClock{})220 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
221 firstTransport := &closeTrackingTransport{}221 firstTransport := &closeTrackingTransport{}
222- sess, _, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)222+ sess, _, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
223 require.NoError(t, err)223 require.NoError(t, err)
224 secondTransport := &closeTrackingTransport{}224 secondTransport := &closeTrackingTransport{}
225- _, _, err = d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)225+ _, _, err = d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
226 require.NoError(t, err)226 require.NoError(t, err)
227 parkedTransport := &closeTrackingTransport{}227 parkedTransport := &closeTrackingTransport{}
228- _, parkedAttachment, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), parkedTransport)228+ _, parkedAttachment, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), parkedTransport)
229 require.NoError(t, err)229 require.NoError(t, err)
230 d.clientGone(sess, parkedAttachment, parkedTransport, false)230 d.clientGone(sess, parkedAttachment, parkedTransport, false)
231 parkedToken := parkedAttachment.resumeToken231 parkedToken := parkedAttachment.resumeToken
232 232
233- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))233+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
234 d.waitNotifies()234 d.waitNotifies()
235 require.True(t, firstTransport.Closed())235 require.True(t, firstTransport.Closed())
236 require.True(t, secondTransport.Closed())236 require.True(t, secondTransport.Closed())
@@ -246,14 +246,14 @@ func TestResumeCredentialIsInvalidAfterDaemonRestart(t *testing.T) {
246 defer release()246 defer release()
247 d := newTestDaemon(t, newFactory(t, pty), stubClock{})247 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
248 transport := &closeTrackingTransport{}248 transport := &closeTrackingTransport{}
249- _, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), transport)249+ _, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), transport)
250 require.NoError(t, err)250 require.NoError(t, err)
251 token := ac.resumeToken251 token := ac.resumeToken
252 restarted := newTestDaemon(t, newFactory(t, pty), stubClock{})252 restarted := newTestDaemon(t, newFactory(t, pty), stubClock{})
253- _, _, err = restarted.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})253+ _, _, err = restarted.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
254 var protocolErr *protoErr254 var protocolErr *protoErr
255 require.ErrorAs(t, err, &protocolErr)255 require.ErrorAs(t, err, &protocolErr)
256- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)256+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
257 }257 }
258 258
259 func TestHandleHelloAllowsMultipleAttachments(t *testing.T) {259 func TestHandleHelloAllowsMultipleAttachments(t *testing.T) {
@@ -261,10 +261,10 @@ func TestHandleHelloAllowsMultipleAttachments(t *testing.T) {
261 defer release()261 defer release()
262 d := newTestDaemon(t, newFactory(t, pty), stubClock{})262 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
263 firstTransport := &closeTrackingTransport{}263 firstTransport := &closeTrackingTransport{}
264- sess, first, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), firstTransport)264+ sess, first, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), firstTransport)
265 require.NoError(t, err)265 require.NoError(t, err)
266 secondTransport := &closeTrackingTransport{}266 secondTransport := &closeTrackingTransport{}
267- _, second, err := d.route(helloResumeCapable(ports.IntentAttach, "work", 0), secondTransport)267+ _, second, err := d.route(helloResumeCapable(protocol.IntentAttach, "work", 0), secondTransport)
268 require.NoError(t, err)268 require.NoError(t, err)
269 require.Equal(t, true, sess.attachmentRegistered(first))269 require.Equal(t, true, sess.attachmentRegistered(first))
270 require.Equal(t, true, sess.attachmentRegistered(second))270 require.Equal(t, true, sess.attachmentRegistered(second))
M internal/usecase/daemon/attachment_render_test.go +14 -14
@@ -103,14 +103,14 @@ func TestAttachmentResizeKeepsPeerWindowAndExpandsSharedContent(t *testing.T) {
103 defer releasePTY()103 defer releasePTY()
104 d := newTestDaemon(t, newFactory(t, pty), stubClock{})104 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
105 firstTransport, _ := newCapturingTransport(t)105 firstTransport, _ := newCapturingTransport(t)
106- sess, first, err := d.route(ports.Hello{106+ sess, first, err := d.route(protocol.Hello{
107- Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work",107+ Version: protocol.Version, Intent: protocol.IntentNew, Name: "work",
108 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},108 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},
109 }, firstTransport)109 }, firstTransport)
110 require.NoError(t, err)110 require.NoError(t, err)
111 secondTransport, _ := newCapturingTransport(t)111 secondTransport, _ := newCapturingTransport(t)
112- _, second, err := d.route(ports.Hello{112+ _, second, err := d.route(protocol.Hello{
113- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",113+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
114 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},114 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},
115 }, secondTransport)115 }, secondTransport)
116 require.NoError(t, err)116 require.NoError(t, err)
@@ -147,10 +147,10 @@ func TestAttachmentFirstPaintDoesNotWaitForBlockedPeer(t *testing.T) {
147 clock := &signalClock{timers: make(chan *signalTimer, 16)}147 clock := &signalClock{timers: make(chan *signalTimer, 16)}
148 d := newTestDaemon(t, newFactory(t, pty), clock)148 d := newTestDaemon(t, newFactory(t, pty), clock)
149 oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}149 oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
150- sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)150+ sess, old, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)
151 require.NoError(t, err)151 require.NoError(t, err)
152 newTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}152 newTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
153- _, fresh, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, newTransport)153+ _, fresh, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, newTransport)
154 require.NoError(t, err)154 require.NoError(t, err)
155 155
156 rc := sess.renderCoordinator()156 rc := sess.renderCoordinator()
@@ -183,7 +183,7 @@ func TestMultiAttachmentHandshakeFirstPaintNotGatedByBlockedPeer(t *testing.T) {
183 clock := &signalClock{timers: make(chan *signalTimer, 16)}183 clock := &signalClock{timers: make(chan *signalTimer, 16)}
184 d := newTestDaemon(t, newFactory(t, pty), clock)184 d := newTestDaemon(t, newFactory(t, pty), clock)
185 oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}185 oldTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
186- sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)186+ sess, old, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, oldTransport)
187 require.NoError(t, err)187 require.NoError(t, err)
188 rc := sess.renderCoordinator()188 rc := sess.renderCoordinator()
189 require.True(t, rc.markAttachmentReady(rc.attachmentLease(old)))189 require.True(t, rc.markAttachmentReady(rc.attachmentLease(old)))
@@ -201,7 +201,7 @@ func TestMultiAttachmentHandshakeFirstPaintNotGatedByBlockedPeer(t *testing.T) {
201 }()201 }()
202 awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output")202 awaitTestCompletion(t, oldTransport.entered, "slow attachment did not begin its blocked output")
203 203
204- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}204+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}
205 tr, sends, releaseConn := newConn(t, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)})205 tr, sends, releaseConn := newConn(t, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)})
206 defer releaseConn()206 defer releaseConn()
207 handshakeDone := make(chan struct{})207 handshakeDone := make(chan struct{})
@@ -223,10 +223,10 @@ func TestAttachmentPaintFanoutDoesNotWaitForBlockedPeer(t *testing.T) {
223 defer releasePTY()223 defer releasePTY()
224 d := newTestDaemon(t, newFactory(t, pty), stubClock{})224 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
225 firstTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}225 firstTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
226- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, firstTransport)226+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1}}, firstTransport)
227 require.NoError(t, err)227 require.NoError(t, err)
228 secondTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}228 secondTransport := &blockedAttachmentTransport{entered: make(chan struct{}), release: make(chan struct{}), sends: make(chan ports.Frame, 8)}
229- _, _, err = d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, secondTransport)229+ _, _, err = d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{2}}, secondTransport)
230 require.NoError(t, err)230 require.NoError(t, err)
231 rc := sess.renderCoordinator()231 rc := sess.renderCoordinator()
232 for _, ac := range sess.snapshotAttachments() {232 for _, ac := range sess.snapshotAttachments() {
@@ -259,14 +259,14 @@ func TestAttachmentResizeUsesLatestClaimedSessionGeometry(t *testing.T) {
259 defer releasePTY()259 defer releasePTY()
260 d := newTestDaemon(t, newFactory(t, pty), stubClock{})260 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
261 firstTransport, _ := newCapturingTransport(t)261 firstTransport, _ := newCapturingTransport(t)
262- sess, first, err := d.route(ports.Hello{262+ sess, first, err := d.route(protocol.Hello{
263- Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work",263+ Version: protocol.Version, Intent: protocol.IntentNew, Name: "work",
264 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},264 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},
265 }, firstTransport)265 }, firstTransport)
266 require.NoError(t, err)266 require.NoError(t, err)
267 secondTransport, _ := newCapturingTransport(t)267 secondTransport, _ := newCapturingTransport(t)
268- _, second, err := d.route(ports.Hello{268+ _, second, err := d.route(protocol.Hello{
269- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",269+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
270 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},270 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},
271 }, secondTransport)271 }, secondTransport)
272 require.NoError(t, err)272 require.NoError(t, err)
M internal/usecase/daemon/client.go +8 -8
@@ -1016,7 +1016,7 @@ func (d *Daemon) resizeAttachmentGeometryForLease(effect *attachmentEffect, geom
1016 // attachment-routing operation: attachment publication also takes routingMu1016 // attachment-routing operation: attachment publication also takes routingMu
1017 // before changing membership. Never retain sess.mu while touching notice or overlay1017 // before changing membership. Never retain sess.mu while touching notice or overlay
1018 // state, and retain the routingMu -> sess.mu order used by attachment paths.1018 // state, and retain the routingMu -> sess.mu order used by attachment paths.
1019-func (d *Daemon) handleClientNotice(sess *session, ac *attachedClient, notice ports.ClientNotice) {1019+func (d *Daemon) handleClientNotice(sess *session, ac *attachedClient, notice protocol.ClientNotice) {
1020 if sess == nil || ac == nil {1020 if sess == nil || ac == nil {
1021 return1021 return
1022 }1022 }
@@ -1036,7 +1036,7 @@ func (d *Daemon) handleClientNotice(sess *session, ac *attachedClient, notice po
1036 d.handleClientNoticeForAttachment(effect, notice)1036 d.handleClientNoticeForAttachment(effect, notice)
1037 }1037 }
1038 1038
1039-func (d *Daemon) handleClientNoticeForAttachment(effect *attachmentEffect, notice ports.ClientNotice) {1039+func (d *Daemon) handleClientNoticeForAttachment(effect *attachmentEffect, notice protocol.ClientNotice) {
1040 d.notices.routingMu.Lock()1040 d.notices.routingMu.Lock()
1041 if !effect.current() {1041 if !effect.current() {
1042 d.notices.routingMu.Unlock()1042 d.notices.routingMu.Unlock()
@@ -1060,7 +1060,7 @@ func (d *Daemon) handleClientNoticeForAttachment(effect *attachmentEffect, notic
1060 }1060 }
1061 }1061 }
1062 1062
1063-func (d *Daemon) handleClientNoticeDirect(sess *session, ac *attachedClient, notice ports.ClientNotice) {1063+func (d *Daemon) handleClientNoticeDirect(sess *session, ac *attachedClient, notice protocol.ClientNotice) {
1064 d.notices.routingMu.Lock()1064 d.notices.routingMu.Lock()
1065 sess.mu.Lock()1065 sess.mu.Lock()
1066 _, current := sess.attachments[ac]1066 _, current := sess.attachments[ac]
@@ -1079,15 +1079,15 @@ func (d *Daemon) handleClientNoticeDirect(sess *session, ac *attachedClient, not
1079 }1079 }
1080 }1080 }
1081 1081
1082-func (d *Daemon) mutateClientNotice(sess *session, ac *attachedClient, notice ports.ClientNotice) bool {1082+func (d *Daemon) mutateClientNotice(sess *session, ac *attachedClient, notice protocol.ClientNotice) bool {
1083 switch notice.Action {1083 switch notice.Action {
1084- case ports.ClientNoticeClipboardFallback:1084+ case protocol.ClientNoticeClipboardFallback:
1085 return d.recordClientNotice(sess, ac, domain.NoticeError, domain.NoticeClipboard, "image paste failed; sent Ctrl+V")1085 return d.recordClientNotice(sess, ac, domain.NoticeError, domain.NoticeClipboard, "image paste failed; sent Ctrl+V")
1086- case ports.ClientNoticeClipboardTooLarge:1086+ case protocol.ClientNoticeClipboardTooLarge:
1087 return d.recordClientNotice(sess, ac, domain.NoticeWarn, domain.NoticeClipboardTooLarge, "image too large to paste")1087 return d.recordClientNotice(sess, ac, domain.NoticeWarn, domain.NoticeClipboardTooLarge, "image too large to paste")
1088- case ports.ClientNoticeLinkDegraded:1088+ case protocol.ClientNoticeLinkDegraded:
1089 return d.recordClientNotice(sess, ac, domain.NoticeWarn, domain.NoticeConnection, "connection degraded")1089 return d.recordClientNotice(sess, ac, domain.NoticeWarn, domain.NoticeConnection, "connection degraded")
1090- case ports.ClientNoticeLinkConnected:1090+ case protocol.ClientNoticeLinkConnected:
1091 return d.dismissToastWithoutRepaint(ac, domain.NoticeConnection, sess.id)1091 return d.dismissToastWithoutRepaint(ac, domain.NoticeConnection, sess.id)
1092 default:1092 default:
1093 return false1093 return false
M internal/usecase/daemon/client_frame_routing.go +14 -14
@@ -234,52 +234,52 @@ func (d *Daemon) commandResultEffect(effect *attachmentEffect) (*attachmentEffec
234 return effect.ac.beginAttachmentEffect(capability)234 return effect.ac.beginAttachmentEffect(capability)
235 }235 }
236 236
237-func (d *Daemon) executeAttachedCommand(effect *attachmentEffect, request ports.CommandRequest) ports.CommandResult {237+func (d *Daemon) executeAttachedCommand(effect *attachmentEffect, request protocol.CommandRequest) protocol.CommandResult {
238- result := ports.CommandResult{RequestID: request.RequestID}238+ result := protocol.CommandResult{RequestID: request.RequestID}
239- if request.Version != ports.ProtocolVersion {239+ if request.Version != protocol.Version {
240- result.Code = ports.ErrInvalidCommandArgs240+ result.Code = protocol.ErrInvalidCommandArgs
241 result.Text = "unsupported command protocol version"241 result.Text = "unsupported command protocol version"
242 return result242 return result
243 }243 }
244 if !request.Attached {244 if !request.Attached {
245- result.Code = ports.ErrInvalidCommandArgs245+ result.Code = protocol.ErrInvalidCommandArgs
246 result.Text = "attached command flag is required"246 result.Text = "attached command flag is required"
247 return result247 return result
248 }248 }
249 if request.RequestID == 0 {249 if request.RequestID == 0 {
250- result.Code = ports.ErrInvalidCommandArgs250+ result.Code = protocol.ErrInvalidCommandArgs
251 result.Text = "command request id is required"251 result.Text = "command request id is required"
252 return result252 return result
253 }253 }
254 if request.Self {254 if request.Self {
255- result.Code = ports.ErrInvalidCommandArgs255+ result.Code = protocol.ErrInvalidCommandArgs
256 result.Text = "attached commands cannot target themselves"256 result.Text = "attached commands cannot target themselves"
257 return result257 return result
258 }258 }
259 if request.TargetSession != "" || request.TargetTab != "" || request.TargetPane != "" {259 if request.TargetSession != "" || request.TargetTab != "" || request.TargetPane != "" {
260- result.Code = ports.ErrInvalidCommandArgs260+ result.Code = protocol.ErrInvalidCommandArgs
261 result.Text = "attached commands cannot override their active session target"261 result.Text = "attached commands cannot override their active session target"
262 return result262 return result
263 }263 }
264 sess := effect.sess264 sess := effect.sess
265 if sess == nil || !effect.current() {265 if sess == nil || !effect.current() {
266- result.Code = ports.ErrNoSuchTarget266+ result.Code = protocol.ErrNoSuchTarget
267 result.Text = "attached session is no longer active"267 result.Text = "attached session is no longer active"
268 return result268 return result
269 }269 }
270 cmd, ok := command.BySlug(request.Slug)270 cmd, ok := command.BySlug(request.Slug)
271 if !ok {271 if !ok {
272- result.Code = ports.ErrUnknownCommand272+ result.Code = protocol.ErrUnknownCommand
273 result.Text = "unknown command: " + request.Slug273 result.Text = "unknown command: " + request.Slug
274 return result274 return result
275 }275 }
276 if !cmd.PaletteVisible {276 if !cmd.PaletteVisible {
277- result.Code = ports.ErrNotScriptable277+ result.Code = protocol.ErrNotScriptable
278 result.Text = request.Slug + " is not available in the palette"278 result.Text = request.Slug + " is not available in the palette"
279 return result279 return result
280 }280 }
281 if cmd.Run == nil {281 if cmd.Run == nil {
282- result.Code = ports.ErrNotScriptable282+ result.Code = protocol.ErrNotScriptable
283 result.Text = request.Slug + " is not scriptable"283 result.Text = request.Slug + " is not scriptable"
284 return result284 return result
285 }285 }
@@ -293,11 +293,11 @@ func (d *Daemon) executeAttachedCommand(effect *attachmentEffect, request ports.
293 return result293 return result
294 }294 }
295 if errors.Is(err, command.ErrInvalidArguments) {295 if errors.Is(err, command.ErrInvalidArguments) {
296- result.Code = ports.ErrInvalidCommandArgs296+ result.Code = protocol.ErrInvalidCommandArgs
297 result.Text = "usage: " + cmd.Usage297 result.Text = "usage: " + cmd.Usage
298 return result298 return result
299 }299 }
300- result.Code = ports.ErrInternal300+ result.Code = protocol.ErrInternal
301 result.Text = err.Error()301 result.Text = err.Error()
302 return result302 return result
303 }303 }
M internal/usecase/daemon/client_transport_errors.go +2 -1
@@ -2,6 +2,7 @@ package daemon
2 2
3 import (3 import (
4 "github.com/bnema/vev/internal/ports"4 "github.com/bnema/vev/internal/ports"
5+ "github.com/bnema/vev/internal/protocol"
5 )6 )
6 7
7 // clientGone detaches ac if it is still the session's current client. The8 // clientGone detaches ac if it is still the session's current client. The
@@ -110,7 +111,7 @@ func (d *Daemon) finishClientGone(sess *session, ac *attachedClient, failed port
110 // (the client is actively awaiting it), but deadline-bounded so a111 // (the client is actively awaiting it), but deadline-bounded so a
111 // wedged client cannot pin this conn handler and hang Serve's112 // wedged client cannot pin this conn handler and hang Serve's
112 // connWg.Wait.113 // connWg.Wait.
113- d.boundedSend(ac, frameDetached(ports.ReasonDetach))114+ d.boundedSend(ac, frameDetached(protocol.ReasonDetach))
114 }115 }
115 _ = ac.closeCapturedTransport(oldTr)116 _ = ac.closeCapturedTransport(oldTr)
116 d.log.Info("client detached", "session", name, "explicit", explicit)117 d.log.Info("client detached", "session", name, "explicit", explicit)
M internal/usecase/daemon/clipboard_test.go +1 -1
@@ -176,7 +176,7 @@ func TestKillSessionRemovesClipboardTempFiles(t *testing.T) {
176 path := string(injected)176 path := string(injected)
177 require.FileExists(t, path)177 require.FileExists(t, path)
178 178
179- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))179+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
180 require.NoFileExists(t, path)180 require.NoFileExists(t, path)
181 }181 }
182 182
M internal/usecase/daemon/coderabbit_regression_test.go +24 -23
@@ -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/picker"18 "github.com/bnema/vev/internal/usecase/picker"
18 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"19 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
19 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"20 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
@@ -40,7 +41,7 @@ func TestRestoreAmbiguousBadVersionLoadFailurePreservesCheckpoint(t *testing.T)
40 d.mu.Lock()41 d.mu.Lock()
41 entry := d.inactive[record.Name]42 entry := d.inactive[record.Name]
42 d.mu.Unlock()43 d.mu.Unlock()
43- require.Equal(t, ports.SessionBroken, entry.state)44+ require.Equal(t, protocol.SessionBroken, entry.state)
44 require.Equal(t, persisted, entry.record)45 require.Equal(t, persisted, entry.record)
45 }46 }
46 47
@@ -62,9 +63,9 @@ func TestRestoreTransientLoadFailureBecomesDiscardable(t *testing.T) {
62 d.mu.Lock()63 d.mu.Lock()
63 entry := d.inactive[record.Name]64 entry := d.inactive[record.Name]
64 d.mu.Unlock()65 d.mu.Unlock()
65- require.Equal(t, ports.SessionBroken, entry.state)66+ require.Equal(t, protocol.SessionBroken, entry.state)
66 require.Equal(t, persisted, entry.record)67 require.Equal(t, persisted, entry.record)
67- require.Equal(t, ports.SessionBroken, listSessions(t, d).Sessions[0].State)68+ require.Equal(t, protocol.SessionBroken, listSessions(t, d).Sessions[0].State)
68 select {69 select {
69 case <-entry.restoreDone:70 case <-entry.restoreDone:
70 default:71 default:
@@ -122,14 +123,14 @@ func TestRestoreFailureCannotOverwriteDiscardReplacement(t *testing.T) {
122 entry := d.inactive[record.Name]123 entry := d.inactive[record.Name]
123 d.mu.Unlock()124 d.mu.Unlock()
124 require.Equal(t, persisted, entry.record)125 require.Equal(t, persisted, entry.record)
125- require.Equal(t, ports.SessionDown, entry.state)126+ require.Equal(t, protocol.SessionDown, entry.state)
126 }127 }
127 128
128 func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {129 func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
129 tests := []struct {130 tests := []struct {
130 name string131 name string
131 replacement func(domain.CatalogueRecord) domain.CatalogueRecord132 replacement func(domain.CatalogueRecord) domain.CatalogueRecord
132- state ports.SessionState133+ state protocol.SessionState
133 }{134 }{
134 {135 {
135 name: "newer checkpoint",136 name: "newer checkpoint",
@@ -140,7 +141,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
140 record.Committed = &ref141 record.Committed = &ref
141 return record142 return record
142 },143 },
143- state: ports.SessionDown,144+ state: protocol.SessionDown,
144 },145 },
145 {146 {
146 name: "newer incarnation",147 name: "newer incarnation",
@@ -148,7 +149,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
148 record.IncarnationID = domain.IncarnationID{9}149 record.IncarnationID = domain.IncarnationID{9}
149 return record150 return record
150 },151 },
151- state: ports.SessionDown,152+ state: protocol.SessionDown,
152 },153 },
153 {154 {
154 name: "degraded authority",155 name: "degraded authority",
@@ -156,7 +157,7 @@ func TestStaleIncompatibleRestoreCannotOverwriteNewerAuthority(t *testing.T) {
156 record.DegradedReason = "newer restore decision"157 record.DegradedReason = "newer restore decision"
157 return record158 return record
158 },159 },
159- state: ports.SessionBroken,160+ state: protocol.SessionBroken,
160 },161 },
161 }162 }
162 for _, tt := range tests {163 for _, tt := range tests {
@@ -225,7 +226,7 @@ func TestCreateSessionRechecksShutdownAfterCatalogueRead(t *testing.T) {
225 d := newTestDaemon(t, nil, stubClock{})226 d := newTestDaemon(t, nil, stubClock{})
226 d.catalogue = catalogue227 d.catalogue = catalogue
227 d.persistEnabled = true228 d.persistEnabled = true
228- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, make(chan struct{}))229+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, make(chan struct{}))
229 230
230 result := make(chan error, 1)231 result := make(chan error, 1)
231 go func() {232 go func() {
@@ -276,7 +277,7 @@ func TestResumeStoppedSessionRejectsReplacementDuringCatalogueRead(t *testing.T)
276 d.catalogue = catalogue277 d.catalogue = catalogue
277 d.persistEnabled = true278 d.persistEnabled = true
278 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)279 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
279- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)280+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
280 281
281 target := picker.Target{282 target := picker.Target{
282 Name: record.Name,283 Name: record.Name,
@@ -294,7 +295,7 @@ func TestResumeStoppedSessionRejectsReplacementDuringCatalogueRead(t *testing.T)
294 replacement.UpdatedAt = replacement.CreatedAt295 replacement.UpdatedAt = replacement.CreatedAt
295 require.NoError(t, catalogue.Replace(replacement.Name, replacement))296 require.NoError(t, catalogue.Replace(replacement.Name, replacement))
296 d.mu.Lock()297 d.mu.Lock()
297- d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, ports.SessionDown, nil)298+ d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, protocol.SessionDown, nil)
298 d.mu.Unlock()299 d.mu.Unlock()
299 close(catalogue.release)300 close(catalogue.release)
300 301
@@ -322,7 +323,7 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
322 d.catalogue = catalogue323 d.catalogue = catalogue
323 d.persistEnabled = true324 d.persistEnabled = true
324 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)325 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
325- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)326+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
326 327
327 target := domain.RemoteSessionTarget{328 target := domain.RemoteSessionTarget{
328 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: record.IncarnationID,329 Endpoint: "remote", DisplayOrigin: "remote", LifecycleID: record.IncarnationID,
@@ -331,10 +332,10 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
331 transport, _ := newCapturingTransport(t)332 transport, _ := newCapturingTransport(t)
332 result := make(chan error, 1)333 result := make(chan error, 1)
333 go func() {334 go func() {
334- _, _, err := d.routeWithContext(context.Background(), ports.Hello{335+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
335- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name,336+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name,
336 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,337 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
337- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,338+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
338 }, transport)339 }, transport)
339 result <- err340 result <- err
340 }()341 }()
@@ -346,13 +347,13 @@ func TestRemoteInactiveRouteRejectsReplacementDuringCatalogueRead(t *testing.T)
346 replacement.UpdatedAt = replacement.CreatedAt347 replacement.UpdatedAt = replacement.CreatedAt
347 require.NoError(t, catalogue.Replace(replacement.Name, replacement))348 require.NoError(t, catalogue.Replace(replacement.Name, replacement))
348 d.mu.Lock()349 d.mu.Lock()
349- d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, ports.SessionDown, nil)350+ d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, protocol.SessionDown, nil)
350 d.mu.Unlock()351 d.mu.Unlock()
351 close(catalogue.release)352 close(catalogue.release)
352 353
353 var protocolErr *protoErr354 var protocolErr *protoErr
354 require.ErrorAs(t, <-result, &protocolErr)355 require.ErrorAs(t, <-result, &protocolErr)
355- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)356+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
356 require.Empty(t, catalogue.MetadataUpdates())357 require.Empty(t, catalogue.MetadataUpdates())
357 d.mu.Lock()358 d.mu.Lock()
358 defer d.mu.Unlock()359 defer d.mu.Unlock()
@@ -411,7 +412,7 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
411 d.catalogue = catalogue412 d.catalogue = catalogue
412 d.persistEnabled = true413 d.persistEnabled = true
413 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)414 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
414- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)415+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
415 416
416 target := test.target417 target := test.target
417 target.Endpoint = "remote"418 target.Endpoint = "remote"
@@ -421,10 +422,10 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
421 transport, _ := newCapturingTransport(t)422 transport, _ := newCapturingTransport(t)
422 result := make(chan error, 1)423 result := make(chan error, 1)
423 go func() {424 go func() {
424- _, _, err := d.routeWithContext(context.Background(), ports.Hello{425+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
425- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name,426+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name,
426 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,427 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
427- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,428+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
428 }, transport)429 }, transport)
429 result <- err430 result <- err
430 }()431 }()
@@ -438,14 +439,14 @@ func TestRemoteInactiveRouteRejectsTabAuthorityChangeDuringCatalogueRead(t *test
438 require.NoError(t, catalogue.Replace(replacement.Name, replacement))439 require.NoError(t, catalogue.Replace(replacement.Name, replacement))
439 if test.updateMap {440 if test.updateMap {
440 d.mu.Lock()441 d.mu.Lock()
441- d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, ports.SessionDown, nil)442+ d.inactive[replacement.Name] = inactiveSessionFromRecord(replacement, protocol.SessionDown, nil)
442 d.mu.Unlock()443 d.mu.Unlock()
443 }444 }
444 close(catalogue.release)445 close(catalogue.release)
445 446
446 var protocolErr *protoErr447 var protocolErr *protoErr
447 require.ErrorAs(t, <-result, &protocolErr)448 require.ErrorAs(t, <-result, &protocolErr)
448- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)449+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
449 require.Empty(t, catalogue.MetadataUpdates())450 require.Empty(t, catalogue.MetadataUpdates())
450 d.mu.Lock()451 d.mu.Lock()
451 defer d.mu.Unlock()452 defer d.mu.Unlock()
M internal/usecase/daemon/command_tracker.go +9 -8
@@ -7,6 +7,7 @@ import (
7 "time"7 "time"
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 const CommandRequestTimeout = 10 * time.Second13 const CommandRequestTimeout = 10 * time.Second
@@ -18,7 +19,7 @@ var (
18 19
19 // CommandRequestOutcome is the result delivered for one tracked request.20 // CommandRequestOutcome is the result delivered for one tracked request.
20 type CommandRequestOutcome struct {21 type CommandRequestOutcome struct {
21- Result ports.CommandResult22+ Result protocol.CommandResult
22 Err error23 Err error
23 }24 }
24 25
@@ -82,21 +83,21 @@ func (t *CommandRequestTracker) trackLocked(requestID, generation uint64) <-chan
82 83
83 // Wait waits without holding the tracker or connection sender lock. Timeout84 // Wait waits without holding the tracker or connection sender lock. Timeout
84 // and cancellation remove only the matching request.85 // and cancellation remove only the matching request.
85-func (t *CommandRequestTracker) Wait(ctx context.Context, clock ports.Clock, requestID, generation uint64, outcome <-chan CommandRequestOutcome) (ports.CommandResult, error) {86+func (t *CommandRequestTracker) Wait(ctx context.Context, clock ports.Clock, requestID, generation uint64, outcome <-chan CommandRequestOutcome) (protocol.CommandResult, error) {
86 if t == nil || clock == nil {87 if t == nil || clock == nil {
87- return ports.CommandResult{}, ErrCommandRequestUnavailable88+ return protocol.CommandResult{}, ErrCommandRequestUnavailable
88 }89 }
89 if ctx == nil {90 if ctx == nil {
90 ctx = context.Background()91 ctx = context.Background()
91 }92 }
92 if err := ctx.Err(); err != nil {93 if err := ctx.Err(); err != nil {
93 t.Remove(requestID, generation)94 t.Remove(requestID, generation)
94- return ports.CommandResult{}, err95+ return protocol.CommandResult{}, err
95 }96 }
96 timer := clock.NewTimer(CommandRequestTimeout)97 timer := clock.NewTimer(CommandRequestTimeout)
97 if timer == nil {98 if timer == nil {
98 t.Remove(requestID, generation)99 t.Remove(requestID, generation)
99- return ports.CommandResult{}, ErrCommandRequestTimeout100+ return protocol.CommandResult{}, ErrCommandRequestTimeout
100 }101 }
101 defer timer.Stop()102 defer timer.Stop()
102 select {103 select {
@@ -104,10 +105,10 @@ func (t *CommandRequestTracker) Wait(ctx context.Context, clock ports.Clock, req
104 return completed.Result, completed.Err105 return completed.Result, completed.Err
105 case <-ctx.Done():106 case <-ctx.Done():
106 t.Remove(requestID, generation)107 t.Remove(requestID, generation)
107- return ports.CommandResult{}, ctx.Err()108+ return protocol.CommandResult{}, ctx.Err()
108 case <-timer.C():109 case <-timer.C():
109 t.Remove(requestID, generation)110 t.Remove(requestID, generation)
110- return ports.CommandResult{}, ErrCommandRequestTimeout111+ return protocol.CommandResult{}, ErrCommandRequestTimeout
111 }112 }
112 }113 }
113 114
@@ -126,7 +127,7 @@ func (t *CommandRequestTracker) Remove(requestID, generation uint64) {
126 127
127 // Complete accepts only the exact request generation. Unknown, late, and old128 // Complete accepts only the exact request generation. Unknown, late, and old
128 // generation results are safe no-ops.129 // generation results are safe no-ops.
129-func (t *CommandRequestTracker) Complete(generation uint64, result ports.CommandResult) {130+func (t *CommandRequestTracker) Complete(generation uint64, result protocol.CommandResult) {
130 t.finish(result.RequestID, generation, CommandRequestOutcome{Result: result})131 t.finish(result.RequestID, generation, CommandRequestOutcome{Result: result})
131 }132 }
132 133
M internal/usecase/daemon/command_tracker_test.go +3 -2
@@ -10,6 +10,7 @@ import (
10 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
11 11
12 "github.com/bnema/vev/internal/ports"12 "github.com/bnema/vev/internal/ports"
13+ "github.com/bnema/vev/internal/protocol"
13 )14 )
14 15
15 type commandTrackerTestTimer struct {16 type commandTrackerTestTimer struct {
@@ -60,13 +61,13 @@ func TestCommandRequestTrackerTimeoutAndGenerationIsolation(t *testing.T) {
60 // old connection must not complete the new request.61 // old connection must not complete the new request.
61 secondOutcome, ok := tracker.Track(requestID, 8)62 secondOutcome, ok := tracker.Track(requestID, 8)
62 require.True(t, ok)63 require.True(t, ok)
63- tracker.Complete(4, ports.CommandResult{RequestID: requestID, OK: true})64+ tracker.Complete(4, protocol.CommandResult{RequestID: requestID, OK: true})
64 select {65 select {
65 case <-secondOutcome:66 case <-secondOutcome:
66 t.Fatal("late result from the old generation completed a newer request")67 t.Fatal("late result from the old generation completed a newer request")
67 default:68 default:
68 }69 }
69- tracker.Complete(8, ports.CommandResult{RequestID: requestID, OK: true})70+ tracker.Complete(8, protocol.CommandResult{RequestID: requestID, OK: true})
70 result := <-secondOutcome71 result := <-secondOutcome
71 require.True(t, result.Result.OK)72 require.True(t, result.Result.OK)
72 73
M internal/usecase/daemon/control.go +38 -37
@@ -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 "github.com/bnema/vev/internal/usecase/command"16 "github.com/bnema/vev/internal/usecase/command"
16 "github.com/bnema/vev/internal/usecase/layout"17 "github.com/bnema/vev/internal/usecase/layout"
17 )18 )
@@ -21,17 +22,17 @@ import (
21 func (d *Daemon) handleCommand(tr ports.Transport, f ports.Frame) error {22 func (d *Daemon) handleCommand(tr ports.Transport, f ports.Frame) error {
22 defer func() { _ = tr.Close() }()23 defer func() { _ = tr.Close() }()
23 24
24- if version, ok := ports.PeekCommandVersion(f.Payload); !ok || version != ports.ProtocolVersion {25+ if version, ok := ports.PeekCommandVersion(f.Payload); !ok || version != protocol.Version {
25- return d.sendCommandResult(tr, ports.CommandResult{Code: ports.ErrVersionMismatch, Text: "protocol version mismatch"})26+ return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrVersionMismatch, Text: "protocol version mismatch"})
26 }27 }
27 request, err := ports.UnmarshalCommandRequest(f.Payload)28 request, err := ports.UnmarshalCommandRequest(f.Payload)
28 if err != nil {29 if err != nil {
29- return d.sendCommandResult(tr, ports.CommandResult{Code: ports.ErrInternal, Text: "malformed command request"})30+ return d.sendCommandResult(tr, protocol.CommandResult{Code: protocol.ErrInternal, Text: "malformed command request"})
30 }31 }
31 if request.Attached {32 if request.Attached {
32- return d.sendCommandResult(tr, ports.CommandResult{33+ return d.sendCommandResult(tr, protocol.CommandResult{
33 RequestID: request.RequestID,34 RequestID: request.RequestID,
34- Code: ports.ErrNotScriptable,35+ Code: protocol.ErrNotScriptable,
35 Text: "attached command relay is not enabled",36 Text: "attached command relay is not enabled",
36 })37 })
37 }38 }
@@ -56,9 +57,9 @@ func (d *Daemon) handleCommand(tr ports.Transport, f ports.Frame) error {
56 }57 }
57 result, waitErr := tracker.Wait(commandCtx, commandClock, request.RequestID, generation, outcome)58 result, waitErr := tracker.Wait(commandCtx, commandClock, request.RequestID, generation, outcome)
58 if waitErr != nil {59 if waitErr != nil {
59- return d.sendCommandResult(tr, ports.CommandResult{60+ return d.sendCommandResult(tr, protocol.CommandResult{
60 RequestID: request.RequestID,61 RequestID: request.RequestID,
61- Code: ports.ErrInternal,62+ Code: protocol.ErrInternal,
62 Text: waitErr.Error(),63 Text: waitErr.Error(),
63 })64 })
64 }65 }
@@ -67,7 +68,7 @@ func (d *Daemon) handleCommand(tr ports.Transport, f ports.Frame) error {
67 68
68 // sendCommandResult gives one-shot control responses the same observable69 // sendCommandResult gives one-shot control responses the same observable
69 // transport-failure behavior regardless of which validation path produced it.70 // transport-failure behavior regardless of which validation path produced it.
70-func (d *Daemon) sendCommandResult(tr ports.Transport, result ports.CommandResult) error {71+func (d *Daemon) sendCommandResult(tr ports.Transport, result protocol.CommandResult) error {
71 if err := d.boundedControlSend(tr, frameCommandResult(result)); err != nil {72 if err := d.boundedControlSend(tr, frameCommandResult(result)); err != nil {
72 d.log.Warn("command response send failed", "err", err)73 d.log.Warn("command response send failed", "err", err)
73 return err74 return err
@@ -86,17 +87,17 @@ func (d *Daemon) boundedControlSend(tr ports.Transport, frame ports.Frame) error
86 return err87 return err
87 }88 }
88 89
89-func frameCommandResult(result ports.CommandResult) ports.Frame {90+func frameCommandResult(result protocol.CommandResult) ports.Frame {
90 return ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(result)}91 return ports.Frame{Type: ports.MsgCommandResult, Payload: ports.MarshalCommandResult(result)}
91 }92 }
92 93
93-func (d *Daemon) dispatchCommand(ctx context.Context, request ports.CommandRequest) ports.CommandResult {94+func (d *Daemon) dispatchCommand(ctx context.Context, request protocol.CommandRequest) protocol.CommandResult {
94 cmd, ok := command.BySlug(request.Slug)95 cmd, ok := command.BySlug(request.Slug)
95 if !ok {96 if !ok {
96- return commandFailure(ports.ErrUnknownCommand, "unknown command: "+request.Slug)97+ return commandFailure(protocol.ErrUnknownCommand, "unknown command: "+request.Slug)
97 }98 }
98 if !cmd.Scriptable || cmd.Control == nil {99 if !cmd.Scriptable || cmd.Control == nil {
99- return commandFailure(ports.ErrNotScriptable, request.Slug+" requires an attached client")100+ return commandFailure(protocol.ErrNotScriptable, request.Slug+" requires an attached client")
100 }101 }
101 if cmd.Target == command.TargetNone && !request.Self {102 if cmd.Target == command.TargetNone && !request.Self {
102 return d.runControl(cmd, controlExec{ctx: ctx, d: d, recoveryName: request.TargetSession}, request)103 return d.runControl(cmd, controlExec{ctx: ctx, d: d, recoveryName: request.TargetSession}, request)
@@ -122,7 +123,7 @@ func (d *Daemon) dispatchCommand(ctx context.Context, request ports.CommandReque
122 for {123 for {
123 select {124 select {
124 case <-ctx.Done():125 case <-ctx.Done():
125- return commandFailure(ports.ErrInternal, ctx.Err().Error())126+ return commandFailure(protocol.ErrInternal, ctx.Err().Error())
126 default:127 default:
127 }128 }
128 if sess.dispatchMu.TryLock() {129 if sess.dispatchMu.TryLock() {
@@ -139,51 +140,51 @@ func (d *Daemon) dispatchCommand(ctx context.Context, request ports.CommandReque
139 return d.runControl(cmd, controlExec{ctx: ctx, d: d, sess: sess, tab: tb, target: daemonActionTarget{session: sess, tab: tb, pane: pane}}, request)140 return d.runControl(cmd, controlExec{ctx: ctx, d: d, sess: sess, tab: tb, target: daemonActionTarget{session: sess, tab: tb, pane: pane}}, request)
140 }141 }
141 142
142-func (d *Daemon) runControl(cmd command.Command, exec controlExec, request ports.CommandRequest) ports.CommandResult {143+func (d *Daemon) runControl(cmd command.Command, exec controlExec, request protocol.CommandRequest) protocol.CommandResult {
143 result, err := cmd.Control(exec, request.Args, command.ControlOptions{JSON: request.JSON})144 result, err := cmd.Control(exec, request.Args, command.ControlOptions{JSON: request.JSON})
144 if err == nil {145 if err == nil {
145- return ports.CommandResult{OK: true, Output: result.Output}146+ return protocol.CommandResult{OK: true, Output: result.Output}
146 }147 }
147 switch {148 switch {
148 case errors.Is(err, command.ErrInvalidArguments), errors.Is(err, errSessionNameRequired), errors.Is(err, domain.ErrInvalidSessionName):149 case errors.Is(err, command.ErrInvalidArguments), errors.Is(err, errSessionNameRequired), errors.Is(err, domain.ErrInvalidSessionName):
149- return commandFailure(ports.ErrInvalidCommandArgs, "usage: "+cmd.Usage)150+ return commandFailure(protocol.ErrInvalidCommandArgs, "usage: "+cmd.Usage)
150 case errors.Is(err, errSessionNameInUse):151 case errors.Is(err, errSessionNameInUse):
151- return commandFailure(ports.ErrNameTaken, err.Error())152+ return commandFailure(protocol.ErrNameTaken, err.Error())
152 case isMoveCommandError(err):153 case isMoveCommandError(err):
153 return moveCommandFailure(err)154 return moveCommandFailure(err)
154 case errors.Is(err, layout.ErrNotInSplit):155 case errors.Is(err, layout.ErrNotInSplit):
155- return commandFailure(ports.ErrNoSuchTarget, "pane is not in a split")156+ return commandFailure(protocol.ErrNoSuchTarget, "pane is not in a split")
156 case errors.Is(err, layout.ErrTooSmall):157 case errors.Is(err, layout.ErrTooSmall):
157- return commandFailure(ports.ErrNoSuchTarget, "pane cannot be resized further")158+ return commandFailure(protocol.ErrNoSuchTarget, "pane cannot be resized further")
158 default:159 default:
159- return commandFailure(ports.ErrInternal, err.Error())160+ return commandFailure(protocol.ErrInternal, err.Error())
160 }161 }
161 }162 }
162 163
163-func commandFailure(code uint16, text string) ports.CommandResult {164+func commandFailure(code uint16, text string) protocol.CommandResult {
164- return ports.CommandResult{Code: code, Text: text}165+ return protocol.CommandResult{Code: code, Text: text}
165 }166 }
166 167
167 // resolveTargetSession applies explicit-name, stable-ID, then unique-session168 // resolveTargetSession applies explicit-name, stable-ID, then unique-session
168 // resolution. A name paired with complete stable IDs is always advisory because169 // resolution. A name paired with complete stable IDs is always advisory because
169 // a process keeps its original VEV session component after relocation.170 // a process keeps its original VEV session component after relocation.
170-func (d *Daemon) resolveTargetSession(request ports.CommandRequest) (*session, uint16, string) {171+func (d *Daemon) resolveTargetSession(request protocol.CommandRequest) (*session, uint16, string) {
171 d.mu.Lock()172 d.mu.Lock()
172 defer d.mu.Unlock()173 defer d.mu.Unlock()
173 174
174 named := d.findByNameLocked(request.TargetSession)175 named := d.findByNameLocked(request.TargetSession)
175 if request.Self && (request.TargetTab == "" || request.TargetPane == "") {176 if request.Self && (request.TargetTab == "" || request.TargetPane == "") {
176- return nil, ports.ErrNoSuchTarget, "--self requires target tab and pane IDs"177+ return nil, protocol.ErrNoSuchTarget, "--self requires target tab and pane IDs"
177 }178 }
178 if request.TargetSession != "" && request.TargetTab == "" && request.TargetPane == "" {179 if request.TargetSession != "" && request.TargetTab == "" && request.TargetPane == "" {
179 if named == nil {180 if named == nil {
180- return nil, ports.ErrNoSuchTarget, "no such session: " + request.TargetSession181+ return nil, protocol.ErrNoSuchTarget, "no such session: " + request.TargetSession
181 }182 }
182 return named, 0, ""183 return named, 0, ""
183 }184 }
184 if request.TargetTab != "" || request.TargetPane != "" {185 if request.TargetTab != "" || request.TargetPane != "" {
185 if request.TargetTab == "" || request.TargetPane == "" {186 if request.TargetTab == "" || request.TargetPane == "" {
186- return nil, ports.ErrNoSuchTarget, "target tab and pane IDs must be provided together"187+ return nil, protocol.ErrNoSuchTarget, "target tab and pane IDs must be provided together"
187 }188 }
188 var match *session189 var match *session
189 for _, sess := range d.sessions {190 for _, sess := range d.sessions {
@@ -191,21 +192,21 @@ func (d *Daemon) resolveTargetSession(request ports.CommandRequest) (*session, u
191 continue192 continue
192 }193 }
193 if match != nil {194 if match != nil {
194- return nil, ports.ErrAmbiguousTarget, "target tab and pane IDs are ambiguous"195+ return nil, protocol.ErrAmbiguousTarget, "target tab and pane IDs are ambiguous"
195 }196 }
196 match = sess197 match = sess
197 }198 }
198 if match != nil {199 if match != nil {
199 return match, 0, ""200 return match, 0, ""
200 }201 }
201- return nil, ports.ErrNoSuchTarget, "no live session contains the target tab/pane"202+ return nil, protocol.ErrNoSuchTarget, "no live session contains the target tab/pane"
202 }203 }
203 locals := sessionsSnapshot(d.sessions)204 locals := sessionsSnapshot(d.sessions)
204 if len(locals) == 0 {205 if len(locals) == 0 {
205- return nil, ports.ErrNoSuchTarget, "no live sessions"206+ return nil, protocol.ErrNoSuchTarget, "no live sessions"
206 }207 }
207 if len(locals) != 1 {208 if len(locals) != 1 {
208- return nil, ports.ErrAmbiguousTarget, "several sessions are live; use -s <session> or run from inside a pane"209+ return nil, protocol.ErrAmbiguousTarget, "several sessions are live; use -s <session> or run from inside a pane"
209 }210 }
210 return locals[0], 0, ""211 return locals[0], 0, ""
211 }212 }
@@ -226,7 +227,7 @@ func (s *session) containsStableIDs(tabID, paneID string) bool {
226 return false227 return false
227 }228 }
228 229
229-func resolveControlTarget(sess *session, kind command.TargetKind, request ports.CommandRequest) (*tab, *pane, uint16, string) {230+func resolveControlTarget(sess *session, kind command.TargetKind, request protocol.CommandRequest) (*tab, *pane, uint16, string) {
230 sess.mu.Lock()231 sess.mu.Lock()
231 tabs := append([]*tab(nil), sess.tabs...)232 tabs := append([]*tab(nil), sess.tabs...)
232 sess.mu.Unlock()233 sess.mu.Unlock()
@@ -240,14 +241,14 @@ func resolveControlTarget(sess *session, kind command.TargetKind, request ports.
240 target := paneByStableIDLocked(tb, request.TargetPane)241 target := paneByStableIDLocked(tb, request.TargetPane)
241 tb.mu.Unlock()242 tb.mu.Unlock()
242 if target == nil {243 if target == nil {
243- return nil, nil, ports.ErrNoSuchTarget, "target pane does not belong to target tab"244+ return nil, nil, protocol.ErrNoSuchTarget, "target pane does not belong to target tab"
244 }245 }
245 return tb, target, 0, ""246 return tb, target, 0, ""
246 }247 }
247- return nil, nil, ports.ErrNoSuchTarget, "no such target tab"248+ return nil, nil, protocol.ErrNoSuchTarget, "no such target tab"
248 }249 }
249 if len(tabs) == 0 {250 if len(tabs) == 0 {
250- return nil, nil, ports.ErrNoSuchTarget, "target session has no tabs"251+ return nil, nil, protocol.ErrNoSuchTarget, "target session has no tabs"
251 }252 }
252 tb := tabs[0]253 tb := tabs[0]
253 if kind != command.TargetPane {254 if kind != command.TargetPane {
@@ -257,7 +258,7 @@ func resolveControlTarget(sess *session, kind command.TargetKind, request ports.
257 target := tb.focusedPane()258 target := tb.focusedPane()
258 tb.mu.Unlock()259 tb.mu.Unlock()
259 if target == nil {260 if target == nil {
260- return nil, nil, ports.ErrNoSuchTarget, "no such target pane"261+ return nil, nil, protocol.ErrNoSuchTarget, "no such target pane"
261 }262 }
262 return tb, target, 0, ""263 return tb, target, 0, ""
263 }264 }
@@ -657,7 +658,7 @@ func (e controlExec) SessionRecovery(action string) (string, error) {
657 return "", err658 return "", err
658 }659 }
659 if ok {660 if ok {
660- e.d.setStoppedRecovery(record, ports.SessionDown)661+ e.d.setStoppedRecovery(record, protocol.SessionDown)
661 }662 }
662 return "", nil663 return "", nil
663 }664 }
@@ -772,7 +773,7 @@ func (e controlExec) RemoteCatalog(asJSON bool) (string, error) {
772 return rows[i].LifecycleID.String() < rows[j].LifecycleID.String()773 return rows[i].LifecycleID.String() < rows[j].LifecycleID.String()
773 })774 })
774 catalog := ports.RemoteCatalog{775 catalog := ports.RemoteCatalog{
775- ProtocolVersion: ports.ProtocolVersion,776+ ProtocolVersion: protocol.Version,
776 SchemaVersion: ports.RemoteCatalogSchemaVersion,777 SchemaVersion: ports.RemoteCatalogSchemaVersion,
777 Sessions: rows,778 Sessions: rows,
778 }779 }
M internal/usecase/daemon/control_test.go +113 -112
@@ -16,6 +16,7 @@ import (
16 "github.com/bnema/vev/internal/domain"16 "github.com/bnema/vev/internal/domain"
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/command"20 "github.com/bnema/vev/internal/usecase/command"
20 "github.com/bnema/vev/internal/usecase/layout"21 "github.com/bnema/vev/internal/usecase/layout"
21 "github.com/stretchr/testify/require"22 "github.com/stretchr/testify/require"
@@ -69,7 +70,7 @@ func TestConsumeOrExpelControlSelfTargetsNonFocusedPane(t *testing.T) {
69 targetStableID := h.panes["pane-3"].stableID70 targetStableID := h.panes["pane-3"].stableID
70 h.tab.mu.Unlock()71 h.tab.mu.Unlock()
71 72
72- result := sendCommand(t, h.daemon, ports.CommandRequest{73+ result := sendCommand(t, h.daemon, protocol.CommandRequest{
73 Slug: "consume-or-expel-pane-left",74 Slug: "consume-or-expel-pane-left",
74 Self: true,75 Self: true,
75 TargetSession: "work",76 TargetSession: "work",
@@ -100,7 +101,7 @@ func TestConsumeOrExpelControlEdgeNoopReturnsOKAndPreservesFocus(t *testing.T) {
100 h.session.installRenderCoordinator(rc)101 h.session.installRenderCoordinator(rc)
101 before := h.snapshot()102 before := h.snapshot()
102 103
103- result := sendCommand(t, h.daemon, ports.CommandRequest{104+ result := sendCommand(t, h.daemon, protocol.CommandRequest{
104 Slug: "consume-or-expel-pane-left",105 Slug: "consume-or-expel-pane-left",
105 Self: true,106 Self: true,
106 TargetSession: "work",107 TargetSession: "work",
@@ -130,7 +131,7 @@ func TestHandleCommandTimesOutWithRequestGeneration(t *testing.T) {
130 clock := &signalClock{timers: make(chan *signalTimer, 1)}131 clock := &signalClock{timers: make(chan *signalTimer, 1)}
131 d := newTestDaemon(t, factory, clock)132 d := newTestDaemon(t, factory, clock)
132 addControlSession(d, "work", "t_work", "p_work")133 addControlSession(d, "work", "t_work", "p_work")
133- frame := commandFrame(t, ports.CommandRequest{134+ frame := commandFrame(t, protocol.CommandRequest{
134 RequestID: 17, Slug: "new-tab", TargetSession: "work",135 RequestID: 17, Slug: "new-tab", TargetSession: "work",
135 })136 })
136 tr, sends, releaseConn := newConn(t, frame)137 tr, sends, releaseConn := newConn(t, frame)
@@ -145,7 +146,7 @@ func TestHandleCommandTimesOutWithRequestGeneration(t *testing.T) {
145 timer.ch <- time.Time{}146 timer.ch <- time.Time{}
146 result := awaitCommandResult(t, sends)147 result := awaitCommandResult(t, sends)
147 require.Equal(t, uint64(17), result.RequestID)148 require.Equal(t, uint64(17), result.RequestID)
148- require.Equal(t, ports.ErrInternal, result.Code)149+ require.Equal(t, protocol.ErrInternal, result.Code)
149 require.Equal(t, ErrCommandRequestTimeout.Error(), result.Text)150 require.Equal(t, ErrCommandRequestTimeout.Error(), result.Text)
150 require.NoError(t, <-done)151 require.NoError(t, <-done)
151 close(factory.release)152 close(factory.release)
@@ -155,7 +156,7 @@ func TestHandleCommandResponseSendTimeoutClosesTransport(t *testing.T) {
155 clock := &signalClock{timers: make(chan *signalTimer, 1)}156 clock := &signalClock{timers: make(chan *signalTimer, 1)}
156 d := newTestDaemon(t, nil, clock)157 d := newTestDaemon(t, nil, clock)
157 tr := newBlockingControlSendTransport()158 tr := newBlockingControlSendTransport()
158- frame := commandFrame(t, ports.CommandRequest{RequestID: 41, Attached: true})159+ frame := commandFrame(t, protocol.CommandRequest{RequestID: 41, Attached: true})
159 done := make(chan error, 1)160 done := make(chan error, 1)
160 go func() { done <- d.handleCommand(tr, frame) }()161 go func() { done <- d.handleCommand(tr, frame) }()
161 162
@@ -182,7 +183,7 @@ func TestHandleCommandAttachedRejectionReturnsSendFailure(t *testing.T) {
182 sendErr := errors.New("command result send failed")183 sendErr := errors.New("command result send failed")
183 tr := &commandSendErrorTransport{err: sendErr}184 tr := &commandSendErrorTransport{err: sendErr}
184 185
185- err := d.handleCommand(tr, commandFrame(t, ports.CommandRequest{186+ err := d.handleCommand(tr, commandFrame(t, protocol.CommandRequest{
186 RequestID: 41,187 RequestID: 41,
187 Attached: true,188 Attached: true,
188 }))189 }))
@@ -195,7 +196,7 @@ func TestHandleCommandAttachedRejectionReturnsSendFailure(t *testing.T) {
195 func TestHandleCommandRejectsVersionBeforeDecodeOrDispatch(t *testing.T) {196 func TestHandleCommandRejectsVersionBeforeDecodeOrDispatch(t *testing.T) {
196 // The version prefix is valid and mismatched, while the remainder is not a197 // The version prefix is valid and mismatched, while the remainder is not a
197 // decodable request. The daemon must still return ErrVersionMismatch.198 // decodable request. The daemon must still return ErrVersionMismatch.
198- frame := ports.Frame{Type: ports.MsgCommand, Payload: []byte{0, byte(ports.ProtocolVersion + 1)}}199+ frame := ports.Frame{Type: ports.MsgCommand, Payload: []byte{0, byte(protocol.Version + 1)}}
199 tr, sends, _ := newConn(t, frame)200 tr, sends, _ := newConn(t, frame)
200 201
201 d := newTestDaemon(t, nil, stubClock{})202 d := newTestDaemon(t, nil, stubClock{})
@@ -203,12 +204,12 @@ func TestHandleCommandRejectsVersionBeforeDecodeOrDispatch(t *testing.T) {
203 204
204 result := awaitCommandResult(t, sends)205 result := awaitCommandResult(t, sends)
205 require.False(t, result.OK)206 require.False(t, result.OK)
206- require.Equal(t, ports.ErrVersionMismatch, result.Code)207+ require.Equal(t, protocol.ErrVersionMismatch, result.Code)
207 }208 }
208 209
209 func TestHandleConnRoutesCommand(t *testing.T) {210 func TestHandleConnRoutesCommand(t *testing.T) {
210 d := newTestDaemon(t, nil, stubClock{})211 d := newTestDaemon(t, nil, stubClock{})
211- request := ports.CommandRequest{Version: ports.ProtocolVersion, Slug: "list-sessions"}212+ request := protocol.CommandRequest{Version: protocol.Version, Slug: "list-sessions"}
212 payload, err := ports.MarshalCommandRequest(request)213 payload, err := ports.MarshalCommandRequest(request)
213 require.NoError(t, err)214 require.NoError(t, err)
214 frame := ports.Frame{Type: ports.MsgCommand, Payload: payload}215 frame := ports.Frame{Type: ports.MsgCommand, Payload: payload}
@@ -224,35 +225,35 @@ func TestHandleCommandDispatchAndTargetErrors(t *testing.T) {
224 tests := []struct {225 tests := []struct {
225 name string226 name string
226 arrange func(*Daemon)227 arrange func(*Daemon)
227- request ports.CommandRequest228+ request protocol.CommandRequest
228 code uint16229 code uint16
229 }{230 }{
230- {name: "unknown slug", request: ports.CommandRequest{Slug: "no-such"}, code: ports.ErrUnknownCommand},231+ {name: "unknown slug", request: protocol.CommandRequest{Slug: "no-such"}, code: protocol.ErrUnknownCommand},
231- {name: "non-scriptable command", request: ports.CommandRequest{Slug: "session-picker"}, code: ports.ErrNotScriptable},232+ {name: "non-scriptable command", request: protocol.CommandRequest{Slug: "session-picker"}, code: protocol.ErrNotScriptable},
232- {name: "no sessions", request: ports.CommandRequest{Slug: "split-right"}, code: ports.ErrNoSuchTarget},233+ {name: "no sessions", request: protocol.CommandRequest{Slug: "split-right"}, code: protocol.ErrNoSuchTarget},
233 {name: "ambiguous sessions", arrange: func(d *Daemon) {234 {name: "ambiguous sessions", arrange: func(d *Daemon) {
234 addControlSession(d, "one", "t_one", "p_one")235 addControlSession(d, "one", "t_one", "p_one")
235 addControlSession(d, "two", "t_two", "p_two")236 addControlSession(d, "two", "t_two", "p_two")
236- }, request: ports.CommandRequest{Slug: "split-right"}, code: ports.ErrAmbiguousTarget},237+ }, request: protocol.CommandRequest{Slug: "split-right"}, code: protocol.ErrAmbiguousTarget},
237 {name: "missing explicit session", arrange: func(d *Daemon) {238 {name: "missing explicit session", arrange: func(d *Daemon) {
238 addControlSession(d, "work", "t_work", "p_work")239 addControlSession(d, "work", "t_work", "p_work")
239- }, request: ports.CommandRequest{Slug: "new-tab", TargetSession: "missing"}, code: ports.ErrNoSuchTarget},240+ }, request: protocol.CommandRequest{Slug: "new-tab", TargetSession: "missing"}, code: protocol.ErrNoSuchTarget},
240 {name: "missing stable IDs", arrange: func(d *Daemon) {241 {name: "missing stable IDs", arrange: func(d *Daemon) {
241 addControlSession(d, "work", "t_work", "p_work")242 addControlSession(d, "work", "t_work", "p_work")
242- }, request: ports.CommandRequest{Slug: "split-right", TargetTab: "t_nope", TargetPane: "p_nope"}, code: ports.ErrNoSuchTarget},243+ }, request: protocol.CommandRequest{Slug: "split-right", TargetTab: "t_nope", TargetPane: "p_nope"}, code: protocol.ErrNoSuchTarget},
243 {name: "duplicate stable IDs are ambiguous", arrange: func(d *Daemon) {244 {name: "duplicate stable IDs are ambiguous", arrange: func(d *Daemon) {
244 addControlSession(d, "one", "t_shared", "p_shared")245 addControlSession(d, "one", "t_shared", "p_shared")
245 addControlSession(d, "two", "t_shared", "p_shared")246 addControlSession(d, "two", "t_shared", "p_shared")
246- }, request: ports.CommandRequest{Slug: "toast", Args: []string{"hello"}, TargetTab: "t_shared", TargetPane: "p_shared"}, code: ports.ErrAmbiguousTarget},247+ }, request: protocol.CommandRequest{Slug: "toast", Args: []string{"hello"}, TargetTab: "t_shared", TargetPane: "p_shared"}, code: protocol.ErrAmbiguousTarget},
247 {name: "self requires both IDs", arrange: func(d *Daemon) {248 {name: "self requires both IDs", arrange: func(d *Daemon) {
248 addControlSession(d, "work", "t_work", "p_work")249 addControlSession(d, "work", "t_work", "p_work")
249- }, request: ports.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work"}, code: ports.ErrNoSuchTarget},250+ }, request: protocol.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work"}, code: protocol.ErrNoSuchTarget},
250 {name: "self rejects pane from another tab", arrange: func(d *Daemon) {251 {name: "self rejects pane from another tab", arrange: func(d *Daemon) {
251 sess := addControlSession(d, "work", "t_work", "p_work")252 sess := addControlSession(d, "work", "t_work", "p_work")
252 other := newTabWithStableID("t_other", "p_other", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})253 other := newTabWithStableID("t_other", "p_other", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
253 other.ctx, other.cancel = context.WithCancel(d.serveCtx)254 other.ctx, other.cancel = context.WithCancel(d.serveCtx)
254 sess.tabs = append(sess.tabs, other)255 sess.tabs = append(sess.tabs, other)
255- }, request: ports.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work", TargetPane: "p_other"}, code: ports.ErrNoSuchTarget},256+ }, request: protocol.CommandRequest{Slug: "split-right", Self: true, TargetSession: "work", TargetTab: "t_work", TargetPane: "p_other"}, code: protocol.ErrNoSuchTarget},
256 }257 }
257 258
258 for _, tt := range tests {259 for _, tt := range tests {
@@ -272,7 +273,7 @@ func TestHandleCommandResolvesStaleSessionNameByStableIDs(t *testing.T) {
272 d := newTestDaemon(t, nil, stubClock{})273 d := newTestDaemon(t, nil, stubClock{})
273 sess := addControlSession(d, "renamed", "t_work", "p_work")274 sess := addControlSession(d, "renamed", "t_work", "p_work")
274 275
275- result := sendCommand(t, d, ports.CommandRequest{276+ result := sendCommand(t, d, protocol.CommandRequest{
276 Slug: "toast", Args: []string{"hello"},277 Slug: "toast", Args: []string{"hello"},
277 TargetSession: "old-name", TargetTab: "t_work", TargetPane: "p_work",278 TargetSession: "old-name", TargetTab: "t_work", TargetPane: "p_work",
278 })279 })
@@ -292,7 +293,7 @@ func TestHandleCommandStableIDsResolveSessionWithoutSelectingTab(t *testing.T) {
292 selectTestAttachmentTabLocked(sess, 0)293 selectTestAttachmentTabLocked(sess, 0)
293 sess.mu.Unlock()294 sess.mu.Unlock()
294 295
295- result := sendCommand(t, d, ports.CommandRequest{296+ result := sendCommand(t, d, protocol.CommandRequest{
296 Slug: "rename-tab", Args: []string{"targeted"}, TargetSession: "work",297 Slug: "rename-tab", Args: []string{"targeted"}, TargetSession: "work",
297 TargetTab: "t_second", TargetPane: "p_second",298 TargetTab: "t_second", TargetPane: "p_second",
298 })299 })
@@ -324,7 +325,7 @@ func TestHandleCommandStableIDsDoNotRedirectSplitFromCurrentFocus(t *testing.T)
324 selectTestAttachmentTabLocked(sess, 0)325 selectTestAttachmentTabLocked(sess, 0)
325 sess.mu.Unlock()326 sess.mu.Unlock()
326 327
327- result := sendCommand(t, d, ports.CommandRequest{328+ result := sendCommand(t, d, protocol.CommandRequest{
328 Slug: "split-right", TargetSession: "work",329 Slug: "split-right", TargetSession: "work",
329 TargetTab: "t_origin", TargetPane: "p_origin",330 TargetTab: "t_origin", TargetPane: "p_origin",
330 })331 })
@@ -361,7 +362,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
361 selectTestAttachmentTabLocked(sess, 0)362 selectTestAttachmentTabLocked(sess, 0)
362 sess.mu.Unlock()363 sess.mu.Unlock()
363 364
364- rename := sendCommand(t, d, ports.CommandRequest{365+ rename := sendCommand(t, d, protocol.CommandRequest{
365 Slug: "rename-tab", Args: []string{"invoking"}, Self: true,366 Slug: "rename-tab", Args: []string{"invoking"}, Self: true,
366 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",367 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
367 })368 })
@@ -373,7 +374,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
373 require.Equal(t, "invoking", invoking.name)374 require.Equal(t, "invoking", invoking.name)
374 invoking.mu.Unlock()375 invoking.mu.Unlock()
375 376
376- split := sendCommand(t, d, ports.CommandRequest{377+ split := sendCommand(t, d, protocol.CommandRequest{
377 Slug: "split-right", Self: true,378 Slug: "split-right", Self: true,
378 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",379 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
379 })380 })
@@ -387,7 +388,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
387 beforeGeneration := invoking.layoutGeneration388 beforeGeneration := invoking.layoutGeneration
388 invoking.mu.Unlock()389 invoking.mu.Unlock()
389 390
390- grow := sendCommand(t, d, ports.CommandRequest{391+ grow := sendCommand(t, d, protocol.CommandRequest{
391 Slug: "grow-pane-width", Self: true,392 Slug: "grow-pane-width", Self: true,
392 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",393 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
393 })394 })
@@ -401,7 +402,7 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
401 require.Len(t, active.panes, 1, "--self resize must not mutate the active tab")402 require.Len(t, active.panes, 1, "--self resize must not mutate the active tab")
402 active.mu.Unlock()403 active.mu.Unlock()
403 404
404- equalize := sendCommand(t, d, ports.CommandRequest{405+ equalize := sendCommand(t, d, protocol.CommandRequest{
405 Slug: "equalize-panes", Self: true,406 Slug: "equalize-panes", Self: true,
406 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",407 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
407 })408 })
@@ -417,10 +418,10 @@ func TestHandleCommandSelfTargetsNonActiveTabAndPane(t *testing.T) {
417 func TestResizeControlOneShotsTargetDetachedSessions(t *testing.T) {418 func TestResizeControlOneShotsTargetDetachedSessions(t *testing.T) {
418 targets := []struct {419 targets := []struct {
419 name string420 name string
420- request func() ports.CommandRequest421+ request func() protocol.CommandRequest
421 }{422 }{
422- {"named", func() ports.CommandRequest { return ports.CommandRequest{TargetSession: "work"} }},423+ {"named", func() protocol.CommandRequest { return protocol.CommandRequest{TargetSession: "work"} }},
423- {"unique", func() ports.CommandRequest { return ports.CommandRequest{} }},424+ {"unique", func() protocol.CommandRequest { return protocol.CommandRequest{} }},
424 }425 }
425 for _, target := range targets {426 for _, target := range targets {
426 t.Run(target.name, func(t *testing.T) {427 t.Run(target.name, func(t *testing.T) {
@@ -430,8 +431,8 @@ func TestResizeControlOneShotsTargetDetachedSessions(t *testing.T) {
430 d := newTestDaemon(t, factory, stubClock{})431 d := newTestDaemon(t, factory, stubClock{})
431 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })432 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
432 sess := addControlSession(d, "work", "t_work", "p_work")433 sess := addControlSession(d, "work", "t_work", "p_work")
433- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)434+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
434- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-down", TargetSession: "work"}).OK)435+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-down", TargetSession: "work"}).OK)
435 tb := testAttachmentTab(sess)436 tb := testAttachmentTab(sess)
436 tb.mu.Lock()437 tb.mu.Lock()
437 generation := tb.layoutGeneration438 generation := tb.layoutGeneration
@@ -465,7 +466,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
465 d := newTestDaemon(t, factory, stubClock{})466 d := newTestDaemon(t, factory, stubClock{})
466 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })467 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
467 sess := addControlSession(d, "work", "t_active", "p_active")468 sess := addControlSession(d, "work", "t_active", "p_active")
468- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)469+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
469 active := sess.tabs[0]470 active := sess.tabs[0]
470 locator := newTabWithStableID("t_locator", "p_locator", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})471 locator := newTabWithStableID("t_locator", "p_locator", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
471 locator.ctx, locator.cancel = context.WithCancel(d.serveCtx)472 locator.ctx, locator.cancel = context.WithCancel(d.serveCtx)
@@ -477,7 +478,7 @@ func TestResizeControlNonSelfVEVLocatorUsesActiveTarget(t *testing.T) {
477 before := active.layoutGeneration478 before := active.layoutGeneration
478 active.mu.Unlock()479 active.mu.Unlock()
479 480
480- result := sendCommand(t, d, ports.CommandRequest{481+ result := sendCommand(t, d, protocol.CommandRequest{
481 Slug: "grow-pane-width", TargetSession: "work", TargetTab: "t_locator", TargetPane: "p_locator",482 Slug: "grow-pane-width", TargetSession: "work", TargetTab: "t_locator", TargetPane: "p_locator",
482 })483 })
483 require.True(t, result.OK, result.Text)484 require.True(t, result.OK, result.Text)
@@ -497,16 +498,16 @@ func TestResizeControlTooSmallUsesStableFailure(t *testing.T) {
497 d := newTestDaemon(t, factory, stubClock{})498 d := newTestDaemon(t, factory, stubClock{})
498 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })499 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
499 sess := addControlSession(d, "work", "t_work", "p_work")500 sess := addControlSession(d, "work", "t_work", "p_work")
500- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)501+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
501 tb := testAttachmentTab(sess)502 tb := testAttachmentTab(sess)
502 tb.mu.Lock()503 tb.mu.Lock()
503 tb.size.Cols = 41504 tb.size.Cols = 41
504 tb.bumpLayoutGenerationLocked()505 tb.bumpLayoutGenerationLocked()
505 tb.mu.Unlock()506 tb.mu.Unlock()
506 507
507- result := sendCommand(t, d, ports.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})508+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})
508 require.False(t, result.OK)509 require.False(t, result.OK)
509- require.Equal(t, ports.ErrNoSuchTarget, result.Code)510+ require.Equal(t, protocol.ErrNoSuchTarget, result.Code)
510 require.Equal(t, "pane cannot be resized further", result.Text)511 require.Equal(t, "pane cannot be resized further", result.Text)
511 }512 }
512 513
@@ -520,7 +521,7 @@ func TestHandleCommandSelfListPanesUsesInvokingTab(t *testing.T) {
520 selectTestAttachmentTabLocked(sess, 0)521 selectTestAttachmentTabLocked(sess, 0)
521 sess.mu.Unlock()522 sess.mu.Unlock()
522 523
523- result := sendCommand(t, d, ports.CommandRequest{524+ result := sendCommand(t, d, protocol.CommandRequest{
524 Slug: "list-panes", Self: true, JSON: true,525 Slug: "list-panes", Self: true, JSON: true,
525 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",526 TargetSession: "work", TargetTab: "t_invoking", TargetPane: "p_invoking",
526 })527 })
@@ -533,12 +534,12 @@ func TestHandleCommandRenameSessionRejectsInvalidNameAsCommandArgs(t *testing.T)
533 d := newTestDaemon(t, nil, stubClock{})534 d := newTestDaemon(t, nil, stubClock{})
534 addControlSession(d, "work", "t_work", "p_work")535 addControlSession(d, "work", "t_work", "p_work")
535 536
536- result := sendCommand(t, d, ports.CommandRequest{537+ result := sendCommand(t, d, protocol.CommandRequest{
537 Slug: "rename-session", Args: []string{"invalid name"}, TargetSession: "work",538 Slug: "rename-session", Args: []string{"invalid name"}, TargetSession: "work",
538 })539 })
539 540
540 require.False(t, result.OK)541 require.False(t, result.OK)
541- require.Equal(t, ports.ErrInvalidCommandArgs, result.Code)542+ require.Equal(t, protocol.ErrInvalidCommandArgs, result.Code)
542 }543 }
543 544
544 func TestCloseCommandsReportMutationOutcome(t *testing.T) {545 func TestCloseCommandsReportMutationOutcome(t *testing.T) {
@@ -552,10 +553,10 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
552 result := d.runControl(cmd, controlExec{553 result := d.runControl(cmd, controlExec{
553 d: d, sess: sess, tab: stale,554 d: d, sess: sess, tab: stale,
554 target: daemonActionTarget{session: sess, tab: stale},555 target: daemonActionTarget{session: sess, tab: stale},
555- }, ports.CommandRequest{Slug: "close-tab"})556+ }, protocol.CommandRequest{Slug: "close-tab"})
556 557
557 require.False(t, result.OK)558 require.False(t, result.OK)
558- require.Equal(t, ports.ErrInternal, result.Code)559+ require.Equal(t, protocol.ErrInternal, result.Code)
559 require.Len(t, sess.tabs, 1, "a stale close must retain the live tab")560 require.Len(t, sess.tabs, 1, "a stale close must retain the live tab")
560 })561 })
561 562
@@ -569,10 +570,10 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
569 sess.mu.Unlock()570 sess.mu.Unlock()
570 sess.snapEligible.Store(true)571 sess.snapEligible.Store(true)
571 572
572- result := sendCommand(t, d, ports.CommandRequest{Slug: "close-pane", TargetSession: "work"})573+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "close-pane", TargetSession: "work"})
573 574
574 require.False(t, result.OK)575 require.False(t, result.OK)
575- require.Equal(t, ports.ErrInternal, result.Code)576+ require.Equal(t, protocol.ErrInternal, result.Code)
576 d.mu.Lock()577 d.mu.Lock()
577 require.Same(t, sess, d.sessions[sess.id], "failed final-pane close must retain the session")578 require.Same(t, sess, d.sessions[sess.id], "failed final-pane close must retain the session")
578 d.mu.Unlock()579 d.mu.Unlock()
@@ -591,7 +592,7 @@ func TestCloseCommandsReportMutationOutcome(t *testing.T) {
591 selectTestAttachmentTabLocked(sess, 1)592 selectTestAttachmentTabLocked(sess, 1)
592 sess.mu.Unlock()593 sess.mu.Unlock()
593 594
594- result := sendCommand(t, d, ports.CommandRequest{Slug: "close-tab", TargetSession: "work"})595+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "close-tab", TargetSession: "work"})
595 596
596 require.True(t, result.OK, result.Text)597 require.True(t, result.OK, result.Text)
597 sess.mu.Lock()598 sess.mu.Lock()
@@ -634,7 +635,7 @@ func TestHandleCommandHeadlessMutations(t *testing.T) {
634 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })635 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
635 sess := addControlSession(d, "work", "t_work", "p_work")636 sess := addControlSession(d, "work", "t_work", "p_work")
636 637
637- result := sendCommand(t, d, ports.CommandRequest{Slug: tt.slug, Args: tt.args, TargetSession: "work"})638+ result := sendCommand(t, d, protocol.CommandRequest{Slug: tt.slug, Args: tt.args, TargetSession: "work"})
638 639
639 require.True(t, result.OK, result.Text)640 require.True(t, result.OK, result.Text)
640 tt.verify(t, d, sess)641 tt.verify(t, d, sess)
@@ -653,7 +654,7 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T
653 source.tabs[0].size = domain.Size{Cols: 118, Rows: 38}654 source.tabs[0].size = domain.Size{Cols: 118, Rows: 38}
654 source.mu.Unlock()655 source.mu.Unlock()
655 656
656- result := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})657+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
657 658
658 require.True(t, result.OK, result.Text)659 require.True(t, result.OK, result.Text)
659 d.mu.Lock()660 d.mu.Lock()
@@ -671,9 +672,9 @@ func TestHandleCommandNewSessionInheritsHeadlessIdentityAndViewport(t *testing.T
671 tb.mu.Unlock()672 tb.mu.Unlock()
672 require.Empty(t, source.snapshotAttachments())673 require.Empty(t, source.snapshotAttachments())
673 674
674- taken := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})675+ taken := sendCommand(t, d, protocol.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
675 require.False(t, taken.OK)676 require.False(t, taken.OK)
676- require.Equal(t, ports.ErrNameTaken, taken.Code)677+ require.Equal(t, protocol.ErrNameTaken, taken.Code)
677 }678 }
678 679
679 func TestHandleCommandNewSessionPreservesSourceGeometry(t *testing.T) {680 func TestHandleCommandNewSessionPreservesSourceGeometry(t *testing.T) {
@@ -687,7 +688,7 @@ func TestHandleCommandNewSessionPreservesSourceGeometry(t *testing.T) {
687 })688 })
688 require.True(t, source.registerAttachment(client))689 require.True(t, source.registerAttachment(client))
689 690
690- result := sendCommand(t, d, ports.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})691+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "new-session", Args: []string{"scripted"}, TargetSession: "work"})
691 692
692 require.True(t, result.OK, result.Text)693 require.True(t, result.OK, result.Text)
693 d.mu.Lock()694 d.mu.Lock()
@@ -710,11 +711,11 @@ func TestHandleCommandValidatesToastAndQueuesForDetachedSession(t *testing.T) {
710 d := newTestDaemon(t, nil, stubClock{})711 d := newTestDaemon(t, nil, stubClock{})
711 sess := addControlSession(d, "work", "t_work", "p_work")712 sess := addControlSession(d, "work", "t_work", "p_work")
712 713
713- bad := sendCommand(t, d, ports.CommandRequest{Slug: "toast", Args: []string{"-l", "loud", "hello"}, TargetSession: "work"})714+ bad := sendCommand(t, d, protocol.CommandRequest{Slug: "toast", Args: []string{"-l", "loud", "hello"}, TargetSession: "work"})
714 require.False(t, bad.OK)715 require.False(t, bad.OK)
715- require.Equal(t, ports.ErrInvalidCommandArgs, bad.Code)716+ require.Equal(t, protocol.ErrInvalidCommandArgs, bad.Code)
716 717
717- good := sendCommand(t, d, ports.CommandRequest{Slug: "toast", Args: []string{"-l", "warn", "hello"}, TargetSession: "work"})718+ good := sendCommand(t, d, protocol.CommandRequest{Slug: "toast", Args: []string{"-l", "warn", "hello"}, TargetSession: "work"})
718 require.True(t, good.OK, good.Text)719 require.True(t, good.OK, good.Text)
719 d.notices.mu.Lock()720 d.notices.mu.Lock()
720 require.Len(t, d.notices.pending, 1)721 require.Len(t, d.notices.pending, 1)
@@ -728,16 +729,16 @@ func TestHandleCommandListingsContainStableIDsMarkersAndCWD(t *testing.T) {
728 sess := addControlSession(d, "work", "t_work", "p_work")729 sess := addControlSession(d, "work", "t_work", "p_work")
729 sess.tabs[0].name = "shell"730 sess.tabs[0].name = "shell"
730 731
731- sessions := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})732+ sessions := sendCommand(t, d, protocol.CommandRequest{Slug: "list-sessions"})
732 require.True(t, sessions.OK, sessions.Text)733 require.True(t, sessions.OK, sessions.Text)
733 require.Contains(t, sessions.Output, "NAME\tSTATE\tTABS\tATTACHED\tACTIVE")734 require.Contains(t, sessions.Output, "NAME\tSTATE\tTABS\tATTACHED\tACTIVE")
734 require.Contains(t, sessions.Output, "work\tephemeral\t1\tfalse\ttrue")735 require.Contains(t, sessions.Output, "work\tephemeral\t1\tfalse\ttrue")
735 736
736- tabs := sendCommand(t, d, ports.CommandRequest{Slug: "list-tabs", TargetSession: "work"})737+ tabs := sendCommand(t, d, protocol.CommandRequest{Slug: "list-tabs", TargetSession: "work"})
737 require.True(t, tabs.OK, tabs.Text)738 require.True(t, tabs.OK, tabs.Text)
738 require.Contains(t, tabs.Output, "0\tt_work\tshell\t1\ttrue")739 require.Contains(t, tabs.Output, "0\tt_work\tshell\t1\ttrue")
739 740
740- panes := sendCommand(t, d, ports.CommandRequest{Slug: "list-panes", TargetSession: "work", JSON: true})741+ panes := sendCommand(t, d, protocol.CommandRequest{Slug: "list-panes", TargetSession: "work", JSON: true})
741 require.True(t, panes.OK, panes.Text)742 require.True(t, panes.OK, panes.Text)
742 var decoded []map[string]any743 var decoded []map[string]any
743 require.NoError(t, json.Unmarshal([]byte(panes.Output), &decoded))744 require.NoError(t, json.Unmarshal([]byte(panes.Output), &decoded))
@@ -766,29 +767,29 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
766 build.mruAt.Store(1)767 build.mruAt.Store(1)
767 768
768 d.mu.Lock()769 d.mu.Lock()
769- d.inactive["old"] = inactiveSession{name: "old", cwd: "/tmp/old", createdAt: 1, incarnation: domain.IncarnationID{3}, state: ports.SessionDown}770+ d.inactive["old"] = inactiveSession{name: "old", cwd: "/tmp/old", createdAt: 1, incarnation: domain.IncarnationID{3}, state: protocol.SessionDown}
770 // Live runtime authority wins if both registries contain the same name.771 // Live runtime authority wins if both registries contain the same name.
771- d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 1, incarnation: domain.IncarnationID{4}, state: ports.SessionDown}772+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 1, incarnation: domain.IncarnationID{4}, state: protocol.SessionDown}
772 d.mu.Unlock()773 d.mu.Unlock()
773 774
774- listBefore := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})775+ listBefore := sendCommand(t, d, protocol.CommandRequest{Slug: "list-sessions"})
775 require.True(t, listBefore.OK, listBefore.Text)776 require.True(t, listBefore.OK, listBefore.Text)
776 777
777- missingJSON := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog"})778+ missingJSON := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog"})
778 require.False(t, missingJSON.OK)779 require.False(t, missingJSON.OK)
779- require.Equal(t, ports.ErrInvalidCommandArgs, missingJSON.Code)780+ require.Equal(t, protocol.ErrInvalidCommandArgs, missingJSON.Code)
780 781
781- withArgs := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", Args: []string{"extra"}, JSON: true})782+ withArgs := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", Args: []string{"extra"}, JSON: true})
782 require.False(t, withArgs.OK)783 require.False(t, withArgs.OK)
783- require.Equal(t, ports.ErrInvalidCommandArgs, withArgs.Code)784+ require.Equal(t, protocol.ErrInvalidCommandArgs, withArgs.Code)
784 785
785- result := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", JSON: true})786+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
786 require.True(t, result.OK, result.Text)787 require.True(t, result.OK, result.Text)
787 require.True(t, strings.HasSuffix(result.Output, "\n"), "catalog output must be newline-terminated")788 require.True(t, strings.HasSuffix(result.Output, "\n"), "catalog output must be newline-terminated")
788 789
789 var catalog ports.RemoteCatalog790 var catalog ports.RemoteCatalog
790 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))791 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))
791- require.Equal(t, ports.ProtocolVersion, catalog.ProtocolVersion)792+ require.Equal(t, protocol.Version, catalog.ProtocolVersion)
792 793
793 raw := map[string]json.RawMessage{}794 raw := map[string]json.RawMessage{}
794 require.NoError(t, json.Unmarshal([]byte(result.Output), &raw))795 require.NoError(t, json.Unmarshal([]byte(result.Output), &raw))
@@ -806,7 +807,7 @@ func TestRemoteCatalogJSONOutput(t *testing.T) {
806 807
807 require.Equal(t, ports.RemoteCatalogSessionDown, catalog.Sessions[1].State)808 require.Equal(t, ports.RemoteCatalogSessionDown, catalog.Sessions[1].State)
808 809
809- listAfter := sendCommand(t, d, ports.CommandRequest{Slug: "list-sessions"})810+ listAfter := sendCommand(t, d, protocol.CommandRequest{Slug: "list-sessions"})
810 require.True(t, listAfter.OK, listAfter.Text)811 require.True(t, listAfter.OK, listAfter.Text)
811 require.Equal(t, listBefore.Output, listAfter.Output)812 require.Equal(t, listBefore.Output, listAfter.Output)
812 require.Contains(t, listAfter.Output, "work\tnamed\t2\ttrue\t")813 require.Contains(t, listAfter.Output, "work\tnamed\t2\ttrue\t")
@@ -828,7 +829,7 @@ func TestRemoteCatalogRefreshesFocusedTabTitle(t *testing.T) {
828 return "fish", nil829 return "fish", nil
829 }830 }
830 831
831- result := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", JSON: true})832+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
832 require.True(t, result.OK, result.Text)833 require.True(t, result.OK, result.Text)
833 var catalog ports.RemoteCatalog834 var catalog ports.RemoteCatalog
834 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))835 require.NoError(t, json.Unmarshal([]byte(result.Output), &catalog))
@@ -860,7 +861,7 @@ func TestRemoteCatalogRejectsTooManyTabs(t *testing.T) {
860 }861 }
861 sess.mu.Unlock()862 sess.mu.Unlock()
862 863
863- result := sendCommand(t, d, ports.CommandRequest{Slug: "remote-catalog", JSON: true})864+ result := sendCommand(t, d, protocol.CommandRequest{Slug: "remote-catalog", JSON: true})
864 require.False(t, result.OK)865 require.False(t, result.OK)
865 require.Contains(t, result.Text, "too many tabs")866 require.Contains(t, result.Text, "too many tabs")
866 }867 }
@@ -878,7 +879,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
878 sess.mu.Unlock()879 sess.mu.Unlock()
879 original := target.tree.Focus880 original := target.tree.Focus
880 881
881- firstFrame := commandFrame(t, ports.CommandRequest{882+ firstFrame := commandFrame(t, protocol.CommandRequest{
882 Slug: "split-right", Self: true, TargetSession: "work",883 Slug: "split-right", Self: true, TargetSession: "work",
883 TargetTab: "t_invoking", TargetPane: "p_invoking",884 TargetTab: "t_invoking", TargetPane: "p_invoking",
884 })885 })
@@ -896,7 +897,7 @@ func TestHandleCommandSerializesSelfTargetOnNonActiveTab(t *testing.T) {
896 t.Fatal("first command did not enter its blocked action")897 t.Fatal("first command did not enter its blocked action")
897 }898 }
898 899
899- secondFrame := commandFrame(t, ports.CommandRequest{900+ secondFrame := commandFrame(t, protocol.CommandRequest{
900 Slug: "focus-pane-right", Self: true, TargetSession: "work",901 Slug: "focus-pane-right", Self: true, TargetSession: "work",
901 TargetTab: "t_invoking", TargetPane: "p_invoking",902 TargetTab: "t_invoking", TargetPane: "p_invoking",
902 })903 })
@@ -961,7 +962,7 @@ func TestHandleCommandMovePaneUsesActiveFocusedSourceWithSessionFlag(t *testing.
961 source.mu.Unlock()962 source.mu.Unlock()
962 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")963 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
963 964
964- result := sendCommand(t, d, ports.CommandRequest{965+ result := sendCommand(t, d, protocol.CommandRequest{
965 Slug: "move-pane", Args: []string{"dest", "t_dest"}, TargetSession: "work",966 Slug: "move-pane", Args: []string{"dest", "t_dest"}, TargetSession: "work",
966 })967 })
967 require.True(t, result.OK, result.Text)968 require.True(t, result.OK, result.Text)
@@ -996,7 +997,7 @@ func TestHandleCommandMovePaneSelfUsesStableSourceIDs(t *testing.T) {
996 source.mu.Unlock()997 source.mu.Unlock()
997 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")998 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
998 999
999- result := sendCommand(t, d, ports.CommandRequest{1000+ result := sendCommand(t, d, protocol.CommandRequest{
1000 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,1001 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,
1001 TargetSession: "work", TargetTab: "t_inactive", TargetPane: "p_inactive",1002 TargetSession: "work", TargetTab: "t_inactive", TargetPane: "p_inactive",
1002 })1003 })
@@ -1034,7 +1035,7 @@ func TestHandleCommandMoveTabUsesActiveTabWithoutSelf(t *testing.T) {
1034 source.mu.Unlock()1035 source.mu.Unlock()
1035 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1036 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1036 1037
1037- result := sendCommand(t, d, ports.CommandRequest{1038+ result := sendCommand(t, d, protocol.CommandRequest{
1038 Slug: "move-tab", Args: []string{"dest"}, TargetSession: "work", Self: false,1039 Slug: "move-tab", Args: []string{"dest"}, TargetSession: "work", Self: false,
1039 TargetTab: "t_second", TargetPane: "p_second",1040 TargetTab: "t_second", TargetPane: "p_second",
1040 })1041 })
@@ -1062,7 +1063,7 @@ func TestHandleCommandMoveTabSelfUsesStableTab(t *testing.T) {
1062 source.mu.Unlock()1063 source.mu.Unlock()
1063 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1064 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1064 1065
1065- result := sendCommand(t, d, ports.CommandRequest{1066+ result := sendCommand(t, d, protocol.CommandRequest{
1066 Slug: "move-tab", Args: []string{"dest"}, Self: true,1067 Slug: "move-tab", Args: []string{"dest"}, Self: true,
1067 TargetSession: "work", TargetTab: "t_second", TargetPane: "p_second",1068 TargetSession: "work", TargetTab: "t_second", TargetPane: "p_second",
1068 })1069 })
@@ -1081,101 +1082,101 @@ func TestHandleCommandMoveTabSelfUsesStableTab(t *testing.T) {
1081 func TestHandleCommandMoveCommandsRejectInvalidArgsAndTargets(t *testing.T) {1082 func TestHandleCommandMoveCommandsRejectInvalidArgsAndTargets(t *testing.T) {
1082 tests := []struct {1083 tests := []struct {
1083 name string1084 name string
1084- arrange func(*Daemon) ports.CommandRequest1085+ arrange func(*Daemon) protocol.CommandRequest
1085 code uint161086 code uint16
1086 text string1087 text string
1087 }{1088 }{
1088 {1089 {
1089 name: "move-pane missing destination session",1090 name: "move-pane missing destination session",
1090- arrange: func(d *Daemon) ports.CommandRequest {1091+ arrange: func(d *Daemon) protocol.CommandRequest {
1091 addControlSession(d, "work", "t_work", "p_work")1092 addControlSession(d, "work", "t_work", "p_work")
1092- return ports.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}1093+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}
1093 },1094 },
1094- code: ports.ErrNoSuchTarget,1095+ code: protocol.ErrNoSuchTarget,
1095 text: "move request is invalid",1096 text: "move request is invalid",
1096 },1097 },
1097 {1098 {
1098 name: "move-pane unknown destination session",1099 name: "move-pane unknown destination session",
1099- arrange: func(d *Daemon) ports.CommandRequest {1100+ arrange: func(d *Daemon) protocol.CommandRequest {
1100 addControlSession(d, "work", "t_work", "p_work")1101 addControlSession(d, "work", "t_work", "p_work")
1101- return ports.CommandRequest{Slug: "move-pane", Args: []string{"missing", "t_dest"}, TargetSession: "work"}1102+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"missing", "t_dest"}, TargetSession: "work"}
1102 },1103 },
1103- code: ports.ErrNoSuchTarget,1104+ code: protocol.ErrNoSuchTarget,
1104 text: "move target is no longer available",1105 text: "move target is no longer available",
1105 },1106 },
1106 {1107 {
1107 name: "move-pane destination tab not in session",1108 name: "move-pane destination tab not in session",
1108- arrange: func(d *Daemon) ports.CommandRequest {1109+ arrange: func(d *Daemon) protocol.CommandRequest {
1109 addControlSession(d, "work", "t_work", "p_work")1110 addControlSession(d, "work", "t_work", "p_work")
1110 addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1111 addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1111- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_other"}, TargetSession: "work"}1112+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_other"}, TargetSession: "work"}
1112 },1113 },
1113- code: ports.ErrNoSuchTarget,1114+ code: protocol.ErrNoSuchTarget,
1114 text: "move target is no longer available",1115 text: "move target is no longer available",
1115 },1116 },
1116 {1117 {
1117 name: "move-pane same source tab",1118 name: "move-pane same source tab",
1118- arrange: func(d *Daemon) ports.CommandRequest {1119+ arrange: func(d *Daemon) protocol.CommandRequest {
1119 addControlSession(d, "work", "t_work", "p_work")1120 addControlSession(d, "work", "t_work", "p_work")
1120- return ports.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}1121+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"work", "t_work"}, TargetSession: "work"}
1121 },1122 },
1122- code: ports.ErrNoSuchTarget,1123+ code: protocol.ErrNoSuchTarget,
1123 text: "move request is invalid",1124 text: "move request is invalid",
1124 },1125 },
1125 {1126 {
1126 name: "move-tab same session",1127 name: "move-tab same session",
1127- arrange: func(d *Daemon) ports.CommandRequest {1128+ arrange: func(d *Daemon) protocol.CommandRequest {
1128 addControlSession(d, "work", "t_work", "p_work")1129 addControlSession(d, "work", "t_work", "p_work")
1129- return ports.CommandRequest{Slug: "move-tab", Args: []string{"work"}, TargetSession: "work"}1130+ return protocol.CommandRequest{Slug: "move-tab", Args: []string{"work"}, TargetSession: "work"}
1130 },1131 },
1131- code: ports.ErrNoSuchTarget,1132+ code: protocol.ErrNoSuchTarget,
1132 text: "move request is invalid",1133 text: "move request is invalid",
1133 },1134 },
1134 {1135 {
1135 name: "move-tab stopping destination",1136 name: "move-tab stopping destination",
1136- arrange: func(d *Daemon) ports.CommandRequest {1137+ arrange: func(d *Daemon) protocol.CommandRequest {
1137 addControlSession(d, "work", "t_work", "p_work")1138 addControlSession(d, "work", "t_work", "p_work")
1138 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1139 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1139 destination.teardownMu.Lock()1140 destination.teardownMu.Lock()
1140 destination.teardownActive = true1141 destination.teardownActive = true
1141 destination.teardownMu.Unlock()1142 destination.teardownMu.Unlock()
1142- return ports.CommandRequest{Slug: "move-tab", Args: []string{"dest"}, TargetSession: "work"}1143+ return protocol.CommandRequest{Slug: "move-tab", Args: []string{"dest"}, TargetSession: "work"}
1143 },1144 },
1144- code: ports.ErrNoSuchTarget,1145+ code: protocol.ErrNoSuchTarget,
1145 text: "move target is no longer available",1146 text: "move target is no longer available",
1146 },1147 },
1147 {1148 {
1148 name: "move-pane too few args",1149 name: "move-pane too few args",
1149- arrange: func(d *Daemon) ports.CommandRequest {1150+ arrange: func(d *Daemon) protocol.CommandRequest {
1150 addControlSession(d, "work", "t_work", "p_work")1151 addControlSession(d, "work", "t_work", "p_work")
1151- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest"}, TargetSession: "work"}1152+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest"}, TargetSession: "work"}
1152 },1153 },
1153- code: ports.ErrInvalidCommandArgs,1154+ code: protocol.ErrInvalidCommandArgs,
1154 },1155 },
1155 {1156 {
1156 name: "move-pane too many args",1157 name: "move-pane too many args",
1157- arrange: func(d *Daemon) ports.CommandRequest {1158+ arrange: func(d *Daemon) protocol.CommandRequest {
1158 addControlSession(d, "work", "t_work", "p_work")1159 addControlSession(d, "work", "t_work", "p_work")
1159- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest", "extra"}, TargetSession: "work"}1160+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest", "extra"}, TargetSession: "work"}
1160 },1161 },
1161- code: ports.ErrInvalidCommandArgs,1162+ code: protocol.ErrInvalidCommandArgs,
1162 },1163 },
1163 {1164 {
1164 name: "move-tab missing destination",1165 name: "move-tab missing destination",
1165- arrange: func(d *Daemon) ports.CommandRequest {1166+ arrange: func(d *Daemon) protocol.CommandRequest {
1166 addControlSession(d, "work", "t_work", "p_work")1167 addControlSession(d, "work", "t_work", "p_work")
1167- return ports.CommandRequest{Slug: "move-tab", TargetSession: "work"}1168+ return protocol.CommandRequest{Slug: "move-tab", TargetSession: "work"}
1168 },1169 },
1169- code: ports.ErrInvalidCommandArgs,1170+ code: protocol.ErrInvalidCommandArgs,
1170 },1171 },
1171 {1172 {
1172 name: "explicit session name without IDs remains authoritative",1173 name: "explicit session name without IDs remains authoritative",
1173- arrange: func(d *Daemon) ports.CommandRequest {1174+ arrange: func(d *Daemon) protocol.CommandRequest {
1174 addControlSession(d, "renamed", "t_work", "p_work")1175 addControlSession(d, "renamed", "t_work", "p_work")
1175 addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1176 addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1176- return ports.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest"}, TargetSession: "old-name"}1177+ return protocol.CommandRequest{Slug: "move-pane", Args: []string{"dest", "t_dest"}, TargetSession: "old-name"}
1177 },1178 },
1178- code: ports.ErrNoSuchTarget,1179+ code: protocol.ErrNoSuchTarget,
1179 text: "no such session: old-name",1180 text: "no such session: old-name",
1180 },1181 },
1181 }1182 }
@@ -1200,7 +1201,7 @@ func TestHandleCommandMovePaneRelocatedStableIDsOverrideAdvisorySessionName(t *t
1200 source := addControlSession(d, "renamed", "t_work", "p_work")1201 source := addControlSession(d, "renamed", "t_work", "p_work")
1201 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1202 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1202 1203
1203- result := sendCommand(t, d, ports.CommandRequest{1204+ result := sendCommand(t, d, protocol.CommandRequest{
1204 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,1205 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,
1205 TargetSession: "old-name", TargetTab: "t_work", TargetPane: "p_work",1206 TargetSession: "old-name", TargetTab: "t_work", TargetPane: "p_work",
1206 })1207 })
@@ -1232,7 +1233,7 @@ func TestHandleCommandMovePaneStableIDsLocateSessionWithoutSelfRedirect(t *testi
1232 source.mu.Unlock()1233 source.mu.Unlock()
1233 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")1234 destination := addNamedMoveDestination(d, "dest", "t_dest", "p_dest")
1234 1235
1235- result := sendCommand(t, d, ports.CommandRequest{1236+ result := sendCommand(t, d, protocol.CommandRequest{
1236 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,1237 Slug: "move-pane", Args: []string{"dest", "t_dest"}, Self: true,
1237 TargetSession: "old-name", TargetTab: "t_inactive", TargetPane: "p_inactive",1238 TargetSession: "old-name", TargetTab: "t_inactive", TargetPane: "p_inactive",
1238 })1239 })
@@ -1273,10 +1274,10 @@ func TestHandleCommandOppositeMoveCommandsDoNotDeadlock(t *testing.T) {
1273 selectTestAttachmentTabLocked(right, 1)1274 selectTestAttachmentTabLocked(right, 1)
1274 right.mu.Unlock()1275 right.mu.Unlock()
1275 1276
1276- leftFrame := commandFrame(t, ports.CommandRequest{1277+ leftFrame := commandFrame(t, protocol.CommandRequest{
1277 Slug: "move-tab", Args: []string{"right"}, TargetSession: "left",1278 Slug: "move-tab", Args: []string{"right"}, TargetSession: "left",
1278 })1279 })
1279- rightFrame := commandFrame(t, ports.CommandRequest{1280+ rightFrame := commandFrame(t, protocol.CommandRequest{
1280 Slug: "move-tab", Args: []string{"left"}, TargetSession: "right",1281 Slug: "move-tab", Args: []string{"left"}, TargetSession: "right",
1281 })1282 })
1282 leftTransport, leftSends, _ := newConn(t, leftFrame)1283 leftTransport, leftSends, _ := newConn(t, leftFrame)
@@ -1323,17 +1324,17 @@ func addNamedMoveDestination(d *Daemon, name, tabID, paneID string) *session {
1323 return sess1324 return sess
1324 }1325 }
1325 1326
1326-func commandFrame(t *testing.T, request ports.CommandRequest) ports.Frame {1327+func commandFrame(t *testing.T, request protocol.CommandRequest) ports.Frame {
1327 t.Helper()1328 t.Helper()
1328 if request.Version == 0 {1329 if request.Version == 0 {
1329- request.Version = ports.ProtocolVersion1330+ request.Version = protocol.Version
1330 }1331 }
1331 payload, err := ports.MarshalCommandRequest(request)1332 payload, err := ports.MarshalCommandRequest(request)
1332 require.NoError(t, err)1333 require.NoError(t, err)
1333 return ports.Frame{Type: ports.MsgCommand, Payload: payload}1334 return ports.Frame{Type: ports.MsgCommand, Payload: payload}
1334 }1335 }
1335 1336
1336-func sendCommand(t *testing.T, d *Daemon, request ports.CommandRequest) ports.CommandResult {1337+func sendCommand(t *testing.T, d *Daemon, request protocol.CommandRequest) protocol.CommandResult {
1337 t.Helper()1338 t.Helper()
1338 frame := commandFrame(t, request)1339 frame := commandFrame(t, request)
1339 tr, sends, _ := newConn(t, frame)1340 tr, sends, _ := newConn(t, frame)
@@ -1341,7 +1342,7 @@ func sendCommand(t *testing.T, d *Daemon, request ports.CommandRequest) ports.Co
1341 return awaitCommandResult(t, sends)1342 return awaitCommandResult(t, sends)
1342 }1343 }
1343 1344
1344-func awaitCommandResult(t *testing.T, sends chan ports.Frame) ports.CommandResult {1345+func awaitCommandResult(t *testing.T, sends chan ports.Frame) protocol.CommandResult {
1345 t.Helper()1346 t.Helper()
1346 reply := awaitFrame(t, sends, ports.MsgCommandResult)1347 reply := awaitFrame(t, sends, ports.MsgCommandResult)
1347 result, err := ports.UnmarshalCommandResult(reply.Payload)1348 result, err := ports.UnmarshalCommandResult(reply.Payload)
M internal/usecase/daemon/copymode_test.go +3 -2
@@ -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 "github.com/bnema/vev/internal/usecase/layout"23 "github.com/bnema/vev/internal/usecase/layout"
23 "github.com/bnema/vev/internal/usecase/ui"24 "github.com/bnema/vev/internal/usecase/ui"
@@ -416,7 +417,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
416 // relying on wall-clock debounce delivery.417 // relying on wall-clock debounce delivery.
417 clk := newCoordinatorMockClock(t, 16)418 clk := newCoordinatorMockClock(t, 16)
418 d := newTestDaemon(t, newFactory(t, p), clk.clock)419 d := newTestDaemon(t, newFactory(t, p), clk.clock)
419- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 16, Rows: 5}))420+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 16, Rows: 5}))
420 advanceRender := func() {421 advanceRender := func() {
421 deadline := time.NewTimer(2 * time.Second)422 deadline := time.NewTimer(2 * time.Second)
422 defer deadline.Stop()423 defer deadline.Stop()
@@ -483,7 +484,7 @@ func TestScrollbackEvictionFeedsCopyModeYank(t *testing.T) {
483 releaseConn()484 releaseConn()
484 closeOnce.Do(func() { close(readDone) })485 closeOnce.Do(func() { close(readDone) })
485 hg.Wait()486 hg.Wait()
486- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))487+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
487 d.sessWg.Wait()488 d.sessWg.Wait()
488 }489 }
489 490
M internal/usecase/daemon/daemon.go +59 -59
@@ -374,7 +374,7 @@ type inactiveSession struct {
374 tabRecords []domain.CatalogueTabRecord374 tabRecords []domain.CatalogueTabRecord
375 purging bool375 purging bool
376 record domain.CatalogueRecord376 record domain.CatalogueRecord
377- state ports.SessionState377+ state protocol.SessionState
378 restoreDone chan struct{}378 restoreDone chan struct{}
379 }379 }
380 380
@@ -391,7 +391,7 @@ func (s inactiveSession) restorePending() bool {
391 }391 }
392 392
393 func (s inactiveSession) broken() bool {393 func (s inactiveSession) broken() bool {
394- return s.state == ports.SessionBroken || s.record.DegradedReason != ""394+ return s.state == protocol.SessionBroken || s.record.DegradedReason != ""
395 }395 }
396 396
397 func (s inactiveSession) visible() bool {397 func (s inactiveSession) visible() bool {
@@ -399,7 +399,7 @@ func (s inactiveSession) visible() bool {
399 }399 }
400 400
401 func (s inactiveSession) canResume() bool {401 func (s inactiveSession) canResume() bool {
402- return s.visible() && s.state == ports.SessionDown && !s.broken()402+ return s.visible() && s.state == protocol.SessionDown && !s.broken()
403 }403 }
404 404
405 func (s inactiveSession) sameLifecycle(other inactiveSession) bool {405 func (s inactiveSession) sameLifecycle(other inactiveSession) bool {
@@ -717,7 +717,7 @@ func New(ptys ports.PTYFactory, clock ports.Clock, log *slog.Logger, opts ...Opt
717 state, done := initialSessionState(r)717 state, done := initialSessionState(r)
718 d.inactive[r.Name] = inactiveSessionFromRecord(r, state, done)718 d.inactive[r.Name] = inactiveSessionFromRecord(r, state, done)
719 } else {719 } else {
720- d.inactive[r.Name] = inactiveSessionFromRecord(r, ports.SessionDown, nil)720+ d.inactive[r.Name] = inactiveSessionFromRecord(r, protocol.SessionDown, nil)
721 }721 }
722 if !hasCreatedAt || r.CreatedAt > maxCreatedAt {722 if !hasCreatedAt || r.CreatedAt > maxCreatedAt {
723 maxCreatedAt = r.CreatedAt723 maxCreatedAt = r.CreatedAt
@@ -806,7 +806,7 @@ func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error {
806 snapshotDeadline = newSnapshotShutdownDeadline(d.clock)806 snapshotDeadline = newSnapshotShutdownDeadline(d.clock)
807 defer snapshotDeadline.stop()807 defer snapshotDeadline.stop()
808 }808 }
809- d.shutdownAllWithSnapshotDeadline(ports.ReasonServerShutdown, snapshotDeadline)809+ d.shutdownAllWithSnapshotDeadline(protocol.ReasonServerShutdown, snapshotDeadline)
810 d.waitNotifies()810 d.waitNotifies()
811 d.hardCancel()811 d.hardCancel()
812 d.serveCancel()812 d.serveCancel()
@@ -826,7 +826,7 @@ func (d *Daemon) Serve(ctx context.Context, l ports.Listener) error {
826 d.persistShutdownSnapshotFailure(name, context.DeadlineExceeded)826 d.persistShutdownSnapshotFailure(name, context.DeadlineExceeded)
827 }827 }
828 d.WaitDurableWriters()828 d.WaitDurableWriters()
829- d.shutdownAllWithSnapshotDeadline(ports.ReasonServerShutdown, snapshotDeadline)829+ d.shutdownAllWithSnapshotDeadline(protocol.ReasonServerShutdown, snapshotDeadline)
830 d.waitSessionWorkersWithSnapshotDeadline(snapshotDeadline)830 d.waitSessionWorkersWithSnapshotDeadline(snapshotDeadline)
831 d.waitNotifies()831 d.waitNotifies()
832 if err := d.flushCatalogue(); err != nil {832 if err := d.flushCatalogue(); err != nil {
@@ -1058,7 +1058,7 @@ func (d *Daemon) handleConn(tr ports.Transport) {
1058 default:1058 default:
1059 d.log.Warn("hello rejected", "err", "expected hello", "type", first.Type)1059 d.log.Warn("hello rejected", "err", "expected hello", "type", first.Type)
1060 _ = boundedHandshakeOperation(handshakeCtx, tr, func() error {1060 _ = boundedHandshakeOperation(handshakeCtx, tr, func() error {
1061- return tr.Send(frameError(ports.ErrInternal, "expected hello"))1061+ return tr.Send(frameError(protocol.ErrInternal, "expected hello"))
1062 })1062 })
1063 stopTransport()1063 stopTransport()
1064 finishHandshake()1064 finishHandshake()
@@ -1072,17 +1072,17 @@ func (d *Daemon) handleList(tr ports.Transport) {
1072 defer func() { _ = tr.Close() }()1072 defer func() { _ = tr.Close() }()
1073 1073
1074 d.mu.Lock()1074 d.mu.Lock()
1075- infos := make([]ports.SessionInfo, 0, len(d.sessions)+len(d.inactive))1075+ infos := make([]protocol.SessionInfo, 0, len(d.sessions)+len(d.inactive))
1076 liveNames := make(map[string]struct{}, len(d.sessions))1076 liveNames := make(map[string]struct{}, len(d.sessions))
1077 for _, s := range d.sessions {1077 for _, s := range d.sessions {
1078 if s == nil {1078 if s == nil {
1079 continue1079 continue
1080 }1080 }
1081 s.mu.Lock()1081 s.mu.Lock()
1082- info := ports.SessionInfo{1082+ info := protocol.SessionInfo{
1083 SessionID: string(s.id),1083 SessionID: string(s.id),
1084 Name: s.name,1084 Name: s.name,
1085- State: ports.SessionUp,1085+ State: protocol.SessionUp,
1086 Ephemeral: s.ephemeral,1086 Ephemeral: s.ephemeral,
1087 Tabs: uint16(len(s.tabs)),1087 Tabs: uint16(len(s.tabs)),
1088 Attached: len(s.attachments) != 0,1088 Attached: len(s.attachments) != 0,
@@ -1095,11 +1095,11 @@ func (d *Daemon) handleList(tr ports.Transport) {
1095 if _, live := liveNames[name]; live || !inactive.visible() {1095 if _, live := liveNames[name]; live || !inactive.visible() {
1096 continue1096 continue
1097 }1097 }
1098- state := ports.SessionDown1098+ state := protocol.SessionDown
1099 if inactive.broken() {1099 if inactive.broken() {
1100- state = ports.SessionBroken1100+ state = protocol.SessionBroken
1101 }1101 }
1102- infos = append(infos, ports.SessionInfo{Name: name, State: state})1102+ infos = append(infos, protocol.SessionInfo{Name: name, State: state})
1103 }1103 }
1104 d.mu.Unlock()1104 d.mu.Unlock()
1105 1105
@@ -1115,11 +1115,11 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) {
1115 1115
1116 k, err := ports.UnmarshalKill(f.Payload)1116 k, err := ports.UnmarshalKill(f.Payload)
1117 if err != nil {1117 if err != nil {
1118- _ = d.boundedControlSend(tr, frameError(ports.ErrInternal, "malformed kill request"))1118+ _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "malformed kill request"))
1119 return1119 return
1120 }1120 }
1121 if k.All {1121 if k.All {
1122- d.shutdownAll(ports.ReasonServerShutdown)1122+ d.shutdownAll(protocol.ReasonServerShutdown)
1123 return1123 return
1124 }1124 }
1125 1125
@@ -1132,7 +1132,7 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) {
1132 // deletion order as live and offline purges.1132 // deletion order as live and offline purges.
1133 if err := d.retryStoppedPurge(k.Name); err != nil {1133 if err := d.retryStoppedPurge(k.Name); err != nil {
1134 d.log.Warn("deleting stopped session failed", "err", err, "session", k.Name)1134 d.log.Warn("deleting stopped session failed", "err", err, "session", k.Name)
1135- _ = d.boundedControlSend(tr, frameError(ports.ErrInternal, "deleting stopped session failed"))1135+ _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting stopped session failed"))
1136 }1136 }
1137 return1137 return
1138 }1138 }
@@ -1140,11 +1140,11 @@ func (d *Daemon) handleKill(tr ports.Transport, f ports.Frame) {
1140 d.mu.Unlock()1140 d.mu.Unlock()
1141 1141
1142 if target == nil {1142 if target == nil {
1143- _ = d.boundedControlSend(tr, frameError(ports.ErrNoSuchSession, "no such session: "+k.Name))1143+ _ = d.boundedControlSend(tr, frameError(protocol.ErrNoSuchSession, "no such session: "+k.Name))
1144 return1144 return
1145 }1145 }
1146- if err := d.killSession(target, ports.ReasonSessionKilled, true); err != nil {1146+ if err := d.killSession(target, protocol.ReasonSessionKilled, true); err != nil {
1147- _ = d.boundedControlSend(tr, frameError(ports.ErrInternal, "deleting persisted session failed"))1147+ _ = d.boundedControlSend(tr, frameError(protocol.ErrInternal, "deleting persisted session failed"))
1148 }1148 }
1149 }1149 }
1150 1150
@@ -1175,19 +1175,19 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
1175 }1175 }
1176 h, err := ports.UnmarshalHello(f.Payload)1176 h, err := ports.UnmarshalHello(f.Payload)
1177 if err != nil {1177 if err != nil {
1178- if version, ok := ports.PeekHelloVersion(f.Payload); ok && version != ports.ProtocolVersion {1178+ if version, ok := ports.PeekHelloVersion(f.Payload); ok && version != protocol.Version {
1179- d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", version, "expected", ports.ProtocolVersion)1179+ d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", version, "expected", protocol.Version)
1180- _ = sendHandshake(frameError(ports.ErrVersionMismatch, "protocol version mismatch"))1180+ _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1181 } else {1181 } else {
1182 d.log.Warn("hello rejected", "err", err)1182 d.log.Warn("hello rejected", "err", err)
1183- _ = sendHandshake(frameError(ports.ErrInternal, "malformed hello"))1183+ _ = sendHandshake(frameError(protocol.ErrInternal, "malformed hello"))
1184 }1184 }
1185 _ = tr.Close()1185 _ = tr.Close()
1186 return1186 return
1187 }1187 }
1188- if h.Version != ports.ProtocolVersion {1188+ if h.Version != protocol.Version {
1189- d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", h.Version, "expected", ports.ProtocolVersion, "intent", h.Intent, "session", h.Name)1189+ d.log.Warn("hello rejected", "err", "protocol version mismatch", "version", h.Version, "expected", protocol.Version, "intent", h.Intent, "session", h.Name)
1190- _ = sendHandshake(frameError(ports.ErrVersionMismatch, "protocol version mismatch"))1190+ _ = sendHandshake(frameError(protocol.ErrVersionMismatch, "protocol version mismatch"))
1191 _ = tr.Close()1191 _ = tr.Close()
1192 return1192 return
1193 }1193 }
@@ -1198,7 +1198,7 @@ func (d *Daemon) handleHelloWithContext(handshakeCtx context.Context, timedOut <
1198 if pe, ok := errors.AsType[*protoErr](rerr); ok {1198 if pe, ok := errors.AsType[*protoErr](rerr); ok {
1199 _ = sendHandshake(frameError(pe.code, pe.text))1199 _ = sendHandshake(frameError(pe.code, pe.text))
1200 } else {1200 } else {
1201- _ = sendHandshake(frameError(ports.ErrInternal, rerr.Error()))1201+ _ = sendHandshake(frameError(protocol.ErrInternal, rerr.Error()))
1202 }1202 }
1203 _ = tr.Close()1203 _ = tr.Close()
1204 return1204 return
@@ -1310,7 +1310,7 @@ func (e *protoErr) Error() string { return e.text }
1310 // unlocking d.mu before returning, including every error path. It publishes1310 // unlocking d.mu before returning, including every error path. It publishes
1311 // terminal and role state before releasing d.mu, then defers coordinator1311 // terminal and role state before releasing d.mu, then defers coordinator
1312 // cleanup so obsolete workers never delay the new handshake.1312 // cleanup so obsolete workers never delay the new handshake.
1313-func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello) (*attachedClient, error) {1313+func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello) (*attachedClient, error) {
1314 initialTabIndex := -11314 initialTabIndex := -1
1315 if h.RemoteTarget != nil {1315 if h.RemoteTarget != nil {
1316 var ok bool1316 var ok bool
@@ -1320,7 +1320,7 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size,
1320 _ = tr.Close()1320 _ = tr.Close()
1321 }1321 }
1322 d.mu.Unlock()1322 d.mu.Unlock()
1323- return nil, &protoErr{ports.ErrNoSuchTarget, "remote tab no longer exists"}1323+ return nil, &protoErr{protocol.ErrNoSuchTarget, "remote tab no longer exists"}
1324 }1324 }
1325 }1325 }
1326 if h.PreferredTabID != "" {1326 if h.PreferredTabID != "" {
@@ -1334,7 +1334,7 @@ func (d *Daemon) finishAttach(sess *session, tr ports.Transport, sz domain.Size,
1334 // A picker handoff deliberately leaves the daemon-owned environment and CWD1334 // A picker handoff deliberately leaves the daemon-owned environment and CWD
1335 // untouched, even though Hello retains those fields for direct CLI clients.1335 // untouched, even though Hello retains those fields for direct CLI clients.
1336 sess.mu.Lock()1336 sess.mu.Lock()
1337- if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {1337+ if h.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
1338 sess.env = copyEnvironment(h.Env)1338 sess.env = copyEnvironment(h.Env)
1339 }1339 }
1340 sess.mu.Unlock()1340 sess.mu.Unlock()
@@ -1439,12 +1439,12 @@ func (d *Daemon) waitForTargetRestore(ctx context.Context, name string) error {
1439 return nil1439 return nil
1440 }1440 }
1441 if stopped.broken() {1441 if stopped.broken() {
1442- return &protoErr{ports.ErrInternal, "session durable state is broken: " + name}1442+ return &protoErr{protocol.ErrInternal, "session durable state is broken: " + name}
1443 }1443 }
1444 if stopped.record.Committed == nil {1444 if stopped.record.Committed == nil {
1445 return nil1445 return nil
1446 }1446 }
1447- return &protoErr{ports.ErrInternal, "session was not restored into this daemon: " + name}1447+ return &protoErr{protocol.ErrInternal, "session was not restored into this daemon: " + name}
1448 }1448 }
1449 1449
1450 // 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
@@ -1453,20 +1453,20 @@ func (d *Daemon) waitForTargetRestore(ctx context.Context, name string) error {
1453 func (d *Daemon) validateExactSessionTargetLocked(target protocol.ExactSessionTarget) error {1453 func (d *Daemon) validateExactSessionTargetLocked(target protocol.ExactSessionTarget) error {
1454 if sess := d.findByNameLocked(target.SessionName); sess != nil {1454 if sess := d.findByNameLocked(target.SessionName); sess != nil {
1455 if sess.incarnation != target.LifecycleID {1455 if sess.incarnation != target.LifecycleID {
1456- return &protoErr{ports.ErrNoSuchSession, "session lifecycle has changed: " + target.SessionName}1456+ return &protoErr{protocol.ErrNoSuchSession, "session lifecycle has changed: " + target.SessionName}
1457 }1457 }
1458 return nil1458 return nil
1459 }1459 }
1460 stopped, ok := d.inactive[target.SessionName]1460 stopped, ok := d.inactive[target.SessionName]
1461 if !ok || !stopped.visible() || stopped.incarnation != target.LifecycleID {1461 if !ok || !stopped.visible() || stopped.incarnation != target.LifecycleID {
1462- return &protoErr{ports.ErrNoSuchSession, "no such session lifecycle: " + target.SessionName}1462+ return &protoErr{protocol.ErrNoSuchSession, "no such session lifecycle: " + target.SessionName}
1463 }1463 }
1464 return nil1464 return nil
1465 }1465 }
1466 1466
1467 func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol.ExactSessionTarget) error {1467 func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol.ExactSessionTarget) error {
1468 if err := target.Validate(); err != nil {1468 if err := target.Validate(); err != nil {
1469- return &protoErr{ports.ErrNoSuchSession, "invalid exact session target"}1469+ return &protoErr{protocol.ErrNoSuchSession, "invalid exact session target"}
1470 }1470 }
1471 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {1471 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {
1472 return err1472 return err
@@ -1477,7 +1477,7 @@ func (d *Daemon) validateExactSessionTarget(ctx context.Context, target protocol
1477 return d.validateExactSessionTargetLocked(target)1477 return d.validateExactSessionTargetLocked(target)
1478 }1478 }
1479 1479
1480-func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedClient, error) {1480+func (d *Daemon) route(h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
1481 ctx := d.serveCtx1481 ctx := d.serveCtx
1482 if ctx == nil {1482 if ctx == nil {
1483 ctx = context.Background()1483 ctx = context.Background()
@@ -1485,7 +1485,7 @@ func (d *Daemon) route(h ports.Hello, tr ports.Transport) (*session, *attachedCl
1485 return d.routeWithContext(ctx, h, tr)1485 return d.routeWithContext(ctx, h, tr)
1486 }1486 }
1487 1487
1488-func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.Transport) (*session, *attachedClient, error) {1488+func (d *Daemon) routeWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
1489 if ctx == nil {1489 if ctx == nil {
1490 ctx = context.Background()1490 ctx = context.Background()
1491 }1491 }
@@ -1494,13 +1494,13 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
1494 }1494 }
1495 sz := h.Size1495 sz := h.Size
1496 if !sz.Valid() {1496 if !sz.Valid() {
1497- return nil, nil, &protoErr{ports.ErrInternal, "invalid terminal size"}1497+ return nil, nil, &protoErr{protocol.ErrInternal, "invalid terminal size"}
1498 }1498 }
1499- if h.Intent == ports.IntentResume && h.ResumeToken == 0 {1499+ if h.Intent == protocol.IntentResume && h.ResumeToken == 0 {
1500- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is required"}1500+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is required"}
1501 }1501 }
1502 if h.ExactTarget != nil && h.ResumeToken == 0 && h.Name != h.ExactTarget.SessionName {1502 if h.ExactTarget != nil && h.ResumeToken == 0 && h.Name != h.ExactTarget.SessionName {
1503- return nil, nil, &protoErr{ports.ErrNoSuchSession, "exact session target name mismatch"}1503+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "exact session target name mismatch"}
1504 }1504 }
1505 if h.RemoteTarget != nil && h.ResumeToken == 0 {1505 if h.RemoteTarget != nil && h.ResumeToken == 0 {
1506 return d.routeRemoteTargetWithContext(ctx, h, tr)1506 return d.routeRemoteTargetWithContext(ctx, h, tr)
@@ -1526,36 +1526,36 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
1526 // Live recovery parked then lost a competing resumeParked1526 // Live recovery parked then lost a competing resumeParked
1527 // race; keep the fail-closed wire response instead of1527 // race; keep the fail-closed wire response instead of
1528 // leaking the internal sentinel to handleHello as ErrInternal.1528 // leaking the internal sentinel to handleHello as ErrInternal.
1529- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}1529+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
1530 }1530 }
1531 return nil, nil, err1531 return nil, nil, err
1532 } else if ok {1532 } else if ok {
1533 return sess, ac, nil1533 return sess, ac, nil
1534 }1534 }
1535- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}1535+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
1536 }1536 }
1537 if sess, ac, ok, err := d.resumeParked(h, tr, sz); err == nil {1537 if sess, ac, ok, err := d.resumeParked(h, tr, sz); err == nil {
1538 if ok {1538 if ok {
1539 return sess, ac, nil1539 return sess, ac, nil
1540 }1540 }
1541- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}1541+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
1542 } else if errors.Is(err, errResumeTokenLifecycleRace) {1542 } else if errors.Is(err, errResumeTokenLifecycleRace) {
1543 // The parked entry was replaced while this handshake waited for1543 // The parked entry was replaced while this handshake waited for
1544 // its send lock. Fail closed on the original credential rather1544 // its send lock. Fail closed on the original credential rather
1545 // than falling through to ordinary attach/create routing.1545 // than falling through to ordinary attach/create routing.
1546- return nil, nil, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}1546+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
1547 } else {1547 } else {
1548 return nil, nil, err1548 return nil, nil, err
1549 }1549 }
1550 }1550 }
1551- if h.Intent == ports.IntentResume || h.Intent == ports.IntentAttach {1551+ if h.Intent == protocol.IntentResume || h.Intent == protocol.IntentAttach {
1552 if err := d.waitForTargetRestore(ctx, h.Name); err != nil {1552 if err := d.waitForTargetRestore(ctx, h.Name); err != nil {
1553 return nil, nil, err1553 return nil, nil, err
1554 }1554 }
1555 }1555 }
1556- if h.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned && h.RemoteTarget == nil &&1556+ if h.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned && h.RemoteTarget == nil &&
1557- (h.Intent == ports.IntentNew || h.Intent == ports.IntentEphemeral) {1557+ (h.Intent == protocol.IntentNew || h.Intent == protocol.IntentEphemeral) {
1558- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "daemon-owned environment requires an exact remote target"}1558+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "daemon-owned environment requires an exact remote target"}
1559 }1559 }
1560 d.mu.Lock()1560 d.mu.Lock()
1561 // Shutdown/create interlock: once shutdown has begun (last session removed,1561 // Shutdown/create interlock: once shutdown has begun (last session removed,
@@ -1564,7 +1564,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
1564 // anything inserted now would leak its PTY and hang Serve.1564 // anything inserted now would leak its PTY and hang Serve.
1565 if d.closing {1565 if d.closing {
1566 d.mu.Unlock()1566 d.mu.Unlock()
1567- return nil, nil, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}1567+ return nil, nil, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
1568 }1568 }
1569 // The first exact-target check happens before restore I/O. Recheck while1569 // The first exact-target check happens before restore I/O. Recheck while
1570 // holding d.mu so a same-name lifecycle replacement cannot slip between1570 // holding d.mu so a same-name lifecycle replacement cannot slip between
@@ -1576,7 +1576,7 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
1576 }1576 }
1577 }1577 }
1578 switch h.Intent {1578 switch h.Intent {
1579- case ports.IntentEphemeral:1579+ case protocol.IntentEphemeral:
1580 name := d.allocEphemeralNameLocked()1580 name := d.allocEphemeralNameLocked()
1581 sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, h.Geometry(), h.Env)1581 sess, err := d.createSessionLockedWithMode(name, true, h.Cwd, h.Geometry(), h.Env)
1582 if err != nil {1582 if err != nil {
@@ -1586,18 +1586,18 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
1586 ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true)1586 ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true)
1587 return sess, ac, err1587 return sess, ac, err
1588 1588
1589- case ports.IntentNew:1589+ case protocol.IntentNew:
1590 if h.Name == "" {1590 if h.Name == "" {
1591 d.mu.Unlock()1591 d.mu.Unlock()
1592- return nil, nil, &protoErr{ports.ErrInvalidSessionName, "empty session name"}1592+ return nil, nil, &protoErr{protocol.ErrInvalidSessionName, "empty session name"}
1593 }1593 }
1594 if err := domain.ValidateSessionName(h.Name); err != nil {1594 if err := domain.ValidateSessionName(h.Name); err != nil {
1595 d.mu.Unlock()1595 d.mu.Unlock()
1596- return nil, nil, &protoErr{ports.ErrInvalidSessionName, err.Error()}1596+ return nil, nil, &protoErr{protocol.ErrInvalidSessionName, err.Error()}
1597 }1597 }
1598 if d.nameLiveOrStoppedLocked(h.Name) {1598 if d.nameLiveOrStoppedLocked(h.Name) {
1599 d.mu.Unlock()1599 d.mu.Unlock()
1600- return nil, nil, &protoErr{ports.ErrNameTaken, "session name already in use: " + h.Name}1600+ return nil, nil, &protoErr{protocol.ErrNameTaken, "session name already in use: " + h.Name}
1601 }1601 }
1602 sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, h.Geometry(), h.Env)1602 sess, err := d.createSessionLockedWithMode(h.Name, false, h.Cwd, h.Geometry(), h.Env)
1603 if err != nil {1603 if err != nil {
@@ -1607,18 +1607,18 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
1607 ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true)1607 ac, err := d.finishRouteAttach(sess, tr, sz, h, true, true)
1608 return sess, ac, err1608 return sess, ac, err
1609 1609
1610- case ports.IntentAttach:1610+ case protocol.IntentAttach:
1611 sess := d.findByNameLocked(h.Name)1611 sess := d.findByNameLocked(h.Name)
1612 created := false1612 created := false
1613 if sess == nil {1613 if sess == nil {
1614 stopped, ok := d.inactive[h.Name]1614 stopped, ok := d.inactive[h.Name]
1615 if !ok || !stopped.canResume() {1615 if !ok || !stopped.canResume() {
1616 d.mu.Unlock()1616 d.mu.Unlock()
1617- return nil, nil, &protoErr{ports.ErrNoSuchSession, "no such resumable session: " + h.Name}1617+ return nil, nil, &protoErr{protocol.ErrNoSuchSession, "no such resumable session: " + h.Name}
1618 }1618 }
1619 cwd := d.dirOrHome(stopped.cwd)1619 cwd := d.dirOrHome(stopped.cwd)
1620 env := h.Env1620 env := h.Env
1621- if h.EnvironmentPolicy == ports.EnvironmentPolicyDaemonOwned {1621+ if h.EnvironmentPolicy == protocol.EnvironmentPolicyDaemonOwned {
1622 env = copyEnvironment(d.baseEnv)1622 env = copyEnvironment(d.baseEnv)
1623 }1623 }
1624 var err error1624 var err error
@@ -1634,6 +1634,6 @@ func (d *Daemon) routeWithContext(ctx context.Context, h ports.Hello, tr ports.T
1634 1634
1635 default:1635 default:
1636 d.mu.Unlock()1636 d.mu.Unlock()
1637- return nil, nil, &protoErr{ports.ErrInternal, "unknown intent"}1637+ return nil, nil, &protoErr{protocol.ErrInternal, "unknown intent"}
1638 }1638 }
1639 }1639 }
M internal/usecase/daemon/daemon_test.go +10 -9
@@ -16,6 +16,7 @@ import (
16 "github.com/bnema/vev/internal/domain"16 "github.com/bnema/vev/internal/domain"
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/keys"20 "github.com/bnema/vev/internal/usecase/keys"
20 )21 )
21 22
@@ -265,13 +266,13 @@ func newConn(t *testing.T, first ports.Frame, more ...ports.Frame) (*portsmocks.
265 }266 }
266 267
267 func mustHello(intent uint8, name string, sz domain.Size) ports.Frame {268 func mustHello(intent uint8, name string, sz domain.Size) ports.Frame {
268- return ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{269+ return ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
269- Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color",270+ Version: protocol.Version, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color",
270 })}271 })}
271 }272 }
272 273
273 func frameInput(data []byte) ports.Frame {274 func frameInput(data []byte) ports.Frame {
274- return ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: data})}275+ return ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: data})}
275 }276 }
276 277
277 func screenLineText(s *vt.Screen, y int) string {278 func screenLineText(s *vt.Screen, y int) string {
@@ -315,9 +316,9 @@ func sessionCount(d *Daemon) int {
315 return len(d.sessions)316 return len(d.sessions)
316 }317 }
317 318
318-func listSessions(t *testing.T, d *Daemon) ports.Sessions {319+func listSessions(t *testing.T, d *Daemon) protocol.Sessions {
319 t.Helper()320 t.Helper()
320- tr, sends, _ := newConn(t, ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})})321+ tr, sends, _ := newConn(t, ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(protocol.List{})})
321 d.handleList(tr)322 d.handleList(tr)
322 f := awaitFrame(t, sends, ports.MsgSessions)323 f := awaitFrame(t, sends, ports.MsgSessions)
323 sessions, err := ports.UnmarshalSessions(f.Payload)324 sessions, err := ports.UnmarshalSessions(f.Payload)
@@ -594,7 +595,7 @@ func TestRouteRejectsInvalidTerminalSizeBeforeResumeMutation(t *testing.T) {
594 d := newTestDaemon(t, newFactory(t, pty), stubClock{})595 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
595 596
596 transport := &closeTrackingTransport{}597 transport := &closeTrackingTransport{}
597- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), transport)598+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), transport)
598 require.NoError(t, err)599 require.NoError(t, err)
599 token := ac.resumeToken600 token := ac.resumeToken
600 d.clientGone(sess, ac, transport, false)601 d.clientGone(sess, ac, transport, false)
@@ -604,12 +605,12 @@ func TestRouteRejectsInvalidTerminalSizeBeforeResumeMutation(t *testing.T) {
604 d.mu.Unlock()605 d.mu.Unlock()
605 require.NotNil(t, parked)606 require.NotNil(t, parked)
606 607
607- invalid := helloResumeCapable(ports.IntentResume, "work", token)608+ invalid := helloResumeCapable(protocol.IntentResume, "work", token)
608 invalid.Size = tt.size609 invalid.Size = tt.size
609 _, _, err = d.route(invalid, &closeTrackingTransport{})610 _, _, err = d.route(invalid, &closeTrackingTransport{})
610 var protocolErr *protoErr611 var protocolErr *protoErr
611 require.ErrorAs(t, err, &protocolErr)612 require.ErrorAs(t, err, &protocolErr)
612- require.Equal(t, ports.ErrInternal, protocolErr.code)613+ require.Equal(t, protocol.ErrInternal, protocolErr.code)
613 614
614 d.mu.Lock()615 d.mu.Lock()
615 require.Same(t, parked, d.parked[token], "invalid resume must not consume the parked attachment")616 require.Same(t, parked, d.parked[token], "invalid resume must not consume the parked attachment")
@@ -664,7 +665,7 @@ func TestHandleHelloDefersFreshOutputUntilWelcome(t *testing.T) {
664 tr := newWelcomeBlockingTransport(t)665 tr := newWelcomeBlockingTransport(t)
665 done := make(chan struct{})666 done := make(chan struct{})
666 go func() {667 go func() {
667- d.handleHello(tr.tr, mustHello(ports.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))668+ d.handleHello(tr.tr, mustHello(protocol.IntentNew, "welcome-gate", domain.Size{Cols: 80, Rows: 24}))
668 close(done)669 close(done)
669 }()670 }()
670 671
M internal/usecase/daemon/durable_recovery_test.go +33 -32
@@ -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 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"24 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
24 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"25 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
25 )26 )
@@ -304,8 +305,8 @@ func TestCatalogueRestoreIndependent(t *testing.T) {
304 healthyEntry := d.inactive[healthy.Name]305 healthyEntry := d.inactive[healthy.Name]
305 brokenEntry := d.inactive[broken.Name]306 brokenEntry := d.inactive[broken.Name]
306 d.mu.Unlock()307 d.mu.Unlock()
307- require.Equal(t, ports.SessionDown, healthyEntry.state)308+ require.Equal(t, protocol.SessionDown, healthyEntry.state)
308- require.Equal(t, ports.SessionBroken, brokenEntry.state)309+ require.Equal(t, protocol.SessionBroken, brokenEntry.state)
309 for name, done := range map[string]<-chan struct{}{healthy.Name: healthyEntry.restoreDone, broken.Name: brokenEntry.restoreDone} {310 for name, done := range map[string]<-chan struct{}{healthy.Name: healthyEntry.restoreDone, broken.Name: brokenEntry.restoreDone} {
310 select {311 select {
311 case <-done:312 case <-done:
@@ -344,14 +345,14 @@ func TestCatalogueRestoreResetsOnlyIncompatibleCheckpoint(t *testing.T) {
344 require.Empty(t, fresh.DegradedReason)345 require.Empty(t, fresh.DegradedReason)
345 require.Empty(t, fresh.TabNames)346 require.Empty(t, fresh.TabNames)
346 require.Equal(t, []domain.IncarnationID{incompatible.IncarnationID}, repository.deleted)347 require.Equal(t, []domain.IncarnationID{incompatible.IncarnationID}, repository.deleted)
347- require.Equal(t, ports.SessionDown, d.inactive[incompatible.Name].state)348+ require.Equal(t, protocol.SessionDown, d.inactive[incompatible.Name].state)
348 require.Equal(t, fresh, d.inactive[incompatible.Name].record)349 require.Equal(t, fresh, d.inactive[incompatible.Name].record)
349 350
350 restoredSibling, ok, err := catalogue.Record(healthy.Name)351 restoredSibling, ok, err := catalogue.Record(healthy.Name)
351 require.NoError(t, err)352 require.NoError(t, err)
352 require.True(t, ok)353 require.True(t, ok)
353 require.Equal(t, healthy, restoredSibling)354 require.Equal(t, healthy, restoredSibling)
354- require.Equal(t, ports.SessionDown, d.inactive[healthy.Name].state)355+ require.Equal(t, protocol.SessionDown, d.inactive[healthy.Name].state)
355 require.Equal(t, 1, repository.loads[healthy.Name])356 require.Equal(t, 1, repository.loads[healthy.Name])
356 }357 }
357 358
@@ -381,7 +382,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
381 d.mu.Lock()382 d.mu.Lock()
382 entry := d.inactive[record.Name]383 entry := d.inactive[record.Name]
383 d.mu.Unlock()384 d.mu.Unlock()
384- require.Equal(t, ports.SessionDown, entry.state)385+ require.Equal(t, protocol.SessionDown, entry.state)
385 require.Equal(t, fresh, entry.record)386 require.Equal(t, fresh, entry.record)
386 require.Equal(t, restoreDone, entry.restoreDone)387 require.Equal(t, restoreDone, entry.restoreDone)
387 select {388 select {
@@ -394,7 +395,7 @@ func TestIncompatibleCheckpointDeleteFailurePublishesFreshStoppedAuthority(t *te
394 d.mu.Lock()395 d.mu.Lock()
395 entry = d.inactive[record.Name]396 entry = d.inactive[record.Name]
396 d.mu.Unlock()397 d.mu.Unlock()
397- require.Equal(t, ports.SessionDown, entry.state)398+ require.Equal(t, protocol.SessionDown, entry.state)
398 require.Equal(t, fresh, entry.record)399 require.Equal(t, fresh, entry.record)
399 select {400 select {
400 case <-entry.restoreDone:401 case <-entry.restoreDone:
@@ -452,7 +453,7 @@ func TestRestoreLoadFailuresRetainCheckpointWithoutDeletion(t *testing.T) {
452 require.Equal(t, record.IncarnationID, persisted.IncarnationID)453 require.Equal(t, record.IncarnationID, persisted.IncarnationID)
453 require.Equal(t, record.Committed, persisted.Committed)454 require.Equal(t, record.Committed, persisted.Committed)
454 require.Equal(t, "checkpoint validation failed", persisted.DegradedReason)455 require.Equal(t, "checkpoint validation failed", persisted.DegradedReason)
455- require.Equal(t, ports.SessionBroken, d.inactive[record.Name].state)456+ require.Equal(t, protocol.SessionBroken, d.inactive[record.Name].state)
456 require.Empty(t, repository.deleted)457 require.Empty(t, repository.deleted)
457 })458 })
458 }459 }
@@ -472,7 +473,7 @@ func TestCatalogueRestoreAggregateOver4096(t *testing.T) {
472 473
473 require.Len(t, d.inactive, count)474 require.Len(t, d.inactive, count)
474 for _, record := range records {475 for _, record := range records {
475- require.Equal(t, ports.SessionDown, d.inactive[record.Name].state)476+ require.Equal(t, protocol.SessionDown, d.inactive[record.Name].state)
476 require.Equal(t, 1, repository.loads[record.Name])477 require.Equal(t, 1, repository.loads[record.Name])
477 }478 }
478 }479 }
@@ -486,7 +487,7 @@ func TestCatalogueRestoreSingleSessionOver4096(t *testing.T) {
486 // discovery API and therefore performs exactly one direct catalogue lookup.487 // discovery API and therefore performs exactly one direct catalogue lookup.
487 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))488 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
488 489
489- require.Equal(t, ports.SessionDown, d.inactive[record.Name].state)490+ require.Equal(t, protocol.SessionDown, d.inactive[record.Name].state)
490 require.Equal(t, 1, repository.loads[record.Name])491 require.Equal(t, 1, repository.loads[record.Name])
491 }492 }
492 493
@@ -499,7 +500,7 @@ func TestCatalogueRestoreIncarnationMismatch(t *testing.T) {
499 500
500 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))501 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
501 502
502- require.Equal(t, ports.SessionBroken, d.inactive[record.Name].state)503+ require.Equal(t, protocol.SessionBroken, d.inactive[record.Name].state)
503 got, _, _ := catalogue.Record(record.Name)504 got, _, _ := catalogue.Record(record.Name)
504 require.Equal(t, "checkpoint validation failed", got.DegradedReason)505 require.Equal(t, "checkpoint validation failed", got.DegradedReason)
505 }506 }
@@ -709,7 +710,7 @@ func TestCreateNamedSessionRegistrationFailureRollsBackDurableState(t *testing.T
709 name: record.Name, cwd: record.Cwd, createdAt: record.CreatedAt,710 name: record.Name, cwd: record.Cwd, createdAt: record.CreatedAt,
710 incarnation: record.IncarnationID, lastUsedSeq: record.LastUsedSeq,711 incarnation: record.IncarnationID, lastUsedSeq: record.LastUsedSeq,
711 tabNames: append([]string(nil), record.TabNames...), record: record,712 tabNames: append([]string(nil), record.TabNames...), record: record,
712- state: ports.SessionDown, restoreDone: make(chan struct{}),713+ state: protocol.SessionDown, restoreDone: make(chan struct{}),
713 }714 }
714 }715 }
715 beforeStopped, hadStopped := d.inactive[tc.sessName]716 beforeStopped, hadStopped := d.inactive[tc.sessName]
@@ -769,7 +770,7 @@ func TestSessionRecoveryCommand(t *testing.T) {
769 770
770 _, err := (controlExec{d: d, recoveryName: degraded.Name}).SessionRecovery("discard")771 _, err := (controlExec{d: d, recoveryName: degraded.Name}).SessionRecovery("discard")
771 require.NoError(t, err)772 require.NoError(t, err)
772- require.Equal(t, ports.SessionDown, d.inactive[degraded.Name].state)773+ require.Equal(t, protocol.SessionDown, d.inactive[degraded.Name].state)
773 require.Equal(t, fresh, d.inactive[degraded.Name].record)774 require.Equal(t, fresh, d.inactive[degraded.Name].record)
774 }775 }
775 776
@@ -782,10 +783,10 @@ func TestListShowsDegraded(t *testing.T) {
782 d, _ := newDurableRecoveryDaemon(t, []domain.CatalogueRecord{fresh, restoring, degraded}, &durableRecoveryRepository{})783 d, _ := newDurableRecoveryDaemon(t, []domain.CatalogueRecord{fresh, restoring, degraded}, &durableRecoveryRepository{})
783 784
784 listed := listSessions(t, d)785 listed := listSessions(t, d)
785- require.Equal(t, []ports.SessionInfo{786+ require.Equal(t, []protocol.SessionInfo{
786- {Name: fresh.Name, State: ports.SessionDown},787+ {Name: fresh.Name, State: protocol.SessionDown},
787- {Name: restoring.Name, State: ports.SessionDown},788+ {Name: restoring.Name, State: protocol.SessionDown},
788- {Name: degraded.Name, State: ports.SessionBroken},789+ {Name: degraded.Name, State: protocol.SessionBroken},
789 }, listed.Sessions)790 }, listed.Sessions)
790 }791 }
791 792
@@ -795,7 +796,7 @@ func TestListHidesPurgingInactiveSession(t *testing.T) {
795 d.mu.Lock()796 d.mu.Lock()
796 entry := d.inactive[record.Name]797 entry := d.inactive[record.Name]
797 entry.purging = true798 entry.purging = true
798- entry.state = ports.SessionDown799+ entry.state = protocol.SessionDown
799 d.inactive[record.Name] = entry800 d.inactive[record.Name] = entry
800 d.mu.Unlock()801 d.mu.Unlock()
801 802
@@ -812,7 +813,7 @@ func TestAttachWaitsForRestore(t *testing.T) {
812 813
813 result := make(chan error, 1)814 result := make(chan error, 1)
814 go func() {815 go func() {
815- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name, Size: domain.Size{Cols: 80, Rows: 24}}, nil)816+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name, Size: domain.Size{Cols: 80, Rows: 24}}, nil)
816 result <- err817 result <- err
817 }()818 }()
818 819
@@ -823,13 +824,13 @@ func TestAttachWaitsForRestore(t *testing.T) {
823 }824 }
824 require.Zero(t, factory.calls.Load())825 require.Zero(t, factory.calls.Load())
825 826
826- d.setStoppedRecovery(record, ports.SessionBroken)827+ d.setStoppedRecovery(record, protocol.SessionBroken)
827 d.mu.Lock()828 d.mu.Lock()
828 closeRuntimeRestoreDoneLocked(d.inactive[record.Name].restoreDone)829 closeRuntimeRestoreDoneLocked(d.inactive[record.Name].restoreDone)
829 d.mu.Unlock()830 d.mu.Unlock()
830 var protocolError *protoErr831 var protocolError *protoErr
831 require.ErrorAs(t, <-result, &protocolError)832 require.ErrorAs(t, <-result, &protocolError)
832- require.Equal(t, ports.ErrInternal, protocolError.code)833+ require.Equal(t, protocol.ErrInternal, protocolError.code)
833 require.Zero(t, factory.calls.Load())834 require.Zero(t, factory.calls.Load())
834 }835 }
835 836
@@ -885,17 +886,17 @@ func TestRestoreCancellationTransitionsBeforeAttachCompletion(t *testing.T) {
885 886
886 attached := make(chan error, 1)887 attached := make(chan error, 1)
887 go func() {888 go func() {
888- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name}, nil)889+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name}, nil)
889 attached <- err890 attached <- err
890 }()891 }()
891 cancelRestore()892 cancelRestore()
892 893
893 var protocolError *protoErr894 var protocolError *protoErr
894 require.ErrorAs(t, <-attached, &protocolError)895 require.ErrorAs(t, <-attached, &protocolError)
895- require.Equal(t, ports.ErrInternal, protocolError.code)896+ require.Equal(t, protocol.ErrInternal, protocolError.code)
896 <-restored897 <-restored
897 entry := d.inactive[record.Name]898 entry := d.inactive[record.Name]
898- require.Equal(t, ports.SessionBroken, entry.state)899+ require.Equal(t, protocol.SessionBroken, entry.state)
899 require.Equal(t, "restore interrupted", entry.record.DegradedReason)900 require.Equal(t, "restore interrupted", entry.record.DegradedReason)
900 select {901 select {
901 case <-entry.restoreDone:902 case <-entry.restoreDone:
@@ -920,7 +921,7 @@ func TestWaitForTargetRestoreRejectsClosedRestoringChannel(t *testing.T) {
920 case err := <-result:921 case err := <-result:
921 var protocolError *protoErr922 var protocolError *protoErr
922 require.ErrorAs(t, err, &protocolError)923 require.ErrorAs(t, err, &protocolError)
923- require.Equal(t, ports.ErrInternal, protocolError.code)924+ require.Equal(t, protocol.ErrInternal, protocolError.code)
924 case <-time.After(time.Second):925 case <-time.After(time.Second):
925 t.Fatal("waiter spun on a closed restore channel")926 t.Fatal("waiter spun on a closed restore channel")
926 }927 }
@@ -932,11 +933,11 @@ func TestWaitForTargetRestoreDistinguishesHealthyRuntime(t *testing.T) {
932 record := durableRecoveryRecord(0)933 record := durableRecoveryRecord(0)
933 tests := []struct {934 tests := []struct {
934 name string935 name string
935- state ports.SessionState936+ state protocol.SessionState
936 message string937 message string
937 }{938 }{
938- {name: "healthy record without runtime", state: ports.SessionDown, message: "session was not restored into this daemon: " + record.Name},939+ {name: "healthy record without runtime", state: protocol.SessionDown, message: "session was not restored into this daemon: " + record.Name},
939- {name: "broken durable state", state: ports.SessionBroken, message: "session durable state is broken: " + record.Name},940+ {name: "broken durable state", state: protocol.SessionBroken, message: "session durable state is broken: " + record.Name},
940 }941 }
941 for _, tt := range tests {942 for _, tt := range tests {
942 t.Run(tt.name, func(t *testing.T) {943 t.Run(tt.name, func(t *testing.T) {
@@ -948,7 +949,7 @@ func TestWaitForTargetRestoreDistinguishesHealthyRuntime(t *testing.T) {
948 949
949 var protocolError *protoErr950 var protocolError *protoErr
950 require.ErrorAs(t, d.waitForTargetRestore(context.Background(), record.Name), &protocolError)951 require.ErrorAs(t, d.waitForTargetRestore(context.Background(), record.Name), &protocolError)
951- require.Equal(t, ports.ErrInternal, protocolError.code)952+ require.Equal(t, protocol.ErrInternal, protocolError.code)
952 require.Equal(t, tt.message, protocolError.Error())953 require.Equal(t, tt.message, protocolError.Error())
953 })954 })
954 }955 }
@@ -962,10 +963,10 @@ func TestAttachRejectsDegraded(t *testing.T) {
962 d.serveCtx, d.serveCancel = context.WithCancel(context.Background())963 d.serveCtx, d.serveCancel = context.WithCancel(context.Background())
963 defer d.serveCancel()964 defer d.serveCancel()
964 965
965- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name}, nil)966+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name}, nil)
966 var protocolError *protoErr967 var protocolError *protoErr
967 require.ErrorAs(t, err, &protocolError)968 require.ErrorAs(t, err, &protocolError)
968- require.Equal(t, ports.ErrInternal, protocolError.code)969+ require.Equal(t, protocol.ErrInternal, protocolError.code)
969 require.Zero(t, factory.calls.Load())970 require.Zero(t, factory.calls.Load())
970 }971 }
971 972
@@ -977,7 +978,7 @@ func TestRestoreUnavailableReleasesPerRecordWaiters(t *testing.T) {
977 d.mu.Lock()978 d.mu.Lock()
978 entry := d.inactive[record.Name]979 entry := d.inactive[record.Name]
979 d.mu.Unlock()980 d.mu.Unlock()
980- require.Equal(t, ports.SessionBroken, entry.state)981+ require.Equal(t, protocol.SessionBroken, entry.state)
981 select {982 select {
982 case <-entry.restoreDone:983 case <-entry.restoreDone:
983 default:984 default:
@@ -993,6 +994,6 @@ func TestCatalogueRestoreDegradedVisible(t *testing.T) {
993 994
994 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))995 d.restoreCatalogue(context.Background(), mustDurableRecords(t, catalogue))
995 996
996- require.Equal(t, ports.SessionBroken, d.inactive[record.Name].state)997+ require.Equal(t, protocol.SessionBroken, d.inactive[record.Name].state)
997 require.Zero(t, repository.loads[record.Name])998 require.Zero(t, repository.loads[record.Name])
998 }999 }
M internal/usecase/daemon/exact_route_test.go +12 -13
@@ -5,7 +5,6 @@ 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/ports"
9 "github.com/bnema/vev/internal/protocol"8 "github.com/bnema/vev/internal/protocol"
10 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
11 )10 )
@@ -17,9 +16,9 @@ func TestRouteExactSessionTargetSelectsLifecycle(t *testing.T) {
17 sess.incarnation = remoteLifecycleForTest()16 sess.incarnation = remoteLifecycleForTest()
18 tr, _ := newCapturingTransport(t)17 tr, _ := newCapturingTransport(t)
19 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}18 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
20- hello := ports.Hello{19+ hello := protocol.Hello{
21- Version: ports.ProtocolVersion,20+ Version: protocol.Version,
22- Intent: ports.IntentAttach,21+ Intent: protocol.IntentAttach,
23 Name: target.SessionName,22 Name: target.SessionName,
24 Size: domain.Size{Cols: 80, Rows: 24},23 Size: domain.Size{Cols: 80, Rows: 24},
25 ExactTarget: &target,24 ExactTarget: &target,
@@ -38,9 +37,9 @@ func TestRouteExactSessionTargetRejectsNameMismatch(t *testing.T) {
38 sess.ephemeral = false37 sess.ephemeral = false
39 sess.incarnation = remoteLifecycleForTest()38 sess.incarnation = remoteLifecycleForTest()
40 tr, _ := newCapturingTransport(t)39 tr, _ := newCapturingTransport(t)
41- hello := ports.Hello{40+ hello := protocol.Hello{
42- Version: ports.ProtocolVersion,41+ Version: protocol.Version,
43- Intent: ports.IntentAttach,42+ Intent: protocol.IntentAttach,
44 Name: "other",43 Name: "other",
45 Size: domain.Size{Cols: 80, Rows: 24},44 Size: domain.Size{Cols: 80, Rows: 24},
46 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},45 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name},
@@ -49,7 +48,7 @@ func TestRouteExactSessionTargetRejectsNameMismatch(t *testing.T) {
49 _, _, err := d.routeWithContext(context.Background(), hello, tr)48 _, _, err := d.routeWithContext(context.Background(), hello, tr)
50 var protocolErr *protoErr49 var protocolErr *protoErr
51 require.ErrorAs(t, err, &protocolErr)50 require.ErrorAs(t, err, &protocolErr)
52- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)51+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
53 }52 }
54 53
55 func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {54 func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
@@ -68,7 +67,7 @@ func TestLockedExactSessionTargetRejectsReplacement(t *testing.T) {
68 67
69 var protocolErr *protoErr68 var protocolErr *protoErr
70 require.ErrorAs(t, err, &protocolErr)69 require.ErrorAs(t, err, &protocolErr)
71- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)70+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
72 }71 }
73 72
74 func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {73 func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
@@ -79,9 +78,9 @@ func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
79 tr, _ := newCapturingTransport(t)78 tr, _ := newCapturingTransport(t)
80 wrong := sess.incarnation79 wrong := sess.incarnation
81 wrong[0]++80 wrong[0]++
82- hello := ports.Hello{81+ hello := protocol.Hello{
83- Version: ports.ProtocolVersion,82+ Version: protocol.Version,
84- Intent: ports.IntentAttach,83+ Intent: protocol.IntentAttach,
85 Name: sess.name,84 Name: sess.name,
86 Size: domain.Size{Cols: 80, Rows: 24},85 Size: domain.Size{Cols: 80, Rows: 24},
87 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},86 ExactTarget: &protocol.ExactSessionTarget{LifecycleID: wrong, SessionName: sess.name},
@@ -90,5 +89,5 @@ func TestRouteExactSessionTargetRejectsLifecycleReplacement(t *testing.T) {
90 _, _, err := d.routeWithContext(context.Background(), hello, tr)89 _, _, err := d.routeWithContext(context.Background(), hello, tr)
91 var protocolErr *protoErr90 var protocolErr *protoErr
92 require.ErrorAs(t, err, &protocolErr)91 require.ErrorAs(t, err, &protocolErr)
93- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)92+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
94 }93 }
M internal/usecase/daemon/final_acceptance_test.go +2 -1
@@ -6,6 +6,7 @@ import (
6 vt "github.com/bnema/vev-vt"6 vt "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/stretchr/testify/require"11 "github.com/stretchr/testify/require"
11 )12 )
@@ -150,7 +151,7 @@ func TestAcceptanceAttachmentStateIsolationAcrossResetResizeAndDetach(t *testing
150 151
151 d.handleAttachmentClientFrame(secondToken, ports.Frame{152 d.handleAttachmentClientFrame(secondToken, ports.Frame{
152 Type: ports.MsgOutputResetRequest,153 Type: ports.MsgOutputResetRequest,
153- Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{}),154+ Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{}),
154 })155 })
155 require.Greater(t, second.output.currentEpoch(), secondEpoch)156 require.Greater(t, second.output.currentEpoch(), secondEpoch)
156 require.Equal(t, firstEpoch, first.output.currentEpoch(), "peer output reset crossed attachment boundary")157 require.Equal(t, firstEpoch, first.output.currentEpoch(), "peer output reset crossed attachment boundary")
M internal/usecase/daemon/floating_open_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 )18 )
18 19
19 type contextAwareFloatingPTY struct {20 type contextAwareFloatingPTY struct {
@@ -94,7 +95,7 @@ func TestFloatingSuccessfulOpenTransfersContextOwnershipToPane(t *testing.T) {
94 name: "visible session",95 name: "visible session",
95 desiredVisible: true,96 desiredVisible: true,
96 teardown: func(d *Daemon, sess *session, _ *tab) {97 teardown: func(d *Daemon, sess *session, _ *tab) {
97- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))98+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
98 },99 },
99 },100 },
100 }101 }
@@ -412,7 +413,7 @@ func TestFloatingOpenCancellationBoundsSessionTeardownAndClosesLatePTY(t *testin
412 }413 }
413 414
414 killed := make(chan error, 1)415 killed := make(chan error, 1)
415- go func() { killed <- d.killSession(sess, ports.ReasonSessionKilled, false) }()416+ go func() { killed <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
416 select {417 select {
417 case err := <-killed:418 case err := <-killed:
418 require.NoError(t, err)419 require.NoError(t, err)
M internal/usecase/daemon/floating_test.go +2 -1
@@ -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/layout"22 "github.com/bnema/vev/internal/usecase/layout"
22 themeui "github.com/bnema/vev/internal/usecase/theme"23 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -324,7 +325,7 @@ func TestFloatingLaunchOwnershipJoinsShutdown(t *testing.T) {
324 case <-time.After(time.Second):325 case <-time.After(time.Second):
325 t.Fatal("floating reader did not start")326 t.Fatal("floating reader did not start")
326 }327 }
327- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))328+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
328 select {329 select {
329 case <-waitGroupDone(&d.sessWg):330 case <-waitGroupDone(&d.sessWg):
330 case <-time.After(time.Second):331 case <-time.After(time.Second):
M internal/usecase/daemon/geometry_propagation_test.go +3 -2
@@ -10,6 +10,7 @@ import (
10 vt "github.com/bnema/vev-vt"10 vt "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/stretchr/testify/require"14 "github.com/stretchr/testify/require"
14 )15 )
15 16
@@ -200,14 +201,14 @@ func TestResumeReappliesClaimingPixelGeometryBeforeReturn(t *testing.T) {
200 pty := &blockingRecordingGeometryPTY{done: make(chan struct{})}201 pty := &blockingRecordingGeometryPTY{done: make(chan struct{})}
201 t.Cleanup(func() { _ = pty.Close() })202 t.Cleanup(func() { _ = pty.Close() })
202 d := newTestDaemon(t, &recordingPTYFactory{pty: pty}, stubClock{})203 d := newTestDaemon(t, &recordingPTYFactory{pty: pty}, stubClock{})
203- firstHello := helloResumeCapable(ports.IntentNew, "pixels", 0)204+ firstHello := helloResumeCapable(protocol.IntentNew, "pixels", 0)
204 firstHello.PixelWidth, firstHello.PixelHeight = 800, 480205 firstHello.PixelWidth, firstHello.PixelHeight = 800, 480
205 firstTransport := &closeTrackingTransport{}206 firstTransport := &closeTrackingTransport{}
206 sess, ac, err := d.route(firstHello, firstTransport)207 sess, ac, err := d.route(firstHello, firstTransport)
207 require.NoError(t, err)208 require.NoError(t, err)
208 d.clientGone(sess, ac, firstTransport, false)209 d.clientGone(sess, ac, firstTransport, false)
209 210
210- resumeHello := helloResumeCapable(ports.IntentResume, "pixels", ac.resumeToken)211+ resumeHello := helloResumeCapable(protocol.IntentResume, "pixels", ac.resumeToken)
211 resumeHello.PixelWidth, resumeHello.PixelHeight = 1000, 600212 resumeHello.PixelWidth, resumeHello.PixelHeight = 1000, 600
212 resumed, same, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, resumeHello.Size)213 resumed, same, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, resumeHello.Size)
213 require.NoError(t, err)214 require.NoError(t, err)
M internal/usecase/daemon/graphics_output_test.go +3 -2
@@ -14,6 +14,7 @@ import (
14 "github.com/bnema/vev-vt/graphics"14 "github.com/bnema/vev-vt/graphics"
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/layout"18 "github.com/bnema/vev/internal/usecase/layout"
18 "github.com/stretchr/testify/require"19 "github.com/stretchr/testify/require"
19 )20 )
@@ -149,7 +150,7 @@ func TestFreshCapabilityDowngradePreservesForeignTerminalGraphics(t *testing.T)
149 ac.output.graphicsOutput = newGraphicsOutputState()150 ac.output.graphicsOutput = newGraphicsOutputState()
150 ac.output.graphicsUnsupportedWarned.Store(true)151 ac.output.graphicsUnsupportedWarned.Store(true)
151 d.mu.Lock()152 d.mu.Lock()
152- d.reconfigureAttachmentOutput(sess, ac, ports.Hello{KittyDirectGraphics: false})153+ d.reconfigureAttachmentOutput(sess, ac, protocol.Hello{KittyDirectGraphics: false})
153 d.mu.Unlock()154 d.mu.Unlock()
154 require.False(t, ac.terminalCapabilities.SupportsKittyGraphics())155 require.False(t, ac.terminalCapabilities.SupportsKittyGraphics())
155 require.Nil(t, ac.output.graphicsOutput)156 require.Nil(t, ac.output.graphicsOutput)
@@ -184,7 +185,7 @@ func TestReplacementKittyTerminalGetsFreshGraphicsState(t *testing.T) {
184 oldState.mayHaveEmitted = true185 oldState.mayHaveEmitted = true
185 ac.output.graphicsOutput = oldState186 ac.output.graphicsOutput = oldState
186 ac.output.graphicsUnsupportedWarned.Store(true)187 ac.output.graphicsUnsupportedWarned.Store(true)
187- d.reconfigureAttachmentOutput(sess, ac, ports.Hello{KittyDirectGraphics: true})188+ d.reconfigureAttachmentOutput(sess, ac, protocol.Hello{KittyDirectGraphics: true})
188 d.mu.Unlock()189 d.mu.Unlock()
189 190
190 require.True(t, ac.terminalCapabilities.SupportsKittyGraphics())191 require.True(t, ac.terminalCapabilities.SupportsKittyGraphics())
M internal/usecase/daemon/handshake.go +4 -3
@@ -6,6 +6,7 @@ import (
6 "sync"6 "sync"
7 7
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 var errHandshakeTimeout = errors.New("handshake timed out")12 var errHandshakeTimeout = errors.New("handshake timed out")
@@ -22,9 +23,9 @@ func (d *Daemon) newHandshakeContext(parent context.Context) (context.Context, <
22 if d != nil && d.clock != nil {23 if d != nil && d.clock != nil {
23 clock = d.clock24 clock = d.clock
24 }25 }
25- timer := clock.NewTimer(ports.HandshakeTimeout)26+ timer := clock.NewTimer(protocol.HandshakeTimeout)
26 if timer == nil {27 if timer == nil {
27- timer = systemClock{}.NewTimer(ports.HandshakeTimeout)28+ timer = systemClock{}.NewTimer(protocol.HandshakeTimeout)
28 }29 }
29 stop := make(chan struct{})30 stop := make(chan struct{})
30 done := make(chan struct{})31 done := make(chan struct{})
@@ -153,6 +154,6 @@ func (d *Daemon) failHandshakeAttachment(sess *session, ac *attachedClient, tr p
153 _ = tr.Close()154 _ = tr.Close()
154 }155 }
155 if ac.routeCreatedSession {156 if ac.routeCreatedSession {
156- _ = d.killSessionIfEmpty(sess, ports.ReasonSessionKilled, ac.routeSessionPurge)157+ _ = d.killSessionIfEmpty(sess, protocol.ReasonSessionKilled, ac.routeSessionPurge)
157 }158 }
158 }159 }
M internal/usecase/daemon/handshake_test.go +19 -18
@@ -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 )15 )
15 16
16 type handshakeBlockingTransport struct {17 type handshakeBlockingTransport struct {
@@ -128,7 +129,7 @@ func TestHandshakeFirstPaintCancellationDoesNotRacePaintResult(t *testing.T) {
128 handshakeDone := make(chan struct{})129 handshakeDone := make(chan struct{})
129 go func() {130 go func() {
130 d.handleHelloWithContext(ctx, make(chan struct{}), func() {}, func() {}, tr,131 d.handleHelloWithContext(ctx, make(chan struct{}), func() {}, func() {}, tr,
131- mustHello(ports.IntentNew, "paint-cancel", domain.Size{Cols: 80, Rows: 24}))132+ mustHello(protocol.IntentNew, "paint-cancel", domain.Size{Cols: 80, Rows: 24}))
132 close(handshakeDone)133 close(handshakeDone)
133 }()134 }()
134 135
@@ -156,7 +157,7 @@ func TestHandshakeTimeoutClosesBlockedReceive(t *testing.T) {
156 }()157 }()
157 158
158 timer := <-clock.timers159 timer := <-clock.timers
159- require.Equal(t, ports.HandshakeTimeout, timer.duration)160+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
160 timer.ch <- time.Time{}161 timer.ch <- time.Time{}
161 awaitTestCompletion(t, done, "handshake receive did not stop after timeout")162 awaitTestCompletion(t, done, "handshake receive did not stop after timeout")
162 requireClosedHandshakeTransport(t, tr)163 requireClosedHandshakeTransport(t, tr)
@@ -171,7 +172,7 @@ func TestFailedResumeHandshakeRestoresParkedCredential(t *testing.T) {
171 defer release()172 defer release()
172 d := newTestDaemon(t, newFactory(t, pty), stubClock{})173 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
173 oldTransport := newHandshakeBlockingTransport(false)174 oldTransport := newHandshakeBlockingTransport(false)
174- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "resume-timeout", 0), oldTransport)175+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "resume-timeout", 0), oldTransport)
175 require.NoError(t, err)176 require.NoError(t, err)
176 token := ac.resumeToken177 token := ac.resumeToken
177 d.clientGone(sess, ac, oldTransport, false)178 d.clientGone(sess, ac, oldTransport, false)
@@ -182,12 +183,12 @@ func TestFailedResumeHandshakeRestoresParkedCredential(t *testing.T) {
182 resumeTransport := newHandshakeBlockingTransport(true)183 resumeTransport := newHandshakeBlockingTransport(true)
183 done := make(chan struct{})184 done := make(chan struct{})
184 go func() {185 go func() {
185- d.handleHello(resumeTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(ports.IntentResume, sess.name, token))})186+ d.handleHello(resumeTransport, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(helloResumeCapable(protocol.IntentResume, sess.name, token))})
186 close(done)187 close(done)
187 }()188 }()
188 189
189 timer := <-clock.timers190 timer := <-clock.timers
190- require.Equal(t, ports.HandshakeTimeout, timer.duration)191+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
191 <-resumeTransport.welcome192 <-resumeTransport.welcome
192 timer.ch <- time.Time{}193 timer.ch <- time.Time{}
193 awaitTestCompletion(t, done, "failed resume handshake did not finish")194 awaitTestCompletion(t, done, "failed resume handshake did not finish")
@@ -210,12 +211,12 @@ func TestHandshakeTimeoutClosesBlockedWelcomeSend(t *testing.T) {
210 tr := newHandshakeBlockingTransport(true)211 tr := newHandshakeBlockingTransport(true)
211 done := make(chan struct{})212 done := make(chan struct{})
212 go func() {213 go func() {
213- d.handleHello(tr, mustHello(ports.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))214+ d.handleHello(tr, mustHello(protocol.IntentNew, "timeout-send", domain.Size{Cols: 80, Rows: 24}))
214 close(done)215 close(done)
215 }()216 }()
216 217
217 timer := <-clock.timers218 timer := <-clock.timers
218- require.Equal(t, ports.HandshakeTimeout, timer.duration)219+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
219 <-tr.welcome220 <-tr.welcome
220 timer.ch <- time.Time{}221 timer.ch <- time.Time{}
221 awaitTestCompletion(t, done, "handshake send did not stop after timeout")222 awaitTestCompletion(t, done, "handshake send did not stop after timeout")
@@ -246,12 +247,12 @@ func TestHandshakeTimeoutCancelsRouteRestoreWait(t *testing.T) {
246 tr := newHandshakeBlockingTransport(false)247 tr := newHandshakeBlockingTransport(false)
247 done := make(chan struct{})248 done := make(chan struct{})
248 go func() {249 go func() {
249- d.handleHello(tr, mustHello(ports.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))250+ d.handleHello(tr, mustHello(protocol.IntentAttach, "restoring", domain.Size{Cols: 80, Rows: 24}))
250 close(done)251 close(done)
251 }()252 }()
252 253
253 timer := <-clock.timers254 timer := <-clock.timers
254- require.Equal(t, ports.HandshakeTimeout, timer.duration)255+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
255 timer.ch <- time.Time{}256 timer.ch <- time.Time{}
256 awaitTestCompletion(t, done, "route restore wait did not stop after handshake timeout")257 awaitTestCompletion(t, done, "route restore wait did not stop after handshake timeout")
257 requireClosedHandshakeTransport(t, tr)258 requireClosedHandshakeTransport(t, tr)
@@ -266,7 +267,7 @@ func TestHandshakeTimeoutRemovesRestoredEmptySession(t *testing.T) {
266 defer release()267 defer release()
267 d := newTestDaemon(t, newFactory(t, pty), stubClock{})268 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
268 d.mu.Lock()269 d.mu.Lock()
269- d.inactive["restored"] = inactiveSession{name: "restored", cwd: "/tmp", state: ports.SessionDown}270+ d.inactive["restored"] = inactiveSession{name: "restored", cwd: "/tmp", state: protocol.SessionDown}
270 d.mu.Unlock()271 d.mu.Unlock()
271 272
272 clock := &signalClock{timers: make(chan *signalTimer, 4)}273 clock := &signalClock{timers: make(chan *signalTimer, 4)}
@@ -274,12 +275,12 @@ func TestHandshakeTimeoutRemovesRestoredEmptySession(t *testing.T) {
274 tr := newHandshakeBlockingTransport(true)275 tr := newHandshakeBlockingTransport(true)
275 done := make(chan struct{})276 done := make(chan struct{})
276 go func() {277 go func() {
277- d.handleHello(tr, mustHello(ports.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))278+ d.handleHello(tr, mustHello(protocol.IntentAttach, "restored", domain.Size{Cols: 80, Rows: 24}))
278 close(done)279 close(done)
279 }()280 }()
280 281
281 timer := <-clock.timers282 timer := <-clock.timers
282- require.Equal(t, ports.HandshakeTimeout, timer.duration)283+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
283 <-tr.welcome284 <-tr.welcome
284 timer.ch <- time.Time{}285 timer.ch <- time.Time{}
285 awaitTestCompletion(t, done, "timed-out restored attachment cleanup did not finish")286 awaitTestCompletion(t, done, "timed-out restored attachment cleanup did not finish")
@@ -296,7 +297,7 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
296 defer release()297 defer release()
297 d := newTestDaemon(t, newFactory(t, pty), stubClock{})298 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
298 oldTransport := &closeTrackingTransport{}299 oldTransport := &closeTrackingTransport{}
299- sess, old, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "shared", Size: domain.Size{Cols: 80, Rows: 24}}, oldTransport)300+ sess, old, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "shared", Size: domain.Size{Cols: 80, Rows: 24}}, oldTransport)
300 require.NoError(t, err)301 require.NoError(t, err)
301 302
302 clock := &signalClock{timers: make(chan *signalTimer, 1)}303 clock := &signalClock{timers: make(chan *signalTimer, 1)}
@@ -304,12 +305,12 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
304 tr := newHandshakeBlockingTransport(true)305 tr := newHandshakeBlockingTransport(true)
305 done := make(chan struct{})306 done := make(chan struct{})
306 go func() {307 go func() {
307- d.handleHello(tr, mustHello(ports.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))308+ d.handleHello(tr, mustHello(protocol.IntentAttach, "shared", domain.Size{Cols: 80, Rows: 24}))
308 close(done)309 close(done)
309 }()310 }()
310 311
311 timer := <-clock.timers312 timer := <-clock.timers
312- require.Equal(t, ports.HandshakeTimeout, timer.duration)313+ require.Equal(t, protocol.HandshakeTimeout, timer.duration)
313 <-tr.welcome314 <-tr.welcome
314 timer.ch <- time.Time{}315 timer.ch <- time.Time{}
315 awaitTestCompletion(t, done, "timed-out attachment cleanup did not finish")316 awaitTestCompletion(t, done, "timed-out attachment cleanup did not finish")
@@ -321,7 +322,7 @@ func TestHandshakeTimeoutPreservesUnrelatedAttachment(t *testing.T) {
321 require.Len(t, d.sessions, 1)322 require.Len(t, d.sessions, 1)
322 require.Empty(t, d.parked)323 require.Empty(t, d.parked)
323 d.mu.Unlock()324 d.mu.Unlock()
324- _ = d.killSession(sess, ports.ReasonServerShutdown, false)325+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
325 }326 }
326 327
327 func TestSuccessfulRouteOutlivesStoppedHandshakeContext(t *testing.T) {328 func TestSuccessfulRouteOutlivesStoppedHandshakeContext(t *testing.T) {
@@ -329,7 +330,7 @@ func TestSuccessfulRouteOutlivesStoppedHandshakeContext(t *testing.T) {
329 defer release()330 defer release()
330 d := newTestDaemon(t, newFactory(t, pty), stubClock{})331 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
331 ctx, cancel := context.WithCancel(context.Background())332 ctx, cancel := context.WithCancel(context.Background())
332- sess, ac, err := d.routeWithContext(ctx, ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "lifetime", Size: domain.Size{Cols: 80, Rows: 24}}, &closeTrackingTransport{})333+ sess, ac, err := d.routeWithContext(ctx, protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "lifetime", Size: domain.Size{Cols: 80, Rows: 24}}, &closeTrackingTransport{})
333 require.NoError(t, err)334 require.NoError(t, err)
334 cancel()335 cancel()
335 select {336 select {
@@ -338,5 +339,5 @@ func TestSuccessfulRouteOutlivesStoppedHandshakeContext(t *testing.T) {
338 default:339 default:
339 }340 }
340 require.NotNil(t, ac)341 require.NotNil(t, ac)
341- _ = d.killSession(sess, ports.ReasonServerShutdown, false)342+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
342 }343 }
M internal/usecase/daemon/inactive_session_test.go +7 -7
@@ -6,7 +6,7 @@ import (
6 "github.com/stretchr/testify/require"6 "github.com/stretchr/testify/require"
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 )10 )
11 11
12 func TestInactiveSessionPredicates(t *testing.T) {12 func TestInactiveSessionPredicates(t *testing.T) {
@@ -22,12 +22,12 @@ func TestInactiveSessionPredicates(t *testing.T) {
22 canResume bool22 canResume bool
23 restorePending bool23 restorePending bool
24 }{24 }{
25- {name: "healthy down", entry: inactiveSession{state: ports.SessionDown}, visible: true, canResume: true},25+ {name: "healthy down", entry: inactiveSession{state: protocol.SessionDown}, visible: true, canResume: true},
26- {name: "restore pending remains resumable after wait", entry: inactiveSession{state: ports.SessionDown, restoreDone: pending}, visible: true, canResume: true, restorePending: true},26+ {name: "restore pending remains resumable after wait", entry: inactiveSession{state: protocol.SessionDown, restoreDone: pending}, visible: true, canResume: true, restorePending: true},
27- {name: "settled restore", entry: inactiveSession{state: ports.SessionDown, restoreDone: settled}, visible: true, canResume: true},27+ {name: "settled restore", entry: inactiveSession{state: protocol.SessionDown, restoreDone: settled}, visible: true, canResume: true},
28- {name: "broken", entry: inactiveSession{state: ports.SessionBroken}, visible: true, broken: true},28+ {name: "broken", entry: inactiveSession{state: protocol.SessionBroken}, visible: true, broken: true},
29- {name: "degraded", entry: inactiveSession{state: ports.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}, visible: true, broken: true},29+ {name: "degraded", entry: inactiveSession{state: protocol.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}, visible: true, broken: true},
30- {name: "purging", entry: inactiveSession{state: ports.SessionDown, purging: true}},30+ {name: "purging", entry: inactiveSession{state: protocol.SessionDown, purging: true}},
31 }31 }
32 32
33 for _, test := range tests {33 for _, test := range tests {
M internal/usecase/daemon/input_test.go +1 -1
@@ -884,7 +884,7 @@ func TestAltDDetachesCurrentClient(t *testing.T) {
884 f := awaitFrame(t, sends, ports.MsgDetached)884 f := awaitFrame(t, sends, ports.MsgDetached)
885 det, err := ports.UnmarshalDetached(f.Payload)885 det, err := ports.UnmarshalDetached(f.Payload)
886 require.NoError(t, err)886 require.NoError(t, err)
887- require.Equal(t, ports.ReasonDetach, det.Reason)887+ require.Equal(t, protocol.ReasonDetach, det.Reason)
888 }888 }
889 889
890 func TestRNSOpensPromptAndEnterPromotesEphemeralSession(t *testing.T) {890 func TestRNSOpensPromptAndEnterPromotesEphemeralSession(t *testing.T) {
M internal/usecase/daemon/move_gate_race_test.go +3 -3
@@ -8,7 +8,7 @@ import (
8 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
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/protocol"
12 )12 )
13 13
14 func newMoveGateRaceFixture(t *testing.T) (*Daemon, *session, *tab, *pane, *session, *tab, func()) {14 func newMoveGateRaceFixture(t *testing.T) (*Daemon, *session, *tab, *pane, *session, *tab, func()) {
@@ -93,7 +93,7 @@ func TestMovePaneGateReservationRaceKillWinsWithoutDeadlock(t *testing.T) {
93 waitMoveRace(t, moveReserved, "move reservation")93 waitMoveRace(t, moveReserved, "move reservation")
94 94
95 killDone := make(chan error, 1)95 killDone := make(chan error, 1)
96- go func() { killDone <- d.killSession(source, ports.ReasonSessionKilled, false) }()96+ go func() { killDone <- d.killSession(source, protocol.ReasonSessionKilled, false) }()
97 waitMoveRace(t, killFrozen, "kill gate freeze")97 waitMoveRace(t, killFrozen, "kill gate freeze")
98 close(releaseMoveAdmission)98 close(releaseMoveAdmission)
99 moveErr := waitMoveRace(t, moveDone, "move abort after kill gate acquisition")99 moveErr := waitMoveRace(t, moveDone, "move abort after kill gate acquisition")
@@ -277,7 +277,7 @@ func TestMovePaneGateReservationRaceMoveWinsWithoutDeadlock(t *testing.T) {
277 waitMoveRace(t, moveGateFrozen, "move gate freeze")277 waitMoveRace(t, moveGateFrozen, "move gate freeze")
278 278
279 killDone := make(chan error, 1)279 killDone := make(chan error, 1)
280- go func() { killDone <- d.killSession(source, ports.ReasonSessionKilled, false) }()280+ go func() { killDone <- d.killSession(source, protocol.ReasonSessionKilled, false) }()
281 waitMoveRace(t, killSnapshotted, "kill participant snapshot")281 waitMoveRace(t, killSnapshotted, "kill participant snapshot")
282 close(releaseMove)282 close(releaseMove)
283 283
M internal/usecase/daemon/move_lifecycle_test.go +2 -2
@@ -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/stretchr/testify/require"8 "github.com/stretchr/testify/require"
9 )9 )
10 10
@@ -230,7 +230,7 @@ func TestDaemonShutdownDrainsMoveBeforePaneLifetimeCancellation(t *testing.T) {
230 require.NotNil(t, paneLifetime)230 require.NotNil(t, paneLifetime)
231 231
232 shutdown := make(chan bool, 1)232 shutdown := make(chan bool, 1)
233- go func() { shutdown <- d.shutdownAll(ports.ReasonServerShutdown) }()233+ go func() { shutdown <- d.shutdownAll(protocol.ReasonServerShutdown) }()
234 awaitDaemonMoveClosing(t, d)234 awaitDaemonMoveClosing(t, d)
235 select {235 select {
236 case <-paneLifetime.Done():236 case <-paneLifetime.Done():
M internal/usecase/daemon/move_postcommit.go +2 -2
@@ -6,7 +6,7 @@ import (
6 "os"6 "os"
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 )10 )
11 11
12 // movePostcommitPlan is captured while the move's architecture and owner12 // movePostcommitPlan is captured while the move's architecture and owner
@@ -148,7 +148,7 @@ func (p movePostcommitPlan) execute(d *Daemon) {
148 if err := d.cleanupAttachmentOutput(attachment.ac); err != nil {148 if err := d.cleanupAttachmentOutput(attachment.ac); err != nil {
149 d.log.Warn("move attachment output cleanup failed", "err", err, "operation", p.operation, "source_session", p.sourceName)149 d.log.Warn("move attachment output cleanup failed", "err", err, "operation", p.operation, "source_session", p.sourceName)
150 }150 }
151- d.notifyDetachedSnapshotAsync(attachment, ports.ReasonSessionKilled)151+ d.notifyDetachedSnapshotAsync(attachment, protocol.ReasonSessionKilled)
152 }152 }
153 for _, retirement := range p.retiredParked {153 for _, retirement := range p.retiredParked {
154 if retirement.parked == nil || retirement.parked.ac == nil {154 if retirement.parked == nil || retirement.parked.ac == nil {
M internal/usecase/daemon/move_rejection.go +3 -3
@@ -4,7 +4,7 @@ import (
4 "errors"4 "errors"
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/layout"8 "github.com/bnema/vev/internal/usecase/layout"
9 )9 )
10 10
@@ -70,7 +70,7 @@ func moveRejectionDescriptorByReason(reason moveRejectionReason) moveRejectionDe
70 PaletteCode: domain.NoticeInternal,70 PaletteCode: domain.NoticeInternal,
71 PaletteSeverity: domain.NoticeError,71 PaletteSeverity: domain.NoticeError,
72 PaletteText: "Move failed.",72 PaletteText: "Move failed.",
73- CommandCode: ports.ErrNoSuchTarget,73+ CommandCode: protocol.ErrNoSuchTarget,
74 CommandText: "move request is invalid",74 CommandText: "move request is invalid",
75 }75 }
76 switch reason {76 switch reason {
@@ -129,7 +129,7 @@ func isMoveCommandError(err error) bool {
129 return ok129 return ok
130 }130 }
131 131
132-func moveCommandFailure(err error) ports.CommandResult {132+func moveCommandFailure(err error) protocol.CommandResult {
133 descriptor, ok := moveRejectionDescriptorFor(err)133 descriptor, ok := moveRejectionDescriptorFor(err)
134 if !ok {134 if !ok {
135 descriptor = moveRejectionDescriptorByReason(moveRejectionInvalid)135 descriptor = moveRejectionDescriptorByReason(moveRejectionInvalid)
M internal/usecase/daemon/move_rejection_test.go +2 -2
@@ -7,7 +7,7 @@ import (
7 "github.com/stretchr/testify/require"7 "github.com/stretchr/testify/require"
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/protocol"
11 "github.com/bnema/vev/internal/usecase/layout"11 "github.com/bnema/vev/internal/usecase/layout"
12 )12 )
13 13
@@ -89,7 +89,7 @@ func TestMoveRejectionPresentationParity(t *testing.T) {
89 require.False(t, result.OK)89 require.False(t, result.OK)
90 require.Equal(t, descriptor.CommandCode, result.Code)90 require.Equal(t, descriptor.CommandCode, result.Code)
91 require.Equal(t, descriptor.CommandText, result.Text)91 require.Equal(t, descriptor.CommandText, result.Text)
92- require.Equal(t, ports.ErrNoSuchTarget, result.Code)92+ require.Equal(t, protocol.ErrNoSuchTarget, result.Code)
93 require.Equal(t, tt.commandText, result.Text)93 require.Equal(t, tt.commandText, result.Text)
94 })94 })
95 }95 }
M internal/usecase/daemon/navigation_attachment_handoff_test.go +3 -3
@@ -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 "github.com/bnema/vev/internal/usecase/layout"10 "github.com/bnema/vev/internal/usecase/layout"
11 "github.com/bnema/vev/internal/usecase/picker"11 "github.com/bnema/vev/internal/usecase/picker"
12 "github.com/stretchr/testify/require"12 "github.com/stretchr/testify/require"
@@ -51,7 +51,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
51 run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffect) error {51 run: func(d *Daemon, _ *session, target *session, _ *attachedClient, effect *attachmentEffect) error {
52 d.mu.Lock()52 d.mu.Lock()
53 delete(d.sessions, target.id)53 delete(d.sessions, target.id)
54- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 9, incarnation: domain.IncarnationID{2}, state: ports.SessionDown}54+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 9, incarnation: domain.IncarnationID{2}, state: protocol.SessionDown}
55 d.mu.Unlock()55 d.mu.Unlock()
56 expectedCreatedAt := int64(9)56 expectedCreatedAt := int64(9)
57 return d.switchToTargetForAttachment(effect, picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session")57 return d.switchToTargetForAttachment(effect, picker.Target{Name: "stopped", Stopped: true, ExpectedCreatedAt: &expectedCreatedAt}, sessionHandoffGuard{}, "stopped-session")
@@ -149,7 +149,7 @@ func TestNavigationHandoffsDropReplacedInitiatorWithoutMutation(t *testing.T) {
149 func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.T) {149 func TestStoppedSessionHandoffDoesNotResumeAfterInitiatorReplacement(t *testing.T) {
150 d, source, old, _ := newManualSessionWithPTYs(t, nil)150 d, source, old, _ := newManualSessionWithPTYs(t, nil)
151 d.mu.Lock()151 d.mu.Lock()
152- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 7, incarnation: domain.IncarnationID{3}, state: ports.SessionDown}152+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 7, incarnation: domain.IncarnationID{3}, state: protocol.SessionDown}
153 d.mu.Unlock()153 d.mu.Unlock()
154 154
155 rc := d.attachCoordinator(source, nil, old, true)155 rc := d.attachCoordinator(source, nil, old, true)
M internal/usecase/daemon/nil_pty_test.go +2 -2
@@ -9,7 +9,7 @@ import (
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
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 "github.com/bnema/vev/internal/usecase/layout"13 "github.com/bnema/vev/internal/usecase/layout"
14 )14 )
15 15
@@ -51,7 +51,7 @@ func TestKillSessionIgnoresNilPanePTYs(t *testing.T) {
51 d.sessions[sess.id] = sess51 d.sessions[sess.id] = sess
52 52
53 require.NotPanics(t, func() {53 require.NotPanics(t, func() {
54- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))54+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
55 })55 })
56 require.NotContains(t, d.sessions, sess.id)56 require.NotContains(t, d.sessions, sess.id)
57 }57 }
M internal/usecase/daemon/notice_persist_test.go +4 -4
@@ -6,8 +6,8 @@ import (
6 "github.com/stretchr/testify/require"6 "github.com/stretchr/testify/require"
7 7
8 "github.com/bnema/vev/internal/domain"8 "github.com/bnema/vev/internal/domain"
9- "github.com/bnema/vev/internal/ports"
10 portsmocks "github.com/bnema/vev/internal/ports/mocks"9 portsmocks "github.com/bnema/vev/internal/ports/mocks"
10+ "github.com/bnema/vev/internal/protocol"
11 )11 )
12 12
13 // TestKillSessionServerShutdownSaturatedAppendsNotice proves that when the13 // TestKillSessionServerShutdownSaturatedAppendsNotice proves that when the
@@ -37,9 +37,9 @@ func TestStartupClaimRecordsAndToastsPendingNotices(t *testing.T) {
37 require.Len(t, d.notices.history(), 2, "each drained notice is recorded individually")37 require.Len(t, d.notices.history(), 2, "each drained notice is recorded individually")
38 38
39 tr, _ := newCapturingTransport(t)39 tr, _ := newCapturingTransport(t)
40- sess, ac, err := d.route(ports.Hello{40+ sess, ac, err := d.route(protocol.Hello{
41- Version: ports.ProtocolVersion,41+ Version: protocol.Version,
42- Intent: ports.IntentEphemeral,42+ Intent: protocol.IntentEphemeral,
43 Size: domain.Size{Cols: 80, Rows: 24},43 Size: domain.Size{Cols: 80, Rows: 24},
44 }, tr)44 }, tr)
45 require.NoError(t, err)45 require.NoError(t, err)
M internal/usecase/daemon/notify_test.go +6 -5
@@ -16,6 +16,7 @@ import (
16 "github.com/bnema/vev/internal/domain"16 "github.com/bnema/vev/internal/domain"
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 )20 )
20 21
21 func TestReportAttachmentErrorPreservesLocalOrNilScope(t *testing.T) {22 func TestReportAttachmentErrorPreservesLocalOrNilScope(t *testing.T) {
@@ -30,9 +31,9 @@ func TestReportAttachmentErrorPreservesLocalOrNilScope(t *testing.T) {
30 31
31 func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing.T) {32 func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing.T) {
32 d, sess, ac, _ := newNoticeFixture(t, &noticeClock{})33 d, sess, ac, _ := newNoticeFixture(t, &noticeClock{})
33- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeClipboardFallback})34+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeClipboardFallback})
34- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeClipboardTooLarge})35+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeClipboardTooLarge})
35- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeLinkDegraded})36+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})
36 37
37 toasts, _ := visibleToasts(ac)38 toasts, _ := visibleToasts(ac)
38 require.Len(t, toasts, 3)39 require.Len(t, toasts, 3)
@@ -42,7 +43,7 @@ func TestClientNoticeMapsFixedActionsAndDismissesOnlyConnectionToast(t *testing.
42 {Code: domain.NoticeClipboard, Severity: domain.NoticeError, Message: "image paste failed; sent Ctrl+V", SessionID: sess.id, Count: 1},43 {Code: domain.NoticeClipboard, Severity: domain.NoticeError, Message: "image paste failed; sent Ctrl+V", SessionID: sess.id, Count: 1},
43 }, stripNoticeTimes(toasts))44 }, stripNoticeTimes(toasts))
44 45
45- d.handleClientNotice(sess, ac, ports.ClientNotice{Action: ports.ClientNoticeLinkConnected})46+ d.handleClientNotice(sess, ac, protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})
46 toasts, _ = visibleToasts(ac)47 toasts, _ = visibleToasts(ac)
47 require.Len(t, toasts, 2)48 require.Len(t, toasts, 2)
48 for _, toast := range toasts {49 for _, toast := range toasts {
@@ -57,7 +58,7 @@ func TestMalformedClientNoticeIsIgnored(t *testing.T) {
57 require.True(t, ok, "attached client transport must be a MockTransport")58 require.True(t, ok, "attached client transport must be a MockTransport")
58 frames := []ports.Frame{59 frames := []ports.Frame{
59 {Type: ports.MsgClientNotice, Payload: []byte{0xff}},60 {Type: ports.MsgClientNotice, Payload: []byte{0xff}},
60- {Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})},61+ {Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})},
61 }62 }
62 tr.EXPECT().Recv().RunAndReturn(func() (ports.Frame, error) {63 tr.EXPECT().Recv().RunAndReturn(func() (ports.Frame, error) {
63 frame := frames[0]64 frame := frames[0]
M internal/usecase/daemon/output_state_sync_test.go +6 -6
@@ -72,9 +72,9 @@ func TestDatagramAttachPipelinesRendererBeforeAck(t *testing.T) {
72 defer releasePTY()72 defer releasePTY()
73 d, sess, _, _ := newManualSessionWithPTYs(t, p)73 d, sess, _, _ := newManualSessionWithPTYs(t, p)
74 tr := newDatagramTestTransport()74 tr := newDatagramTestTransport()
75- routed, ac, err := d.route(ports.Hello{75+ routed, ac, err := d.route(protocol.Hello{
76- Version: ports.ProtocolVersion,76+ Version: protocol.Version,
77- Intent: ports.IntentAttach,77+ Intent: protocol.IntentAttach,
78 Name: sess.name,78 Name: sess.name,
79 Size: domain.Size{Cols: 80, Rows: 24},79 Size: domain.Size{Cols: 80, Rows: 24},
80 MaxOutputInFlight: 8,80 MaxOutputInFlight: 8,
@@ -136,9 +136,9 @@ func TestDatagramMultipleUnackedScrollPaintsMatchLatestFrame(t *testing.T) {
136 defer releasePTY()136 defer releasePTY()
137 d, sess, _, _ := newManualSessionWithPTYs(t, p)137 d, sess, _, _ := newManualSessionWithPTYs(t, p)
138 tr := newDatagramTestTransport()138 tr := newDatagramTestTransport()
139- _, ac, err := d.route(ports.Hello{139+ _, ac, err := d.route(protocol.Hello{
140- Version: ports.ProtocolVersion,140+ Version: protocol.Version,
141- Intent: ports.IntentAttach,141+ Intent: protocol.IntentAttach,
142 Name: sess.name,142 Name: sess.name,
143 Size: domain.Size{Cols: 80, Rows: 24},143 Size: domain.Size{Cols: 80, Rows: 24},
144 MaxOutputInFlight: 8,144 MaxOutputInFlight: 8,
M internal/usecase/daemon/palette_test.go +9 -9
@@ -229,10 +229,10 @@ func TestPaletteEntryPublishesEligibleNamedSessionResults(t *testing.T) {
229 d, current, ac, _ := newManualSessionWithPTYs(t, p)229 d, current, ac, _ := newManualSessionWithPTYs(t, p)
230 d.sessions["active"] = &session{sessionCore: sessionCore{id: "active", name: "active", createdAt: 10}}230 d.sessions["active"] = &session{sessionCore: sessionCore{id: "active", name: "active", createdAt: 10}}
231 d.sessions["ephemeral"] = &session{sessionCore: sessionCore{id: "ephemeral", name: "ephemeral", ephemeral: true, createdAt: 11}}231 d.sessions["ephemeral"] = &session{sessionCore: sessionCore{id: "ephemeral", name: "ephemeral", ephemeral: true, createdAt: 11}}
232- d.inactive["stopped"] = inactiveSession{name: "stopped", createdAt: 12, incarnation: domain.IncarnationID{3}, state: ports.SessionDown}232+ d.inactive["stopped"] = inactiveSession{name: "stopped", createdAt: 12, incarnation: domain.IncarnationID{3}, state: protocol.SessionDown}
233- 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: protocol.SessionDown, purging: true}
234- 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: protocol.SessionBroken}
235- 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: protocol.SessionDown, record: domain.CatalogueRecord{DegradedReason: "checkpoint unavailable"}}
236 ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1, Entries: []protocol.RecentRouteEntry{236 ac.setRouteSnapshot(protocol.RecentRouteSnapshot{Generation: 1, Entries: []protocol.RecentRouteEntry{
237 testRouteEntry(1, 1, "purging", 4, protocol.RouteKindLocal),237 testRouteEntry(1, 1, "purging", 4, protocol.RouteKindLocal),
238 testRouteEntry(2, 1, "broken", 5, protocol.RouteKindLocal),238 testRouteEntry(2, 1, "broken", 5, protocol.RouteKindLocal),
@@ -322,7 +322,7 @@ func TestPaletteStoppedSessionResumeFailureKeepsPaletteAndSourceAttachment(t *te
322 p, release := newBlockingPTY(t)322 p, release := newBlockingPTY(t)
323 defer release()323 defer release()
324 d, current, ac, sends := newManualSessionWithPTYs(t, p)324 d, current, ac, sends := newManualSessionWithPTYs(t, p)
325- stopped := inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, lastUsedSeq: 7, tabNames: []string{"shell", "logs"}, state: ports.SessionDown}325+ stopped := inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, lastUsedSeq: 7, tabNames: []string{"shell", "logs"}, state: protocol.SessionDown}
326 d.inactive[stopped.name] = stopped326 d.inactive[stopped.name] = stopped
327 ptys := portsmocks.NewMockPTYFactory(t)327 ptys := portsmocks.NewMockPTYFactory(t)
328 ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("open failed")).Once()328 ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, errors.New("open failed")).Once()
@@ -351,7 +351,7 @@ func TestPaletteSelectedStoppedSessionResumesAndSwitches(t *testing.T) {
351 defer release2()351 defer release2()
352 d, current, ac, sends := newManualSessionWithPTYs(t, p1)352 d, current, ac, sends := newManualSessionWithPTYs(t, p1)
353 d.ptys = newFactory(t, p2)353 d.ptys = newFactory(t, p2)
354- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, state: ports.SessionDown}354+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 42, state: protocol.SessionDown}
355 355
356 d.handleInput(current, ac, []byte("\x1b "))356 d.handleInput(current, ac, []byte("\x1b "))
357 awaitFrame(t, sends, ports.MsgOutput)357 awaitFrame(t, sends, ports.MsgOutput)
@@ -598,9 +598,9 @@ func TestPaletteRemoteCatalogSelectionSendsExactAttachTarget(t *testing.T) {
598 Endpoint: "user@arch", DisplayOrigin: "arch", LifecycleID: remoteLifecycle,598 Endpoint: "user@arch", DisplayOrigin: "arch", LifecycleID: remoteLifecycle,
599 SessionName: "work", LiveTabID: "tab-work",599 SessionName: "work", LiveTabID: "tab-work",
600 }600 }
601- require.Equal(t, ports.AttachTarget{601+ require.Equal(t, protocol.AttachTarget{
602- Endpoint: "user@arch", Session: "work", Intent: ports.IntentAttach,602+ Endpoint: "user@arch", Session: "work", Intent: protocol.IntentAttach,
603- RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,603+ RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
604 }, target)604 }, target)
605 require.False(t, ac.overlays.paletteActive())605 require.False(t, ac.overlays.paletteActive())
606 d.remoteCatalog.mu.Lock()606 d.remoteCatalog.mu.Lock()
M internal/usecase/daemon/parked_route.go +2 -2
@@ -417,7 +417,7 @@ func (d *Daemon) switchParkedRoute(effect *attachmentEffect, request protocol.Pa
417 417
418 func (d *Daemon) waitForParkedTargetRestore(parent context.Context, name string) error {418 func (d *Daemon) waitForParkedTargetRestore(parent context.Context, name string) error {
419 ctx, cancel := context.WithCancel(parent)419 ctx, cancel := context.WithCancel(parent)
420- timer := d.clock.NewTimer(ports.HandshakeTimeout)420+ timer := d.clock.NewTimer(protocol.HandshakeTimeout)
421 stop := make(chan struct{})421 stop := make(chan struct{})
422 go func() {422 go func() {
423 select {423 select {
@@ -501,7 +501,7 @@ func (d *Daemon) transitionParkedRouteTarget(effect *attachmentEffect, target do
501 },501 },
502 })502 })
503 if err != nil && created != nil {503 if err != nil && created != nil {
504- _ = d.killSession(created, ports.ReasonSessionKilled, true)504+ _ = d.killSession(created, protocol.ReasonSessionKilled, true)
505 }505 }
506 if err != nil {506 if err != nil {
507 return attachmentTransitionResult{}, created, err507 return attachmentTransitionResult{}, created, err
M internal/usecase/daemon/parked_route_test.go +1 -1
@@ -168,7 +168,7 @@ func TestParkedRouteRestoresExactStoppedTargetWithDaemonEnvironment(t *testing.T
168 lifecycle := domain.SessionLifecycleID{8}168 lifecycle := domain.SessionLifecycleID{8}
169 d.mu.Lock()169 d.mu.Lock()
170 d.inactive["stopped"] = inactiveSession{170 d.inactive["stopped"] = inactiveSession{
171- name: "stopped", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown,171+ name: "stopped", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown,
172 tabNames: []string{"alpha", "beta"},172 tabNames: []string{"alpha", "beta"},
173 tabRecords: []domain.CatalogueTabRecord{173 tabRecords: []domain.CatalogueTabRecord{
174 {StableID: "tab-a", Name: "alpha"},174 {StableID: "tab-a", Name: "alpha"},
M internal/usecase/daemon/performance_benchmark_test.go +2 -1
@@ -15,6 +15,7 @@ import (
15 renderer "github.com/bnema/vev-vt"15 renderer "github.com/bnema/vev-vt"
16 "github.com/bnema/vev/internal/domain"16 "github.com/bnema/vev/internal/domain"
17 "github.com/bnema/vev/internal/ports"17 "github.com/bnema/vev/internal/ports"
18+ "github.com/bnema/vev/internal/protocol"
18 "github.com/bnema/vev/internal/usecase/layout"19 "github.com/bnema/vev/internal/usecase/layout"
19 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"20 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
20 )21 )
@@ -1100,7 +1101,7 @@ func (f *performanceFixture) close() error {
1100 sess.mu.Lock()1101 sess.mu.Lock()
1101 sess.ephemeral = true1102 sess.ephemeral = true
1102 sess.mu.Unlock()1103 sess.mu.Unlock()
1103- killErr := f.killSession(sess, ports.ReasonServerShutdown, false)1104+ killErr := f.killSession(sess, protocol.ReasonServerShutdown, false)
1104 d.stopSnapshotEncodeWorker()1105 d.stopSnapshotEncodeWorker()
1105 d.serveCancel()1106 d.serveCancel()
1106 d.hardCancel()1107 d.hardCancel()
M internal/usecase/daemon/picker.go +10 -10
@@ -923,11 +923,11 @@ func (d *Daemon) sendLocalAttachTargetForAttachment(effect *attachmentEffect, ta
923 // Explicit tab rows already take the direct transition path above, while923 // Explicit tab rows already take the direct transition path above, while
924 // stopped sessions have no active target session and retain the fallback.924 // stopped sessions have no active target session and retain the fallback.
925 samePeerEligible := guard.allowSamePeer && targetSess != nil && target.TabIndex <= 0925 samePeerEligible := guard.allowSamePeer && targetSess != nil && target.TabIndex <= 0
926- payload := ports.MarshalAttachTarget(ports.AttachTarget{926+ payload := ports.MarshalAttachTarget(protocol.AttachTarget{
927 Session: sessionName,927 Session: sessionName,
928- Intent: ports.IntentAttach,928+ Intent: protocol.IntentAttach,
929 ExactTarget: exactTarget,929 ExactTarget: exactTarget,
930- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,930+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
931 SamePeer: samePeerEligible,931 SamePeer: samePeerEligible,
932 })932 })
933 if payload == nil || exactTarget == nil {933 if payload == nil || exactTarget == nil {
@@ -982,12 +982,12 @@ func (d *Daemon) sendRemoteAttachTargetForAttachment(effect *attachmentEffect, t
982 if err := remoteTarget.Validate(); err != nil || key.Validate() != nil || target.Session != key.ID() || key.Host != remoteTarget.Endpoint || key.Name != remoteTarget.SessionName || key.LifecycleID != remoteTarget.LifecycleID || !d.remoteCatalogTargetReady(remoteTarget) {982 if err := remoteTarget.Validate(); err != nil || key.Validate() != nil || target.Session != key.ID() || key.Host != remoteTarget.Endpoint || key.Name != remoteTarget.SessionName || key.LifecycleID != remoteTarget.LifecycleID || !d.remoteCatalogTargetReady(remoteTarget) {
983 return failUnavailable()983 return failUnavailable()
984 }984 }
985- handoff := ports.AttachTarget{985+ handoff := protocol.AttachTarget{
986 Endpoint: remoteTarget.Endpoint,986 Endpoint: remoteTarget.Endpoint,
987 Session: remoteTarget.SessionName,987 Session: remoteTarget.SessionName,
988- Intent: ports.IntentAttach,988+ Intent: protocol.IntentAttach,
989 RemoteTarget: &remoteTarget,989 RemoteTarget: &remoteTarget,
990- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,990+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
991 }991 }
992 payload := ports.MarshalAttachTarget(handoff)992 payload := ports.MarshalAttachTarget(handoff)
993 if payload == nil {993 if payload == nil {
@@ -1252,7 +1252,7 @@ func (d *Daemon) switchToActiveTargetLocked(from *session, ac *attachedClient, t
1252 // and stopped record untouched.1252 // and stopped record untouched.
1253 func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, target picker.Target, stopped inactiveSession, sourceEffect *attachmentEffect, guard sessionHandoffGuard, action string) (*session, attachmentTransitionResult, bool, error) {1253 func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient, target picker.Target, stopped inactiveSession, sourceEffect *attachmentEffect, guard sessionHandoffGuard, action string) (*session, attachmentTransitionResult, bool, error) {
1254 if stopped.broken() {1254 if stopped.broken() {
1255- return nil, attachmentTransitionResult{}, false, &protoErr{ports.ErrInternal, "session durable state is broken: " + target.Name}1255+ return nil, attachmentTransitionResult{}, false, &protoErr{protocol.ErrInternal, "session durable state is broken: " + target.Name}
1256 }1256 }
1257 if !stopped.canResume() {1257 if !stopped.canResume() {
1258 return nil, attachmentTransitionResult{}, false, errAttachmentTransition1258 return nil, attachmentTransitionResult{}, false, errAttachmentTransition
@@ -1283,7 +1283,7 @@ func (d *Daemon) resumeStoppedAndSwitchLocked(from *session, ac *attachedClient,
1283 if err != nil {1283 if err != nil {
1284 if targetSess != nil {1284 if targetSess != nil {
1285 d.mu.Unlock()1285 d.mu.Unlock()
1286- _ = d.killSession(targetSess, ports.ReasonSessionKilled, true)1286+ _ = d.killSession(targetSess, protocol.ReasonSessionKilled, true)
1287 d.mu.Lock()1287 d.mu.Lock()
1288 }1288 }
1289 return targetSess, attachmentTransitionResult{}, false, err1289 return targetSess, attachmentTransitionResult{}, false, err
@@ -1390,7 +1390,7 @@ func (d *Daemon) killPickerTargetForAttachment(target picker.Target, effect *att
1390 if targetSess == nil {1390 if targetSess == nil {
1391 return nil1391 return nil
1392 }1392 }
1393- return d.killSessionForAttachment(targetSess, ports.ReasonSessionKilled, true, effect, "picker-delete")1393+ return d.killSessionForAttachment(targetSess, protocol.ReasonSessionKilled, true, effect, "picker-delete")
1394 }1394 }
1395 1395
1396 func (d *Daemon) killPickerTarget(target picker.Target) error {1396 func (d *Daemon) killPickerTarget(target picker.Target) error {
@@ -1405,7 +1405,7 @@ func (d *Daemon) killPickerTarget(target picker.Target) error {
1405 targetSess := d.sessions[target.Session]1405 targetSess := d.sessions[target.Session]
1406 d.mu.Unlock()1406 d.mu.Unlock()
1407 if targetSess != nil {1407 if targetSess != nil {
1408- return d.killSession(targetSess, ports.ReasonSessionKilled, true)1408+ return d.killSession(targetSess, protocol.ReasonSessionKilled, true)
1409 }1409 }
1410 return nil1410 return nil
1411 }1411 }
M internal/usecase/daemon/picker_test.go +10 -10
@@ -734,7 +734,7 @@ func TestRestoreCancellationTransitionsBeforePickerCompletion(t *testing.T) {
734 d.mu.Lock()734 d.mu.Lock()
735 entry := d.inactive[record.Name]735 entry := d.inactive[record.Name]
736 d.mu.Unlock()736 d.mu.Unlock()
737- require.Equal(t, ports.SessionBroken, entry.state)737+ require.Equal(t, protocol.SessionBroken, entry.state)
738 require.Equal(t, "restore interrupted", entry.record.DegradedReason)738 require.Equal(t, "restore interrupted", entry.record.DegradedReason)
739 select {739 select {
740 case <-entry.restoreDone:740 case <-entry.restoreDone:
@@ -747,10 +747,10 @@ func TestPickerRejectsCatalogueTargetsWithoutFreshRuntime(t *testing.T) {
747 tests := []struct {747 tests := []struct {
748 name string748 name string
749 broken bool749 broken bool
750- runtimeState ports.SessionState750+ runtimeState protocol.SessionState
751 }{751 }{
752- {name: "broken", broken: true, runtimeState: ports.SessionBroken},752+ {name: "broken", broken: true, runtimeState: protocol.SessionBroken},
753- {name: "healthy without restored runtime", runtimeState: ports.SessionDown},753+ {name: "healthy without restored runtime", runtimeState: protocol.SessionDown},
754 }754 }
755 755
756 for _, tt := range tests {756 for _, tt := range tests {
@@ -783,7 +783,7 @@ func TestPickerRejectsCatalogueTargetsWithoutFreshRuntime(t *testing.T) {
783 require.Equal(t, domain.NoticeSessionUnavailable, userError.Code)783 require.Equal(t, domain.NoticeSessionUnavailable, userError.Code)
784 var protocolError *protoErr784 var protocolError *protoErr
785 require.ErrorAs(t, err, &protocolError)785 require.ErrorAs(t, err, &protocolError)
786- require.Equal(t, ports.ErrInternal, protocolError.code)786+ require.Equal(t, protocol.ErrInternal, protocolError.code)
787 require.Zero(t, factory.calls.Load(), "unsafe target must not open a PTY")787 require.Zero(t, factory.calls.Load(), "unsafe target must not open a PTY")
788 require.Same(t, from, ac.currentSession())788 require.Same(t, from, ac.currentSession())
789 789
@@ -809,7 +809,7 @@ func TestPickerResumesStoppedSessionWithPersistedTabNames(t *testing.T) {
809 defer release3()809 defer release3()
810 d, from, ac, sends := newManualSessionWithPTYs(t, p1)810 d, from, ac, sends := newManualSessionWithPTYs(t, p1)
811 d.ptys = newFactorySeq(t, p2, p3)811 d.ptys = newFactorySeq(t, p2, p3)
812- d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: ports.SessionDown}812+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/tmp/work", createdAt: 7, tabNames: []string{"shell", "logs"}, record: domain.CatalogueRecord{Name: "work"}, state: protocol.SessionDown}
813 813
814 d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "work", Stopped: true})814 d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "work", Stopped: true})
815 awaitFrame(t, sends, ports.MsgOutput)815 awaitFrame(t, sends, ports.MsgOutput)
@@ -1406,11 +1406,11 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
1406 return got.Size != normalSize && got.Size.Valid()1406 return got.Size != normalSize && got.Size.Valid()
1407 })).Return(floating, nil).Once()1407 })).Return(floating, nil).Once()
1408 d := newTestDaemon(t, f, stubClock{})1408 d := newTestDaemon(t, f, stubClock{})
1409- d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: ports.SessionDown}1409+ d.inactive["old"] = inactiveSession{name: "old", cwd: t.TempDir(), createdAt: 1, state: protocol.SessionDown}
1410 tr := portsmocks.NewMockTransport(t)1410 tr := portsmocks.NewMockTransport(t)
1411 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1411 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1412 tr.EXPECT().Close().Return(nil).Maybe()1412 tr.EXPECT().Close().Return(nil).Maybe()
1413- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)1413+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
1414 require.NoError(t, err)1414 require.NoError(t, err)
1415 d.resumeStoppedAndSwitch(sess, ac, picker.Target{Name: "old", Stopped: true})1415 d.resumeStoppedAndSwitch(sess, ac, picker.Target{Name: "old", Stopped: true})
1416 got := ac.currentSession()1416 got := ac.currentSession()
@@ -1428,7 +1428,7 @@ func TestResumeStoppedAndSwitchInheritsTerminalEnv(t *testing.T) {
1428 defer tb.mu.Unlock()1428 defer tb.mu.Unlock()
1429 return tb.floating.pane != nil && tb.floating.pane.pty == floating1429 return tb.floating.pane != nil && tb.floating.pane.pty == floating
1430 }, time.Second, 5*time.Millisecond)1430 }, time.Second, 5*time.Millisecond)
1431- _ = d.killSession(got, ports.ReasonSessionKilled, false)1431+ _ = d.killSession(got, protocol.ReasonSessionKilled, false)
1432 release1()1432 release1()
1433 release2()1433 release2()
1434 d.sessWg.Wait()1434 d.sessWg.Wait()
@@ -1448,7 +1448,7 @@ func TestPickerEnterOnStoppedSessionRestoreFailureSurfacesNoticeAndStaysPut(t *t
1448 p, release := newBlockingPTY(t)1448 p, release := newBlockingPTY(t)
1449 defer release()1449 defer release()
1450 d, from, ac, sends := newManualSessionWithPTYs(t, p)1450 d, from, ac, sends := newManualSessionWithPTYs(t, p)
1451- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp/stopped", createdAt: 7, state: ports.SessionDown}1451+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp/stopped", createdAt: 7, state: protocol.SessionDown}
1452 cause := errors.New("open failed")1452 cause := errors.New("open failed")
1453 ptys := portsmocks.NewMockPTYFactory(t)1453 ptys := portsmocks.NewMockPTYFactory(t)
1454 ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, cause).Once()1454 ptys.EXPECT().Open(mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return(nil, cause).Once()
M internal/usecase/daemon/remote_attach.go +19 -19
@@ -13,24 +13,24 @@ import (
13 // It is deliberately separate from legacy name routing: a lifecycle mismatch,13 // It is deliberately separate from legacy name routing: a lifecycle mismatch,
14 // missing tab, broken record, or stopped-record race is a no-such-target error,14 // missing tab, broken record, or stopped-record race is a no-such-target error,
15 // never an invitation to attach a same-name replacement.15 // never an invitation to attach a same-name replacement.
16-func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello, tr ports.Transport) (*session, *attachedClient, error) {16+func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h protocol.Hello, tr ports.Transport) (*session, *attachedClient, error) {
17 if h.RemoteTarget == nil {17 if h.RemoteTarget == nil {
18 return nil, nil, errors.New("daemon: missing remote target")18 return nil, nil, errors.New("daemon: missing remote target")
19 }19 }
20 target := *h.RemoteTarget20 target := *h.RemoteTarget
21 if err := target.Validate(); err != nil {21 if err := target.Validate(); err != nil {
22- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "invalid remote target"}22+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "invalid remote target"}
23 }23 }
24- if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {24+ if h.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
25- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote target requires daemon-owned environment"}25+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote target requires daemon-owned environment"}
26 }26 }
27- if h.Intent != ports.IntentAttach && h.Intent != ports.IntentResume {27+ if h.Intent != protocol.IntentAttach && h.Intent != protocol.IntentResume {
28- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote target requires attach or resume"}28+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote target requires attach or resume"}
29 }29 }
30 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {30 if err := d.waitForTargetRestore(ctx, target.SessionName); err != nil {
31 var protocolErr *protoErr31 var protocolErr *protoErr
32- if errors.As(err, &protocolErr) && protocolErr.code == ports.ErrInternal {32+ if errors.As(err, &protocolErr) && protocolErr.code == protocol.ErrInternal {
33- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session is unavailable"}33+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session is unavailable"}
34 }34 }
35 return nil, nil, remoteTargetError(err)35 return nil, nil, remoteTargetError(err)
36 }36 }
@@ -38,20 +38,20 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello
38 d.mu.Lock()38 d.mu.Lock()
39 if d.closing {39 if d.closing {
40 d.mu.Unlock()40 d.mu.Unlock()
41- return nil, nil, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}41+ return nil, nil, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
42 }42 }
43 if live := d.findByNameLocked(target.SessionName); live != nil {43 if live := d.findByNameLocked(target.SessionName); live != nil {
44 if live.incarnation != target.LifecycleID {44 if live.incarnation != target.LifecycleID {
45 d.mu.Unlock()45 d.mu.Unlock()
46- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session lifecycle has changed"}46+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session lifecycle has changed"}
47 }47 }
48 if target.Stopped && target.LiveTabID != "" {48 if target.Stopped && target.LiveTabID != "" {
49 d.mu.Unlock()49 d.mu.Unlock()
50- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "stopped target has a live tab selector"}50+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "stopped target has a live tab selector"}
51 }51 }
52 if _, ok := remoteTargetTabIndexLocked(live, target); !ok {52 if _, ok := remoteTargetTabIndexLocked(live, target); !ok {
53 d.mu.Unlock()53 d.mu.Unlock()
54- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote tab no longer exists"}54+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote tab no longer exists"}
55 }55 }
56 ac, err := d.finishRouteAttach(live, tr, h.Size, h, false, false)56 ac, err := d.finishRouteAttach(live, tr, h.Size, h, false, false)
57 return live, ac, err57 return live, ac, err
@@ -59,20 +59,20 @@ func (d *Daemon) routeRemoteTargetWithContext(ctx context.Context, h ports.Hello
59 59
60 if !target.Stopped {60 if !target.Stopped {
61 d.mu.Unlock()61 d.mu.Unlock()
62- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "live remote target has no active runtime"}62+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "live remote target has no active runtime"}
63 }63 }
64 inactive, ok := d.inactive[target.SessionName]64 inactive, ok := d.inactive[target.SessionName]
65 if !ok || inactive.incarnation != target.LifecycleID {65 if !ok || inactive.incarnation != target.LifecycleID {
66 d.mu.Unlock()66 d.mu.Unlock()
67- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session lifecycle no longer exists"}67+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session lifecycle no longer exists"}
68 }68 }
69 if !inactive.canResume() {69 if !inactive.canResume() {
70 d.mu.Unlock()70 d.mu.Unlock()
71- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote session is unavailable"}71+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote session is unavailable"}
72 }72 }
73 if _, ok := remoteTargetTabIndexInactive(inactive, target); !ok {73 if _, ok := remoteTargetTabIndexInactive(inactive, target); !ok {
74 d.mu.Unlock()74 d.mu.Unlock()
75- return nil, nil, &protoErr{ports.ErrNoSuchTarget, "remote stopped tab no longer exists"}75+ return nil, nil, &protoErr{protocol.ErrNoSuchTarget, "remote stopped tab no longer exists"}
76 }76 }
77 77
78 // Picker handoffs use the daemon's own environment and persisted CWD. The78 // Picker handoffs use the daemon's own environment and persisted CWD. The
@@ -156,10 +156,10 @@ func (d *Daemon) sendCommittedRouteIdentityForAttachment(effect *attachmentEffec
156 return effect.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload})156 return effect.sendControl(ports.Frame{Type: ports.MsgCommittedRouteIdentity, Payload: payload})
157 }157 }
158 158
159-func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h ports.Hello, routeCreated, purge bool) (*attachedClient, error) {159+func (d *Daemon) finishRouteAttach(sess *session, tr ports.Transport, sz domain.Size, h protocol.Hello, routeCreated, purge bool) (*attachedClient, error) {
160 ac, err := d.finishAttach(sess, tr, sz, h)160 ac, err := d.finishAttach(sess, tr, sz, h)
161 if err != nil && routeCreated {161 if err != nil && routeCreated {
162- if cleanupErr := d.killSessionIfEmpty(sess, ports.ReasonSessionKilled, purge); cleanupErr != nil {162+ if cleanupErr := d.killSessionIfEmpty(sess, protocol.ReasonSessionKilled, purge); cleanupErr != nil {
163 err = errors.Join(err, cleanupErr)163 err = errors.Join(err, cleanupErr)
164 }164 }
165 return nil, err165 return nil, err
@@ -196,7 +196,7 @@ func remoteTargetError(err error) error {
196 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {196 if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
197 return err197 return err
198 }198 }
199- return &protoErr{ports.ErrNoSuchTarget, "remote target is unavailable"}199+ return &protoErr{protocol.ErrNoSuchTarget, "remote target is unavailable"}
200 }200 }
201 201
202 func stoppedTabMetadata(stopped inactiveSession) []domain.TabSelectorTab {202 func stoppedTabMetadata(stopped inactiveSession) []domain.TabSelectorTab {
M internal/usecase/daemon/remote_attach_test.go +3 -3
@@ -33,7 +33,7 @@ func TestLocalPickerOfferCarriesExactLifecycle(t *testing.T) {
33 got, err := ports.UnmarshalAttachTarget(frame.Payload)33 got, err := ports.UnmarshalAttachTarget(frame.Payload)
34 require.NoError(t, err)34 require.NoError(t, err)
35 require.Empty(t, got.Endpoint)35 require.Empty(t, got.Endpoint)
36- require.Equal(t, ports.IntentAttach, got.Intent)36+ require.Equal(t, protocol.IntentAttach, got.Intent)
37 require.True(t, got.SamePeer)37 require.True(t, got.SamePeer)
38 require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)38 require.Equal(t, &protocol.ExactSessionTarget{LifecycleID: target.incarnation, SessionName: target.name}, got.ExactTarget)
39 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")
@@ -44,7 +44,7 @@ func TestStoppedLocalPickerHandoffWaitsForClientClose(t *testing.T) {
44 source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source")44 source, ac, sends := addRemoteRefreshPickerOwner(t, d, "source")
45 lifecycle := remoteLifecycleForTest()45 lifecycle := remoteLifecycleForTest()
46 d.inactive["stopped"] = inactiveSession{46 d.inactive["stopped"] = inactiveSession{
47- name: "stopped", state: ports.SessionDown, incarnation: lifecycle,47+ name: "stopped", state: protocol.SessionDown, incarnation: lifecycle,
48 }48 }
49 graphics := newGraphicsOutputState()49 graphics := newGraphicsOutputState()
50 graphics.assets["asset"] = graphicsOutputAsset{id: graphics.namespaceBase}50 graphics.assets["asset"] = graphicsOutputAsset{id: graphics.namespaceBase}
@@ -97,7 +97,7 @@ func TestRemotePickerRichHandoffCarriesLifecycleTabAndPolicy(t *testing.T) {
97 require.NoError(t, err)97 require.NoError(t, err)
98 require.NotNil(t, got.RemoteTarget)98 require.NotNil(t, got.RemoteTarget)
99 require.Equal(t, remoteTarget, *got.RemoteTarget)99 require.Equal(t, remoteTarget, *got.RemoteTarget)
100- require.Equal(t, ports.EnvironmentPolicyDaemonOwned, got.EnvironmentPolicy)100+ require.Equal(t, protocol.EnvironmentPolicyDaemonOwned, got.EnvironmentPolicy)
101 }101 }
102 102
103 func TestRemotePickerRichHandoffRejectsMismatchedRouteKey(t *testing.T) {103 func TestRemotePickerRichHandoffRejectsMismatchedRouteKey(t *testing.T) {
M internal/usecase/daemon/remote_picker_test.go +14 -14
@@ -458,7 +458,7 @@ func requireNoRemoteLockViolations(t *testing.T, catalog *channelRemoteCatalog,
458 458
459 func remoteCatalogForTest(sessions ...ports.RemoteCatalogSession) ports.RemoteCatalog {459 func remoteCatalogForTest(sessions ...ports.RemoteCatalogSession) ports.RemoteCatalog {
460 return ports.RemoteCatalog{460 return ports.RemoteCatalog{
461- ProtocolVersion: ports.ProtocolVersion,461+ ProtocolVersion: protocol.Version,
462 SchemaVersion: ports.RemoteCatalogSchemaVersion,462 SchemaVersion: ports.RemoteCatalogSchemaVersion,
463 Sessions: append([]ports.RemoteCatalogSession{}, sessions...),463 Sessions: append([]ports.RemoteCatalogSession{}, sessions...),
464 }464 }
@@ -648,7 +648,7 @@ func TestRemoteRefreshValidationSentinelsRemainMalformed(t *testing.T) {
648 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}648 hosts := &remoteRefreshHostStore{hosts: []string{"arch"}}
649 d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0))649 d, _, _ := newRemoteRefreshDaemon(t, hosts, time.Unix(400, 0))
650 d.remoteCatalog.refresh = 1650 d.remoteCatalog.refresh = 1
651- catalog := ports.RemoteCatalog{ProtocolVersion: ports.ProtocolVersion, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{test.session}}651+ catalog := ports.RemoteCatalog{ProtocolVersion: protocol.Version, SchemaVersion: ports.RemoteCatalogSchemaVersion, Sessions: []ports.RemoteCatalogSession{test.session}}
652 require.False(t, d.applyRemoteRefreshResult(1, "arch", catalog, nil))652 require.False(t, d.applyRemoteRefreshResult(1, "arch", catalog, nil))
653 d.remoteCatalog.mu.Lock()653 d.remoteCatalog.mu.Lock()
654 require.Equal(t, remoteHostMalformed, d.remoteCatalog.status["arch"])654 require.Equal(t, remoteHostMalformed, d.remoteCatalog.status["arch"])
@@ -666,7 +666,7 @@ func TestRemoteRefreshVersionMismatchPreservesStaleRows(t *testing.T) {
666 d.remoteCatalog.refresh = 3666 d.remoteCatalog.refresh = 3
667 d.remoteCatalog.mu.Unlock()667 d.remoteCatalog.mu.Unlock()
668 668
669- require.False(t, d.applyRemoteRefreshResult(3, "arch", ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{Got: 19, Want: ports.ProtocolVersion}))669+ require.False(t, d.applyRemoteRefreshResult(3, "arch", ports.RemoteCatalog{}, &ports.RemoteCatalogVersionMismatchError{Got: 19, Want: protocol.Version}))
670 d.remoteCatalog.mu.Lock()670 d.remoteCatalog.mu.Lock()
671 require.Equal(t, old, d.remoteCatalog.cache["arch"])671 require.Equal(t, old, d.remoteCatalog.cache["arch"])
672 require.Equal(t, remoteHostVersionMismatch, d.remoteCatalog.status["arch"])672 require.Equal(t, remoteHostVersionMismatch, d.remoteCatalog.status["arch"])
@@ -699,7 +699,7 @@ func TestRemotePickerHandoffSendsTargetAndLeavesNoShadowSession(t *testing.T) {
699 require.Equal(t, ports.MsgAttachTarget, frame.Type)699 require.Equal(t, ports.MsgAttachTarget, frame.Type)
700 got, err := ports.UnmarshalAttachTarget(frame.Payload)700 got, err := ports.UnmarshalAttachTarget(frame.Payload)
701 require.NoError(t, err)701 require.NoError(t, err)
702- require.Equal(t, ports.AttachTarget{Endpoint: "arch", Session: "work", Intent: ports.IntentAttach, RemoteTarget: &remoteTarget, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}, got)702+ require.Equal(t, protocol.AttachTarget{Endpoint: "arch", Session: "work", Intent: protocol.IntentAttach, RemoteTarget: &remoteTarget, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}, got)
703 require.Nil(t, ac.currentAttachmentSession())703 require.Nil(t, ac.currentAttachmentSession())
704 d.mu.Lock()704 d.mu.Lock()
705 require.NotContains(t, d.sessions, key.ID(), "remote picker handoff must not create a local session shadow")705 require.NotContains(t, d.sessions, key.ID(), "remote picker handoff must not create a local session shadow")
@@ -759,16 +759,16 @@ func TestRemotePickerSelectsStoppedRemoteTabAndRestoresIt(t *testing.T) {
759 remote := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})759 remote := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
760 remote.mu.Lock()760 remote.mu.Lock()
761 remote.inactive["work"] = inactiveSession{761 remote.inactive["work"] = inactiveSession{
762- name: "work", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown,762+ name: "work", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown,
763 tabNames: []string{"alpha", "beta"},763 tabNames: []string{"alpha", "beta"},
764 tabRecords: []domain.CatalogueTabRecord{{StableID: "tab-a", Name: "alpha"}, {StableID: "tab-b", Name: "beta"}},764 tabRecords: []domain.CatalogueTabRecord{{StableID: "tab-a", Name: "alpha"}, {StableID: "tab-b", Name: "beta"}},
765 }765 }
766 remote.mu.Unlock()766 remote.mu.Unlock()
767 transport, _ := newCapturingTransport(t)767 transport, _ := newCapturingTransport(t)
768- restored, attachment, err := remote.routeWithContext(context.Background(), ports.Hello{768+ restored, attachment, err := remote.routeWithContext(context.Background(), protocol.Hello{
769- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: handoff.Session,769+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: handoff.Session,
770 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: handoff.RemoteTarget,770 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: handoff.RemoteTarget,
771- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,771+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
772 }, transport)772 }, transport)
773 require.NoError(t, err)773 require.NoError(t, err)
774 t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })774 t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })
@@ -825,13 +825,13 @@ func TestRemotePickerResurrectsStoppedRemoteSessionWithoutTabMetadata(t *testing
825 825
826 remote := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})826 remote := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
827 remote.mu.Lock()827 remote.mu.Lock()
828- remote.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown}828+ remote.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown}
829 remote.mu.Unlock()829 remote.mu.Unlock()
830 transport, _ := newCapturingTransport(t)830 transport, _ := newCapturingTransport(t)
831- restored, attachment, err := remote.routeWithContext(context.Background(), ports.Hello{831+ restored, attachment, err := remote.routeWithContext(context.Background(), protocol.Hello{
832- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: handoff.Session,832+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: handoff.Session,
833 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: handoff.RemoteTarget,833 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: handoff.RemoteTarget,
834- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,834+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
835 }, transport)835 }, transport)
836 require.NoError(t, err)836 require.NoError(t, err)
837 t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })837 t.Cleanup(func() { remote.clientGone(restored, attachment, transport, false) })
@@ -962,7 +962,7 @@ func TestRemoteRefreshUpdatesAllOpenPickersPreservingSelection(t *testing.T) {
962 d.remoteCatalog.mu.Unlock()962 d.remoteCatalog.mu.Unlock()
963 963
964 require.True(t, d.applyRemoteRefreshResult(4, "arch", ports.RemoteCatalog{964 require.True(t, d.applyRemoteRefreshResult(4, "arch", ports.RemoteCatalog{
965- ProtocolVersion: ports.ProtocolVersion,965+ ProtocolVersion: protocol.Version,
966 SchemaVersion: ports.RemoteCatalogSchemaVersion,966 SchemaVersion: ports.RemoteCatalogSchemaVersion,
967 Sessions: []ports.RemoteCatalogSession{{967 Sessions: []ports.RemoteCatalogSession{{
968 LifecycleID: remoteLifecycleForTest(), Name: "work", State: "up",968 LifecycleID: remoteLifecycleForTest(), Name: "work", State: "up",
@@ -1117,7 +1117,7 @@ func TestParkedPickerResumePreservesGeneration(t *testing.T) {
1117 d.clientGone(sess, ac, ac.transport(), false)1117 d.clientGone(sess, ac, ac.transport(), false)
1118 token := ac.resumeToken1118 token := ac.resumeToken
1119 tr := &closeTrackingTransport{}1119 tr := &closeTrackingTransport{}
1120- resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, sess.name, token), tr, domain.Size{Cols: 80, Rows: 24})1120+ resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, sess.name, token), tr, domain.Size{Cols: 80, Rows: 24})
1121 require.NoError(t, err)1121 require.NoError(t, err)
1122 require.True(t, ok)1122 require.True(t, ok)
1123 require.Same(t, sess, resumedSess)1123 require.Same(t, sess, resumedSess)
M internal/usecase/daemon/remote_route_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 )13 )
13 14
14 func TestMixedAttachmentCapabilitiesKeepGraphicsAttachmentLocal(t *testing.T) {15 func TestMixedAttachmentCapabilitiesKeepGraphicsAttachmentLocal(t *testing.T) {
@@ -16,8 +17,8 @@ func TestMixedAttachmentCapabilitiesKeepGraphicsAttachmentLocal(t *testing.T) {
16 sess := addControlSession(d, "work", "tab-1", "pane-1")17 sess := addControlSession(d, "work", "tab-1", "pane-1")
17 sess.ephemeral = false18 sess.ephemeral = false
18 kittyTransport, _ := newCapturingTransport(t)19 kittyTransport, _ := newCapturingTransport(t)
19- _, kitty, err := d.route(ports.Hello{20+ _, kitty, err := d.route(protocol.Hello{
20- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,21+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
21 ClientID: [16]byte{1}, KittyDirectGraphics: true,22 ClientID: [16]byte{1}, KittyDirectGraphics: true,
22 }, kittyTransport)23 }, kittyTransport)
23 require.NoError(t, err)24 require.NoError(t, err)
@@ -31,8 +32,8 @@ func TestMixedAttachmentCapabilitiesKeepGraphicsAttachmentLocal(t *testing.T) {
31 pane.mu.Unlock()32 pane.mu.Unlock()
32 33
33 textTransport, _ := newCapturingTransport(t)34 textTransport, _ := newCapturingTransport(t)
34- _, text, err := d.route(ports.Hello{35+ _, text, err := d.route(protocol.Hello{
35- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,36+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
36 ClientID: [16]byte{2},37 ClientID: [16]byte{2},
37 }, textTransport)38 }, textTransport)
38 require.NoError(t, err)39 require.NoError(t, err)
@@ -53,8 +54,8 @@ func TestDirectRemoteAttachSuppressesUndeclaredGraphicsBackend(t *testing.T) {
53 sess := addControlSession(d, "work", "tab-1", "pane-1")54 sess := addControlSession(d, "work", "tab-1", "pane-1")
54 sess.ephemeral = false55 sess.ephemeral = false
55 tr, _ := newCapturingTransport(t)56 tr, _ := newCapturingTransport(t)
56- _, ac, err := d.routeWithContext(context.Background(), ports.Hello{57+ _, ac, err := d.routeWithContext(context.Background(), protocol.Hello{
57- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,58+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
58 Env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, Remote: true,59 Env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, Remote: true,
59 }, tr)60 }, tr)
60 require.NoError(t, err)61 require.NoError(t, err)
@@ -67,8 +68,8 @@ func TestResumeRemoteRoutePreservesDeclaredGraphicsCapability(t *testing.T) {
67 d := newTestDaemon(t, nil, stubClock{})68 d := newTestDaemon(t, nil, stubClock{})
68 sess := addControlSession(d, "work", "tab-1", "pane-1")69 sess := addControlSession(d, "work", "tab-1", "pane-1")
69 sess.ephemeral = false70 sess.ephemeral = false
70- local := ports.Hello{71+ local := protocol.Hello{
71- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,72+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
72 ClientID: [16]byte{1, 2, 3, 4}, Env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, KittyDirectGraphics: true,73 ClientID: [16]byte{1, 2, 3, 4}, Env: []string{"TERM=xterm-kitty", "KITTY_WINDOW_ID=1"}, KittyDirectGraphics: true,
73 }74 }
74 oldTransport, _ := newCapturingTransport(t)75 oldTransport, _ := newCapturingTransport(t)
@@ -78,7 +79,7 @@ func TestResumeRemoteRoutePreservesDeclaredGraphicsCapability(t *testing.T) {
78 token := ac.resumeToken79 token := ac.resumeToken
79 d.clientGone(sess, ac, oldTransport, false)80 d.clientGone(sess, ac, oldTransport, false)
80 81
81- remote := helloResumeCapable(ports.IntentResume, "work", token)82+ remote := helloResumeCapable(protocol.IntentResume, "work", token)
82 remote.Remote = true83 remote.Remote = true
83 remote.KittyDirectGraphics = true84 remote.KittyDirectGraphics = true
84 replacement, _ := newCapturingTransport(t)85 replacement, _ := newCapturingTransport(t)
@@ -98,7 +99,7 @@ func TestRouteRemoteTargetSelectsExactLiveTab(t *testing.T) {
98 sess.incarnation = remoteLifecycleForTest()99 sess.incarnation = remoteLifecycleForTest()
99 tr, _ := newCapturingTransport(t)100 tr, _ := newCapturingTransport(t)
100 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "tab-1"}101 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "tab-1"}
101- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned}102+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
102 _, ac, err := d.routeWithContext(context.Background(), hello, tr)103 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
103 require.NoError(t, err)104 require.NoError(t, err)
104 require.NotNil(t, ac)105 require.NotNil(t, ac)
@@ -114,9 +115,9 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) {
114 sess, err := createSessionForTest(d, "work", false, "/tmp/work", defaultSize, terminalEnv{}, d.baseEnv)115 sess, err := createSessionForTest(d, "work", false, "/tmp/work", defaultSize, terminalEnv{}, d.baseEnv)
115 require.NoError(t, err)116 require.NoError(t, err)
116 target := domain.RemoteSessionTarget{LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "missing-tab"}117 target := domain.RemoteSessionTarget{LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "missing-tab"}
117- hello := ports.Hello{118+ hello := protocol.Hello{
118- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,119+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
119- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,120+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
120 }121 }
121 // The caller must hold d.mu on entry; finishRouteAttach releases it on122 // The caller must hold d.mu on entry; finishRouteAttach releases it on
122 // both success and error paths before returning.123 // both success and error paths before returning.
@@ -124,7 +125,7 @@ func TestFinishRouteAttachRollsBackCreatedSession(t *testing.T) {
124 _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true)125 _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, hello, true, true)
125 var protocolErr *protoErr126 var protocolErr *protoErr
126 require.ErrorAs(t, err, &protocolErr)127 require.ErrorAs(t, err, &protocolErr)
127- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)128+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
128 d.mu.Lock()129 d.mu.Lock()
129 _, retained := d.sessions[sess.id]130 _, retained := d.sessions[sess.id]
130 d.mu.Unlock()131 d.mu.Unlock()
@@ -141,14 +142,14 @@ func TestFinishRouteAttachPreservesConcurrentAttachment(t *testing.T) {
141 142
142 missing := domain.RemoteSessionTarget{LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "missing-tab"}143 missing := domain.RemoteSessionTarget{LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: "missing-tab"}
143 d.mu.Lock()144 d.mu.Lock()
144- _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, ports.Hello{145+ _, err = d.finishRouteAttach(sess, &closeTrackingTransport{}, defaultSize, protocol.Hello{
145- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,146+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
146- RemoteTarget: &missing, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,147+ RemoteTarget: &missing, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
147 }, true, true)148 }, true, true)
148 149
149 var protocolErr *protoErr150 var protocolErr *protoErr
150 require.ErrorAs(t, err, &protocolErr)151 require.ErrorAs(t, err, &protocolErr)
151- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)152+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
152 require.Same(t, sess, winner().currentAttachmentSession())153 require.Same(t, sess, winner().currentAttachmentSession())
153 d.mu.Lock()154 d.mu.Lock()
154 require.Same(t, sess, d.sessions[sess.id])155 require.Same(t, sess, d.sessions[sess.id])
@@ -164,9 +165,9 @@ func TestFailedHandshakeCleanupPreservesConcurrentAttachment(t *testing.T) {
164 require.NoError(t, err)165 require.NoError(t, err)
165 initialTransport, _ := newCapturingTransport(t)166 initialTransport, _ := newCapturingTransport(t)
166 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: domain.TabStableID(sess.tabs[0].stableID)}167 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: sess.incarnation, SessionName: "work", LiveTabID: domain.TabStableID(sess.tabs[0].stableID)}
167- _, initial, err := d.routeWithContext(context.Background(), ports.Hello{168+ _, initial, err := d.routeWithContext(context.Background(), protocol.Hello{
168- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: defaultSize,169+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: defaultSize,
169- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,170+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
170 }, initialTransport)171 }, initialTransport)
171 require.NoError(t, err)172 require.NoError(t, err)
172 initial.routeCreatedSession = true173 initial.routeCreatedSession = true
@@ -193,9 +194,9 @@ func attachWhenRouteCleanupSnapshots(t *testing.T, d *Daemon, sess *session) (fu
193 SessionName: sess.name, LiveTabID: domain.TabStableID(sess.tabs[0].stableID),194 SessionName: sess.name, LiveTabID: domain.TabStableID(sess.tabs[0].stableID),
194 }195 }
195 var err error196 var err error
196- _, winner, err = d.routeWithContext(context.Background(), ports.Hello{197+ _, winner, err = d.routeWithContext(context.Background(), protocol.Hello{
197- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name, Size: defaultSize,198+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name, Size: defaultSize,
198- RemoteTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,199+ RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
199 }, winnerTransport)200 }, winnerTransport)
200 require.NoError(t, err)201 require.NoError(t, err)
201 }202 }
@@ -215,9 +216,9 @@ func TestRouteRemoteTargetRejectsSameNameReplacement(t *testing.T) {
215 var old domain.SessionLifecycleID216 var old domain.SessionLifecycleID
216 old[0] = 8217 old[0] = 8
217 target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: old, SessionName: "work", LiveTabID: "tab-new"}218 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}219+ hello := protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned}
219 _, _, err := d.routeWithContext(context.Background(), hello, tr)220 _, _, err := d.routeWithContext(context.Background(), hello, tr)
220 var protocolErr *protoErr221 var protocolErr *protoErr
221 require.ErrorAs(t, err, &protocolErr)222 require.ErrorAs(t, err, &protocolErr)
222- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)223+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
223 }224 }
M internal/usecase/daemon/remote_stopped_route_test.go +42 -42
@@ -9,8 +9,8 @@ import (
9 "github.com/stretchr/testify/require"9 "github.com/stretchr/testify/require"
10 10
11 "github.com/bnema/vev/internal/domain"11 "github.com/bnema/vev/internal/domain"
12- "github.com/bnema/vev/internal/ports"
13 portsmocks "github.com/bnema/vev/internal/ports/mocks"12 portsmocks "github.com/bnema/vev/internal/ports/mocks"
13+ "github.com/bnema/vev/internal/protocol"
14 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"14 recoveryusecase "github.com/bnema/vev/internal/usecase/recovery"
15 )15 )
16 16
@@ -19,20 +19,20 @@ func TestDaemonOwnedNoExactTargetRejectsNewAndEphemeral(t *testing.T) {
19 name string19 name string
20 intent uint820 intent uint8
21 }{21 }{
22- {name: "new", intent: ports.IntentNew},22+ {name: "new", intent: protocol.IntentNew},
23- {name: "ephemeral", intent: ports.IntentEphemeral},23+ {name: "ephemeral", intent: protocol.IntentEphemeral},
24 } {24 } {
25 t.Run(tt.name, func(t *testing.T) {25 t.Run(tt.name, func(t *testing.T) {
26 d := newTestDaemon(t, nil, stubClock{})26 d := newTestDaemon(t, nil, stubClock{})
27- hello := ports.Hello{27+ hello := protocol.Hello{
28- Version: ports.ProtocolVersion, Intent: tt.intent, Name: "work",28+ Version: protocol.Version, Intent: tt.intent, Name: "work",
29 Size: domain.Size{Cols: 80, Rows: 24}, Cwd: "/untrusted/cwd",29 Size: domain.Size{Cols: 80, Rows: 24}, Cwd: "/untrusted/cwd",
30- Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,30+ Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
31 }31 }
32 _, _, err := d.route(hello, &closeTrackingTransport{})32 _, _, err := d.route(hello, &closeTrackingTransport{})
33 var protocolErr *protoErr33 var protocolErr *protoErr
34 require.ErrorAs(t, err, &protocolErr)34 require.ErrorAs(t, err, &protocolErr)
35- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)35+ require.Equal(t, protocol.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()
@@ -45,13 +45,13 @@ func TestDaemonOwnedStoppedAttachUsesDaemonEnvironment(t *testing.T) {
45 d := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})45 d := newTestDaemon(t, newFactory(t, newQuietPTY()), stubClock{})
46 d.baseEnv = []string{"DAEMON=owned"}46 d.baseEnv = []string{"DAEMON=owned"}
47 d.mu.Lock()47 d.mu.Lock()
48- d.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", tabNames: []string{"main"}, state: ports.SessionDown}48+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/remote/work", tabNames: []string{"main"}, state: protocol.SessionDown}
49 d.mu.Unlock()49 d.mu.Unlock()
50 50
51- hello := ports.Hello{51+ hello := protocol.Hello{
52- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",52+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
53 Size: domain.Size{Cols: 80, Rows: 24}, Cwd: "/untrusted/cwd",53 Size: domain.Size{Cols: 80, Rows: 24}, Cwd: "/untrusted/cwd",
54- Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,54+ Env: []string{"UNTRUSTED=client"}, EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
55 }55 }
56 sess, ac, err := d.routeWithContext(context.Background(), hello, &closeTrackingTransport{})56 sess, ac, err := d.routeWithContext(context.Background(), hello, &closeTrackingTransport{})
57 require.NoError(t, err)57 require.NoError(t, err)
@@ -59,7 +59,7 @@ func TestDaemonOwnedStoppedAttachUsesDaemonEnvironment(t *testing.T) {
59 sess.mu.Lock()59 sess.mu.Lock()
60 require.Equal(t, []string{"DAEMON=owned"}, sess.env)60 require.Equal(t, []string{"DAEMON=owned"}, sess.env)
61 sess.mu.Unlock()61 sess.mu.Unlock()
62- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))62+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
63 }63 }
64 64
65 func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {65 func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
@@ -67,7 +67,7 @@ func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
67 lifecycle := remoteLifecycleForTest()67 lifecycle := remoteLifecycleForTest()
68 d.mu.Lock()68 d.mu.Lock()
69 d.inactive["work"] = inactiveSession{69 d.inactive["work"] = inactiveSession{
70- name: "work", incarnation: lifecycle, state: ports.SessionDown,70+ name: "work", incarnation: lifecycle, state: protocol.SessionDown,
71 tabNames: []string{"main"},71 tabNames: []string{"main"},
72 tabRecords: []domain.CatalogueTabRecord{{StableID: "tab-1", Name: "main"}},72 tabRecords: []domain.CatalogueTabRecord{{StableID: "tab-1", Name: "main"}},
73 }73 }
@@ -77,15 +77,15 @@ func TestRouteRemoteTargetRejectsLiveIntentForInactiveSession(t *testing.T) {
77 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,77 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,
78 SessionName: "work", LiveTabID: "tab-1",78 SessionName: "work", LiveTabID: "tab-1",
79 }79 }
80- _, _, err := d.routeWithContext(context.Background(), ports.Hello{80+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
81- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",81+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
82 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,82 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
83- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,83+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
84 }, &closeTrackingTransport{})84 }, &closeTrackingTransport{})
85 85
86 var protocolErr *protoErr86 var protocolErr *protoErr
87 require.ErrorAs(t, err, &protocolErr)87 require.ErrorAs(t, err, &protocolErr)
88- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)88+ require.Equal(t, protocol.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)
@@ -98,9 +98,9 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
98 name string98 name string
99 entry inactiveSession99 entry inactiveSession
100 }{100 }{
101- {name: "broken", entry: inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionBroken}},101+ {name: "broken", entry: inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionBroken}},
102- {name: "degraded", entry: inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionDown, record: domain.CatalogueRecord{Name: "work", DegradedReason: "checkpoint unavailable"}}},102+ {name: "degraded", entry: inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionDown, record: domain.CatalogueRecord{Name: "work", DegradedReason: "checkpoint unavailable"}}},
103- {name: "purging", entry: inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionDown, purging: true}},103+ {name: "purging", entry: inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionDown, purging: true}},
104 } {104 } {
105 t.Run(test.name, func(t *testing.T) {105 t.Run(test.name, func(t *testing.T) {
106 d := newTestDaemon(t, nil, stubClock{})106 d := newTestDaemon(t, nil, stubClock{})
@@ -109,15 +109,15 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
109 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,109 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,
110 SessionName: "work", Stopped: true,110 SessionName: "work", Stopped: true,
111 }111 }
112- _, _, err := d.routeWithContext(context.Background(), ports.Hello{112+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
113- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",113+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
114 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,114 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
115- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,115+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
116 }, &closeTrackingTransport{})116 }, &closeTrackingTransport{})
117 117
118 var protocolErr *protoErr118 var protocolErr *protoErr
119 require.ErrorAs(t, err, &protocolErr)119 require.ErrorAs(t, err, &protocolErr)
120- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)120+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
121 })121 })
122 }122 }
123 }123 }
@@ -125,22 +125,22 @@ func TestRouteRemoteTargetMapsUnavailableInactiveStatesToNoSuchTarget(t *testing
125 func TestRouteRemoteTargetMapsConcurrentInactiveResumeToNoSuchTarget(t *testing.T) {125 func TestRouteRemoteTargetMapsConcurrentInactiveResumeToNoSuchTarget(t *testing.T) {
126 d := newTestDaemon(t, nil, stubClock{})126 d := newTestDaemon(t, nil, stubClock{})
127 lifecycle := remoteLifecycleForTest()127 lifecycle := remoteLifecycleForTest()
128- d.inactive["work"] = inactiveSession{name: "work", incarnation: lifecycle, state: ports.SessionDown}128+ d.inactive["work"] = inactiveSession{name: "work", incarnation: lifecycle, state: protocol.SessionDown}
129 d.creating["work"] = struct{}{}129 d.creating["work"] = struct{}{}
130 target := domain.RemoteSessionTarget{130 target := domain.RemoteSessionTarget{
131 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,131 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle,
132 SessionName: "work", Stopped: true,132 SessionName: "work", Stopped: true,
133 }133 }
134 134
135- _, _, err := d.routeWithContext(context.Background(), ports.Hello{135+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
136- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",136+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
137 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,137 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
138- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,138+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
139 }, &closeTrackingTransport{})139 }, &closeTrackingTransport{})
140 140
141 var protocolErr *protoErr141 var protocolErr *protoErr
142 require.ErrorAs(t, err, &protocolErr)142 require.ErrorAs(t, err, &protocolErr)
143- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)143+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
144 }144 }
145 145
146 func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {146 func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
@@ -151,10 +151,10 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
151 }151 }
152 }152 }
153 route := func(d *Daemon, target domain.RemoteSessionTarget) error {153 route := func(d *Daemon, target domain.RemoteSessionTarget) error {
154- _, _, err := d.routeWithContext(context.Background(), ports.Hello{154+ _, _, err := d.routeWithContext(context.Background(), protocol.Hello{
155- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: target.SessionName,155+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: target.SessionName,
156 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,156 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
157- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,157+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
158 }, &closeTrackingTransport{})158 }, &closeTrackingTransport{})
159 return err159 return err
160 }160 }
@@ -168,7 +168,7 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
168 record.Committed = nil168 record.Committed = nil
169 record.TabNames = nil169 record.TabNames = nil
170 record.TabRecords = nil170 record.TabRecords = nil
171- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)171+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
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)
@@ -187,7 +187,7 @@ func TestRouteRemoteTargetPreservesInactiveResumeFailures(t *testing.T) {
187 d.catalogue = recordErrorCatalogue{durableRecoveryCatalogue: catalogue, err: cause}187 d.catalogue = recordErrorCatalogue{durableRecoveryCatalogue: catalogue, err: cause}
188 d.persistEnabled = true188 d.persistEnabled = true
189 d.recovery = recoveryusecase.NewCoordinator(d.catalogue, noOpSnapshotRepository{}, nil)189 d.recovery = recoveryusecase.NewCoordinator(d.catalogue, noOpSnapshotRepository{}, nil)
190- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)190+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
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)
@@ -224,17 +224,17 @@ func TestRouteRemoteTargetResumesCanonicalPersistedTabMetadata(t *testing.T) {
224 d.catalogue = catalogue224 d.catalogue = catalogue
225 d.persistEnabled = true225 d.persistEnabled = true
226 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)226 d.recovery = recoveryusecase.NewCoordinator(catalogue, noOpSnapshotRepository{}, nil)
227- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionDown, nil)227+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
228 228
229 target := domain.RemoteSessionTarget{229 target := domain.RemoteSessionTarget{
230 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: record.IncarnationID,230 Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: record.IncarnationID,
231 SessionName: record.Name, Stopped: true, StoppedTab: test.selector,231 SessionName: record.Name, Stopped: true, StoppedTab: test.selector,
232 }232 }
233 transport, _ := newCapturingTransport(t)233 transport, _ := newCapturingTransport(t)
234- sess, attachment, err := d.routeWithContext(context.Background(), ports.Hello{234+ sess, attachment, err := d.routeWithContext(context.Background(), protocol.Hello{
235- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: record.Name,235+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: record.Name,
236 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,236 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
237- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,237+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
238 }, transport)238 }, transport)
239 require.NoError(t, err)239 require.NoError(t, err)
240 t.Cleanup(func() { d.clientGone(sess, attachment, transport, false) })240 t.Cleanup(func() { d.clientGone(sess, attachment, transport, false) })
@@ -249,7 +249,7 @@ func TestRouteRemoteTargetRestoresStoppedStableTabAndOwnsEnvironment(t *testing.
249 lifecycle := remoteLifecycleForTest()249 lifecycle := remoteLifecycleForTest()
250 d.mu.Lock()250 d.mu.Lock()
251 d.inactive["work"] = inactiveSession{251 d.inactive["work"] = inactiveSession{
252- name: "work", cwd: "/remote/work", incarnation: lifecycle, state: ports.SessionDown,252+ name: "work", cwd: "/remote/work", incarnation: lifecycle, state: protocol.SessionDown,
253 tabNames: []string{"alpha", "beta"},253 tabNames: []string{"alpha", "beta"},
254 tabRecords: []domain.CatalogueTabRecord{254 tabRecords: []domain.CatalogueTabRecord{
255 {StableID: "tab-a", Name: "alpha"},255 {StableID: "tab-a", Name: "alpha"},
@@ -264,10 +264,10 @@ func TestRouteRemoteTargetRestoresStoppedStableTabAndOwnsEnvironment(t *testing.
264 StoppedTab: domain.NewStableTabSelector("tab-b"),264 StoppedTab: domain.NewStableTabSelector("tab-b"),
265 }265 }
266 tr, _ := newCapturingTransport(t)266 tr, _ := newCapturingTransport(t)
267- hello := ports.Hello{267+ hello := protocol.Hello{
268- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",268+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
269 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,269 Size: domain.Size{Cols: 80, Rows: 24}, RemoteTarget: &target,
270- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,270+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
271 Env: []string{"VEV_REMOTE_PICKER_SENTINEL=untrusted"},271 Env: []string{"VEV_REMOTE_PICKER_SENTINEL=untrusted"},
272 }272 }
273 sess, ac, err := d.routeWithContext(context.Background(), hello, tr)273 sess, ac, err := d.routeWithContext(context.Background(), hello, tr)
M internal/usecase/daemon/render_coordinator_test.go +2 -2
@@ -46,7 +46,7 @@ func newCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
46 }46 }
47 clk.clock.EXPECT().Now().Return(time.Time{}).Maybe()47 clk.clock.EXPECT().Now().Return(time.Time{}).Maybe()
48 clk.clock.EXPECT().NewTimer(mock.MatchedBy(func(d time.Duration) bool {48 clk.clock.EXPECT().NewTimer(mock.MatchedBy(func(d time.Duration) bool {
49- return d == ports.HandshakeTimeout ||49+ return d == protocol.HandshakeTimeout ||
50 d == urgentRenderDeadline ||50 d == urgentRenderDeadline ||
51 (d >= minOutputRenderDeadline && d <= maxOutputRenderDeadline) ||51 (d >= minOutputRenderDeadline && d <= maxOutputRenderDeadline) ||
52 d == maxSyncUpdateDuration ||52 d == maxSyncUpdateDuration ||
@@ -66,7 +66,7 @@ func newCoordinatorMockClock(t *testing.T, capacity int) *coordinatorMockClock {
66 timer.mock.EXPECT().Stop().Run(func() {66 timer.mock.EXPECT().Stop().Run(func() {
67 timer.stopOnce.Do(func() { close(timer.stopped) })67 timer.stopOnce.Do(func() { close(timer.stopped) })
68 }).Maybe().Return(true)68 }).Maybe().Return(true)
69- if d == ports.HandshakeTimeout {69+ if d == protocol.HandshakeTimeout {
70 clk.handshakeTimers <- timer70 clk.handshakeTimers <- timer
71 } else {71 } else {
72 clk.timers <- timer72 clk.timers <- timer
M internal/usecase/daemon/render_test.go +4 -3
@@ -21,6 +21,7 @@ import (
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 portsmocks "github.com/bnema/vev/internal/ports/mocks"23 portsmocks "github.com/bnema/vev/internal/ports/mocks"
24+ "github.com/bnema/vev/internal/protocol"
24 scopy "github.com/bnema/vev/internal/usecase/copy"25 scopy "github.com/bnema/vev/internal/usecase/copy"
25 "github.com/bnema/vev/internal/usecase/layout"26 "github.com/bnema/vev/internal/usecase/layout"
26 themeui "github.com/bnema/vev/internal/usecase/theme"27 themeui "github.com/bnema/vev/internal/usecase/theme"
@@ -591,7 +592,7 @@ func TestAltXClosesFinalTabAndDetaches(t *testing.T) {
591 f := awaitFrame(t, sends, ports.MsgDetached)592 f := awaitFrame(t, sends, ports.MsgDetached)
592 det, err := ports.UnmarshalDetached(f.Payload)593 det, err := ports.UnmarshalDetached(f.Payload)
593 require.NoError(t, err)594 require.NoError(t, err)
594- require.Equal(t, ports.ReasonSessionKilled, det.Reason)595+ require.Equal(t, protocol.ReasonSessionKilled, det.Reason)
595 }596 }
596 597
597 func TestPTYEOFClosesActiveNonFinalTabAndRepaintsRemaining(t *testing.T) {598 func TestPTYEOFClosesActiveNonFinalTabAndRepaintsRemaining(t *testing.T) {
@@ -657,7 +658,7 @@ func TestPTYEOFFinalTabKillsSessionAndDetaches(t *testing.T) {
657 f := awaitFrame(t, sends, ports.MsgDetached)658 f := awaitFrame(t, sends, ports.MsgDetached)
658 det, err := ports.UnmarshalDetached(f.Payload)659 det, err := ports.UnmarshalDetached(f.Payload)
659 require.NoError(t, err)660 require.NoError(t, err)
660- require.Equal(t, ports.ReasonSessionKilled, det.Reason)661+ require.Equal(t, protocol.ReasonSessionKilled, det.Reason)
661 662
662 d.sessWg.Wait()663 d.sessWg.Wait()
663 }664 }
@@ -986,7 +987,7 @@ func TestSendErrorKeepsEphemeralHeadless(t *testing.T) {
986 require.Empty(t, sess.snapshotAttachmentsLocked())987 require.Empty(t, sess.snapshotAttachmentsLocked())
987 sess.mu.Unlock()988 sess.mu.Unlock()
988 989
989- _ = d.killSession(sess, ports.ReasonServerShutdown, false)990+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
990 cancel()991 cancel()
991 d.waitNotifies()992 d.waitNotifies()
992 }993 }
M internal/usecase/daemon/resize_mode_test.go +5 -5
@@ -5,7 +5,7 @@ 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/ports"8+ "github.com/bnema/vev/internal/protocol"
9 "github.com/bnema/vev/internal/usecase/layout"9 "github.com/bnema/vev/internal/usecase/layout"
10 "github.com/stretchr/testify/require"10 "github.com/stretchr/testify/require"
11 )11 )
@@ -56,12 +56,12 @@ func TestResizeControlHeadlessAndErrors(t *testing.T) {
56 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })56 t.Cleanup(func() { factory.close(); d.sessWg.Wait() })
57 sess := addControlSession(d, "work", "t_work", "p_work")57 sess := addControlSession(d, "work", "t_work", "p_work")
58 58
59- tooEarly := sendCommand(t, d, ports.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})59+ tooEarly := sendCommand(t, d, protocol.CommandRequest{Slug: "grow-pane-width", TargetSession: "work"})
60 require.False(t, tooEarly.OK)60 require.False(t, tooEarly.OK)
61- require.Equal(t, ports.ErrNoSuchTarget, tooEarly.Code)61+ require.Equal(t, protocol.ErrNoSuchTarget, tooEarly.Code)
62 require.Equal(t, "pane is not in a split", tooEarly.Text)62 require.Equal(t, "pane is not in a split", tooEarly.Text)
63 63
64- require.True(t, sendCommand(t, d, ports.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)64+ require.True(t, sendCommand(t, d, protocol.CommandRequest{Slug: "split-right", TargetSession: "work"}).OK)
65 tb := testAttachmentTab(sess)65 tb := testAttachmentTab(sess)
66 tb.mu.Lock()66 tb.mu.Lock()
67 beforeFocus := tb.tree.Focus67 beforeFocus := tb.tree.Focus
@@ -87,7 +87,7 @@ func TestResizeControlHeadlessAndErrors(t *testing.T) {
87 require.Equal(t, beforeGeneration+1, tb.layoutGeneration)87 require.Equal(t, beforeGeneration+1, tb.layoutGeneration)
88 tb.mu.Unlock()88 tb.mu.Unlock()
89 89
90- equalized := sendCommand(t, d, ports.CommandRequest{Slug: "equalize-panes", TargetSession: "work"})90+ equalized := sendCommand(t, d, protocol.CommandRequest{Slug: "equalize-panes", TargetSession: "work"})
91 require.True(t, equalized.OK, equalized.Text)91 require.True(t, equalized.OK, equalized.Text)
92 require.Empty(t, equalized.Output)92 require.Empty(t, equalized.Output)
93 }93 }
M internal/usecase/daemon/resume.go +17 -16
@@ -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 var errResumeTokenLifecycleRace = errors.New("resume token lifecycle race")13 var errResumeTokenLifecycleRace = errors.New("resume token lifecycle race")
@@ -164,7 +165,7 @@ func (d *Daemon) purgeParkingForSessionLocked(sess *session) {
164 // waitParkingInFlight waits for an identified same-client parking publication165 // waitParkingInFlight waits for an identified same-client parking publication
165 // to finish. Returns true when a matching in-flight entry was observed.166 // to finish. Returns true when a matching in-flight entry was observed.
166 // Unknown and mismatched credentials return false without waiting.167 // Unknown and mismatched credentials return false without waiting.
167-func (d *Daemon) waitParkingInFlight(h ports.Hello) bool {168+func (d *Daemon) waitParkingInFlight(h protocol.Hello) bool {
168 if h.ResumeToken == 0 {169 if h.ResumeToken == 0 {
169 return false170 return false
170 }171 }
@@ -479,11 +480,11 @@ func (d *Daemon) finishParkedAttachmentRetirements(retirements []parkedAttachmen
479 // to the named session's active attachment because transport teardown has not480 // to the named session's active attachment because transport teardown has not
480 // parked it yet. Only the exact live owner (same session, token, and client ID)481 // parked it yet. Only the exact live owner (same session, token, and client ID)
481 // is accepted; arbitrary unknown tokens stay fail-closed.482 // is accepted; arbitrary unknown tokens stay fail-closed.
482-func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {483+func (d *Daemon) resumeLiveAttachment(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
483 d.mu.Lock()484 d.mu.Lock()
484 if d.closing {485 if d.closing {
485 d.mu.Unlock()486 d.mu.Unlock()
486- return nil, nil, false, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}487+ return nil, nil, false, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
487 }488 }
488 // Teardown may have parked the credential already.489 // Teardown may have parked the credential already.
489 if d.parked[h.ResumeToken] != nil {490 if d.parked[h.ResumeToken] != nil {
@@ -521,14 +522,14 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma
521 }522 }
522 if credentialMatch && h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) {523 if credentialMatch && h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) {
523 d.mu.Unlock()524 d.mu.Unlock()
524- return nil, nil, false, &protoErr{ports.ErrNoSuchTarget, "remote target no longer matches resumed session"}525+ return nil, nil, false, &protoErr{protocol.ErrNoSuchTarget, "remote target no longer matches resumed session"}
525 }526 }
526 d.mu.Unlock()527 d.mu.Unlock()
527 528
528 if clientMismatch {529 if clientMismatch {
529 reason := "client id mismatch"530 reason := "client id mismatch"
530 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason)531 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason)
531- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}532+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
532 }533 }
533 if !credentialMatch {534 if !credentialMatch {
534 // Detach may have won and not published parked yet; wait out that gap535 // Detach may have won and not published parked yet; wait out that gap
@@ -569,9 +570,9 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma
569 closing := d.closing570 closing := d.closing
570 d.mu.Unlock()571 d.mu.Unlock()
571 if closing {572 if closing {
572- return nil, nil, false, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}573+ return nil, nil, false, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
573 }574 }
574- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}575+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
575 }576 }
576 _ = ac.closeCapturedTransport(oldSnap.transport)577 _ = ac.closeCapturedTransport(oldSnap.transport)
577 d.log.Info("live resume credential parked for reconnect", "session", sess.nameSnapshot())578 d.log.Info("live resume credential parked for reconnect", "session", sess.nameSnapshot())
@@ -582,7 +583,7 @@ func (d *Daemon) resumeLiveAttachment(h ports.Hello, tr ports.Transport, sz doma
582 // registry lock, preserving the global sendMu > Daemon.mu ordering. The parked583 // registry lock, preserving the global sendMu > Daemon.mu ordering. The parked
583 // entry is revalidated after both locks are held because it may expire between584 // entry is revalidated after both locks are held because it may expire between
584 // the initial lookup and lock acquisition.585 // the initial lookup and lock acquisition.
585-func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {586+func (d *Daemon) resumeParked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
586 d.mu.Lock()587 d.mu.Lock()
587 parked := d.parked[h.ResumeToken]588 parked := d.parked[h.ResumeToken]
588 d.mu.Unlock()589 d.mu.Unlock()
@@ -599,7 +600,7 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size)
599 if d.closing {600 if d.closing {
600 d.mu.Unlock()601 d.mu.Unlock()
601 ac.sendMu.Unlock()602 ac.sendMu.Unlock()
602- return nil, nil, false, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}603+ return nil, nil, false, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
603 }604 }
604 if d.parked[h.ResumeToken] != parked || parked.claimed {605 if d.parked[h.ResumeToken] != parked || parked.claimed {
605 if d.sessions[parked.sess.id] == parked.sess {606 if d.sessions[parked.sess.id] == parked.sess {
@@ -609,7 +610,7 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size)
609 }610 }
610 d.mu.Unlock()611 d.mu.Unlock()
611 ac.sendMu.Unlock()612 ac.sendMu.Unlock()
612- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}613+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
613 }614 }
614 sess, resumed, ok, err := d.resumeParkedLocked(h, tr, sz)615 sess, resumed, ok, err := d.resumeParkedLocked(h, tr, sz)
615 d.mu.Unlock()616 d.mu.Unlock()
@@ -631,7 +632,7 @@ func (d *Daemon) resumeParked(h ports.Hello, tr ports.Transport, sz domain.Size)
631 632
632 // resumeParkedLocked completes a validated resume. Caller holds both ac.sendMu633 // resumeParkedLocked completes a validated resume. Caller holds both ac.sendMu
633 // and d.mu in that order.634 // and d.mu in that order.
634-func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {635+func (d *Daemon) resumeParkedLocked(h protocol.Hello, tr ports.Transport, sz domain.Size) (*session, *attachedClient, bool, error) {
635 parked := d.parked[h.ResumeToken]636 parked := d.parked[h.ResumeToken]
636 if parked == nil || h.ResumeToken == 0 {637 if parked == nil || h.ResumeToken == 0 {
637 return nil, nil, false, nil638 return nil, nil, false, nil
@@ -641,7 +642,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
641 if !registered {642 if !registered {
642 d.removeParkedLocked(h.ResumeToken, parked)643 d.removeParkedLocked(h.ResumeToken, parked)
643 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "registered", false)644 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "registered", false)
644- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}645+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
645 }646 }
646 ac := parked.ac647 ac := parked.ac
647 if parked.claimed {648 if parked.claimed {
@@ -650,10 +651,10 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
650 if ac.clientID != h.ClientID {651 if ac.clientID != h.ClientID {
651 reason := "client id mismatch"652 reason := "client id mismatch"
652 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason)653 d.log.Warn("resume rejected", "session", sess.nameSnapshot(), "err", reason)
653- return nil, nil, false, &protoErr{ports.ErrNoSuchSession, "resume token is no longer valid"}654+ return nil, nil, false, &protoErr{protocol.ErrNoSuchSession, "resume token is no longer valid"}
654 }655 }
655 if h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) {656 if h.RemoteTarget != nil && !d.remoteTargetMatchesSessionLocked(sess, *h.RemoteTarget) {
656- return nil, nil, false, &protoErr{ports.ErrNoSuchTarget, "remote target no longer matches resumed session"}657+ return nil, nil, false, &protoErr{protocol.ErrNoSuchTarget, "remote target no longer matches resumed session"}
657 }658 }
658 // Claim the attachment while retaining the old credential until Welcome is659 // Claim the attachment while retaining the old credential until Welcome is
659 // accepted. A failed handshake can therefore restore this exact parked660 // accepted. A failed handshake can therefore restore this exact parked
@@ -680,7 +681,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
680 // The resumed session's snapshot is the sole source for future PTY children.681 // The resumed session's snapshot is the sole source for future PTY children.
681 // Existing PTYs retain the environment they were started with.682 // Existing PTYs retain the environment they were started with.
682 sess.mu.Lock()683 sess.mu.Lock()
683- if h.EnvironmentPolicy != ports.EnvironmentPolicyDaemonOwned {684+ if h.EnvironmentPolicy != protocol.EnvironmentPolicyDaemonOwned {
684 sess.env = copyEnvironment(h.Env)685 sess.env = copyEnvironment(h.Env)
685 }686 }
686 sess.mu.Unlock()687 sess.mu.Unlock()
@@ -702,7 +703,7 @@ func (d *Daemon) resumeParkedLocked(h ports.Hello, tr ports.Transport, sz domain
702 ac.sendMu.Lock()703 ac.sendMu.Lock()
703 d.mu.Lock()704 d.mu.Lock()
704 if err != nil {705 if err != nil {
705- return sess, ac, false, &protoErr{ports.ErrInternal, "resume attachment transition failed"}706+ return sess, ac, false, &protoErr{protocol.ErrInternal, "resume attachment transition failed"}
706 }707 }
707 d.deferAttachmentTransitionCleanups(transition)708 d.deferAttachmentTransitionCleanups(transition)
708 d.touchMRU(sess)709 d.touchMRU(sess)
M internal/usecase/daemon/resume_test.go +118 -117
@@ -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/layout"19 "github.com/bnema/vev/internal/usecase/layout"
19 )20 )
20 21
@@ -85,9 +86,9 @@ func (t *closeCountingBlockedTransport) CloseCount() int {
85 return t.count86 return t.count
86 }87 }
87 88
88-func helloResumeCapable(intent uint8, name string, token uint64) ports.Hello {89+func helloResumeCapable(intent uint8, name string, token uint64) protocol.Hello {
89- return ports.Hello{90+ return protocol.Hello{
90- Version: ports.ProtocolVersion,91+ Version: protocol.Version,
91 Intent: intent,92 Intent: intent,
92 ClientID: [16]byte{1, 2, 3, 4},93 ClientID: [16]byte{1, 2, 3, 4},
93 ResumeToken: token,94 ResumeToken: token,
@@ -100,10 +101,10 @@ func helloResumeCapable(intent uint8, name string, token uint64) ports.Hello {
100 func TestTokenlessResumeIsRejectedBeforeNameRouting(t *testing.T) {101 func TestTokenlessResumeIsRejectedBeforeNameRouting(t *testing.T) {
101 d := newTestDaemon(t, nil, stubClock{})102 d := newTestDaemon(t, nil, stubClock{})
102 tr, _ := newCapturingTransport(t)103 tr, _ := newCapturingTransport(t)
103- _, _, err := d.route(helloResumeCapable(ports.IntentResume, "work", 0), tr)104+ _, _, err := d.route(helloResumeCapable(protocol.IntentResume, "work", 0), tr)
104 var protocolErr *protoErr105 var protocolErr *protoErr
105 require.ErrorAs(t, err, &protocolErr)106 require.ErrorAs(t, err, &protocolErr)
106- require.Equal(t, ports.ErrNoSuchSession, protocolErr.code)107+ require.Equal(t, protocol.ErrNoSuchSession, protocolErr.code)
107 d.mu.Lock()108 d.mu.Lock()
108 sessionCount := len(d.sessions)109 sessionCount := len(d.sessions)
109 d.mu.Unlock()110 d.mu.Unlock()
@@ -115,27 +116,27 @@ func TestResumeRejectsMismatchedRemoteTargetBeforeOwnershipMutation(t *testing.T
115 defer release()116 defer release()
116 d := newTestDaemon(t, newFactory(t, pty), stubClock{})117 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
117 oldTransport := &closeTrackingTransport{}118 oldTransport := &closeTrackingTransport{}
118- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)119+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
119 require.NoError(t, err)120 require.NoError(t, err)
120 token := ac.resumeToken121 token := ac.resumeToken
121 122
122 target := domain.RemoteSessionTarget{123 target := domain.RemoteSessionTarget{
123 LifecycleID: sess.incarnation, SessionName: sess.name, LiveTabID: "missing-tab",124 LifecycleID: sess.incarnation, SessionName: sess.name, LiveTabID: "missing-tab",
124 }125 }
125- resume := helloResumeCapable(ports.IntentResume, "work", token)126+ resume := helloResumeCapable(protocol.IntentResume, "work", token)
126 resume.RemoteTarget = &target127 resume.RemoteTarget = &target
127- resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned128+ resume.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
128 _, _, err = d.route(resume, &closeTrackingTransport{})129 _, _, err = d.route(resume, &closeTrackingTransport{})
129 var protocolErr *protoErr130 var protocolErr *protoErr
130 require.ErrorAs(t, err, &protocolErr)131 require.ErrorAs(t, err, &protocolErr)
131- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)132+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
132 require.Same(t, oldTransport, ac.transport())133 require.Same(t, oldTransport, ac.transport())
133 d.mu.Lock()134 d.mu.Lock()
134 _, parked := d.parked[token]135 _, parked := d.parked[token]
135 d.mu.Unlock()136 d.mu.Unlock()
136 require.False(t, parked, "mismatched target must not park the live attachment")137 require.False(t, parked, "mismatched target must not park the live attachment")
137 138
138- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))139+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
139 }140 }
140 141
141 func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {142 func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
@@ -143,7 +144,7 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
143 defer release()144 defer release()
144 d := newTestDaemon(t, newFactory(t, pty), stubClock{})145 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
145 oldTransport := &closeTrackingTransport{}146 oldTransport := &closeTrackingTransport{}
146- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)147+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
147 require.NoError(t, err)148 require.NoError(t, err)
148 token := ac.resumeToken149 token := ac.resumeToken
149 d.clientGone(sess, ac, oldTransport, false)150 d.clientGone(sess, ac, oldTransport, false)
@@ -151,13 +152,13 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
151 target := domain.RemoteSessionTarget{152 target := domain.RemoteSessionTarget{
152 LifecycleID: sess.incarnation, SessionName: sess.name, LiveTabID: "missing-tab",153 LifecycleID: sess.incarnation, SessionName: sess.name, LiveTabID: "missing-tab",
153 }154 }
154- resume := helloResumeCapable(ports.IntentResume, "work", token)155+ resume := helloResumeCapable(protocol.IntentResume, "work", token)
155 resume.RemoteTarget = &target156 resume.RemoteTarget = &target
156- resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned157+ resume.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
157 _, _, err = d.route(resume, &closeTrackingTransport{})158 _, _, err = d.route(resume, &closeTrackingTransport{})
158 var protocolErr *protoErr159 var protocolErr *protoErr
159 require.ErrorAs(t, err, &protocolErr)160 require.ErrorAs(t, err, &protocolErr)
160- require.Equal(t, ports.ErrNoSuchTarget, protocolErr.code)161+ require.Equal(t, protocol.ErrNoSuchTarget, protocolErr.code)
161 d.mu.Lock()162 d.mu.Lock()
162 parked := d.parked[token]163 parked := d.parked[token]
163 claimed := parked != nil && parked.claimed164 claimed := parked != nil && parked.claimed
@@ -165,7 +166,7 @@ func TestParkedResumeRejectsMismatchedRemoteTargetBeforeClaim(t *testing.T) {
165 require.NotNil(t, parked)166 require.NotNil(t, parked)
166 require.False(t, claimed, "mismatched target must not claim the parked attachment")167 require.False(t, claimed, "mismatched target must not claim the parked attachment")
167 168
168- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))169+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
169 }170 }
170 171
171 func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {172 func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
@@ -175,7 +176,7 @@ func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
175 d := newTestDaemon(t, newFactory(t, pty), clock)176 d := newTestDaemon(t, newFactory(t, pty), clock)
176 177
177 oldTransport := &closeTrackingTransport{}178 oldTransport := &closeTrackingTransport{}
178- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)179+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
179 require.NoError(t, err)180 require.NoError(t, err)
180 token := ac.resumeToken181 token := ac.resumeToken
181 d.clientGone(sess, ac, oldTransport, false)182 d.clientGone(sess, ac, oldTransport, false)
@@ -185,7 +186,7 @@ func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
185 d.mu.Unlock()186 d.mu.Unlock()
186 require.NotNil(t, parked)187 require.NotNil(t, parked)
187 188
188- resumed, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)189+ resumed, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, defaultSize)
189 require.NoError(t, err)190 require.NoError(t, err)
190 require.True(t, ok)191 require.True(t, ok)
191 require.Same(t, sess, resumed)192 require.Same(t, sess, resumed)
@@ -212,7 +213,7 @@ func TestFailedResumeRearmsParkExpiryAfterClaimTimerFires(t *testing.T) {
212 d.mu.Unlock()213 d.mu.Unlock()
213 require.False(t, retained, "rearmed credential must still expire")214 require.False(t, retained, "rearmed credential must still expire")
214 215
215- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))216+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
216 d.sessWg.Wait()217 d.sessWg.Wait()
217 }218 }
218 219
@@ -221,8 +222,8 @@ func TestNamedLinkLossParks(t *testing.T) {
221 defer release()222 defer release()
222 d := newTestDaemon(t, newFactory(t, pty), stubClock{})223 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
223 224
224- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))225+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
225- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)226+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
226 require.NoError(t, err)227 require.NoError(t, err)
227 token := ac.resumeToken228 token := ac.resumeToken
228 require.NotZero(t, token)229 require.NotZero(t, token)
@@ -241,7 +242,7 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
241 defer release()242 defer release()
242 d := newTestDaemon(t, newFactory(t, pty), stubClock{})243 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
243 oldTransport := &closeTrackingTransport{}244 oldTransport := &closeTrackingTransport{}
244- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "bounded-send-resume", 0), oldTransport)245+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "bounded-send-resume", 0), oldTransport)
245 require.NoError(t, err)246 require.NoError(t, err)
246 token := ac.resumeToken247 token := ac.resumeToken
247 d.clientGone(sess, ac, oldTransport, false)248 d.clientGone(sess, ac, oldTransport, false)
@@ -259,7 +260,7 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
259 go func() {260 go func() {
260 transport, sendErr := d.boundedSendWithTimeout(time.Second, expected.transport, func() error {261 transport, sendErr := d.boundedSendWithTimeout(time.Second, expected.transport, func() error {
261 defer close(orphanDone)262 defer close(orphanDone)
262- return ac.sendExpectedTransport(expected, frameDetached(ports.ReasonDetach))263+ return ac.sendExpectedTransport(expected, frameDetached(protocol.ReasonDetach))
263 })264 })
264 result <- boundedResult{transport: transport, err: sendErr}265 result <- boundedResult{transport: transport, err: sendErr}
265 }()266 }()
@@ -276,7 +277,7 @@ func TestBoundedSendTimeoutCannotTargetResumedTransport(t *testing.T) {
276 newTransport := &closeTrackingTransport{}277 newTransport := &closeTrackingTransport{}
277 d.mu.Lock()278 d.mu.Lock()
278 resumedSess, resumedAC, ok, err := d.resumeParkedLocked(279 resumedSess, resumedAC, ok, err := d.resumeParkedLocked(
279- helloResumeCapable(ports.IntentResume, sess.name, token),280+ helloResumeCapable(protocol.IntentResume, sess.name, token),
280 newTransport,281 newTransport,
281 domain.Size{Cols: 80, Rows: 24},282 domain.Size{Cols: 80, Rows: 24},
282 )283 )
@@ -301,7 +302,7 @@ func TestDetachedNotificationTimeoutClosesCapturedTransportOnce(t *testing.T) {
301 d.notifyDetachedSnapshotAsync(detachedAttachmentSnapshot{302 d.notifyDetachedSnapshotAsync(detachedAttachmentSnapshot{
302 ac: ac,303 ac: ac,
303 transport: ac.transportSnapshot(),304 transport: ac.transportSnapshot(),
304- }, ports.ReasonDetach)305+ }, protocol.ReasonDetach)
305 timer := <-clock.timers306 timer := <-clock.timers
306 timer.ch <- time.Time{}307 timer.ch <- time.Time{}
307 d.waitNotifies()308 d.waitNotifies()
@@ -316,14 +317,14 @@ func TestHandleHelloResumeDefersFreshOutputUntilWelcome(t *testing.T) {
316 clock := newCoordinatorMockClock(t, 4)317 clock := newCoordinatorMockClock(t, 4)
317 d := newTestDaemon(t, newFactorySeq(t, pty), clock.clock)318 d := newTestDaemon(t, newFactorySeq(t, pty), clock.clock)
318 oldTransport := &closeTrackingTransport{}319 oldTransport := &closeTrackingTransport{}
319- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "resume-welcome-gate", 0), oldTransport)320+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "resume-welcome-gate", 0), oldTransport)
320 require.NoError(t, err)321 require.NoError(t, err)
321 token := ac.resumeToken322 token := ac.resumeToken
322 d.clientGone(sess, ac, oldTransport, false)323 d.clientGone(sess, ac, oldTransport, false)
323 324
324 tr := newWelcomeBlockingTransport(t)325 tr := newWelcomeBlockingTransport(t)
325 done := make(chan struct{})326 done := make(chan struct{})
326- resumeHello := helloResumeCapable(ports.IntentResume, sess.name, token)327+ resumeHello := helloResumeCapable(protocol.IntentResume, sess.name, token)
327 go func() {328 go func() {
328 d.handleHello(tr.tr, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(resumeHello)})329 d.handleHello(tr.tr, ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(resumeHello)})
329 close(done)330 close(done)
@@ -366,7 +367,7 @@ func TestEphemeralLinkLossParksAndResumes(t *testing.T) {
366 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})367 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
367 368
368 tr := &closeTrackingTransport{}369 tr := &closeTrackingTransport{}
369- initial := helloResumeCapable(ports.IntentEphemeral, "", 0)370+ initial := helloResumeCapable(protocol.IntentEphemeral, "", 0)
370 initial.Size = domain.Size{Cols: 120, Rows: 40}371 initial.Size = domain.Size{Cols: 120, Rows: 40}
371 sess, ac, err := d.route(initial, tr)372 sess, ac, err := d.route(initial, tr)
372 require.NoError(t, err)373 require.NoError(t, err)
@@ -374,7 +375,7 @@ func TestEphemeralLinkLossParksAndResumes(t *testing.T) {
374 token := ac.resumeToken375 token := ac.resumeToken
375 require.NotZero(t, token, "ephemeral sessions receive resume tokens")376 require.NotZero(t, token, "ephemeral sessions receive resume tokens")
376 377
377- peerHello := helloResumeCapable(ports.IntentAttach, sess.name, 0)378+ peerHello := helloResumeCapable(protocol.IntentAttach, sess.name, 0)
378 peerHello.ClientID = [16]byte{9}379 peerHello.ClientID = [16]byte{9}
379 peerHello.Size = domain.Size{Cols: 100, Rows: 30}380 peerHello.Size = domain.Size{Cols: 100, Rows: 30}
380 peer := &closeTrackingTransport{}381 peer := &closeTrackingTransport{}
@@ -404,7 +405,7 @@ func TestEphemeralLinkLossParksAndResumes(t *testing.T) {
404 d.mu.Unlock()405 d.mu.Unlock()
405 require.True(t, parked, "ephemeral resume-capable link loss is parked")406 require.True(t, parked, "ephemeral resume-capable link loss is parked")
406 407
407- resumeHello := helloResumeCapable(ports.IntentResume, sess.name, token)408+ resumeHello := helloResumeCapable(protocol.IntentResume, sess.name, token)
408 resumeHello.Size = domain.Size{Cols: 80, Rows: 24}409 resumeHello.Size = domain.Size{Cols: 80, Rows: 24}
409 newTr := &closeTrackingTransport{}410 newTr := &closeTrackingTransport{}
410 resumedSess, resumedAC, err := d.route(resumeHello, newTr)411 resumedSess, resumedAC, err := d.route(resumeHello, newTr)
@@ -431,7 +432,7 @@ func TestResumeParkedTokenReplacedDuringWaitFailsClosed(t *testing.T) {
431 d := newTestDaemon(t, newFactory(t, pty), stubClock{})432 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
432 433
433 oldTr := &closeTrackingTransport{}434 oldTr := &closeTrackingTransport{}
434- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)435+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
435 require.NoError(t, err)436 require.NoError(t, err)
436 token := ac.resumeToken437 token := ac.resumeToken
437 require.NotZero(t, token)438 require.NotZero(t, token)
@@ -460,7 +461,7 @@ func TestResumeParkedTokenReplacedDuringWaitFailsClosed(t *testing.T) {
460 }461 }
461 result := make(chan routeResult, 1)462 result := make(chan routeResult, 1)
462 go func() {463 go func() {
463- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), resumeTr)464+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), resumeTr)
464 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}465 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
465 }()466 }()
466 awaitTestCompletion(t, reachedLookup, "resumeParked did not pause after the parked lookup")467 awaitTestCompletion(t, reachedLookup, "resumeParked did not pause after the parked lookup")
@@ -480,7 +481,7 @@ func TestResumeParkedTokenReplacedDuringWaitFailsClosed(t *testing.T) {
480 require.Nil(t, got.ac)481 require.Nil(t, got.ac)
481 var pe *protoErr482 var pe *protoErr
482 require.ErrorAs(t, got.err, &pe)483 require.ErrorAs(t, got.err, &pe)
483- require.Equal(t, ports.ErrNoSuchSession, pe.code)484+ require.Equal(t, protocol.ErrNoSuchSession, pe.code)
484 require.Contains(t, pe.Error(), "resume token is no longer valid")485 require.Contains(t, pe.Error(), "resume token is no longer valid")
485 require.Empty(t, sess.snapshotAttachments(), "lifecycle-race resume must not take over the named session")486 require.Empty(t, sess.snapshotAttachments(), "lifecycle-race resume must not take over the named session")
486 require.Empty(t, resumeTr.Sends(), "failed resume must not complete a Welcome handshake")487 require.Empty(t, resumeTr.Sends(), "failed resume must not complete a Welcome handshake")
@@ -503,7 +504,7 @@ func TestResumeLiveAttachmentParkedResumeRaceFailsClosed(t *testing.T) {
503 d := newTestDaemon(t, newFactory(t, pty), stubClock{})504 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
504 505
505 oldTr := &closeTrackingTransport{}506 oldTr := &closeTrackingTransport{}
506- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)507+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
507 require.NoError(t, err)508 require.NoError(t, err)
508 token := ac.resumeToken509 token := ac.resumeToken
509 require.NotZero(t, token)510 require.NotZero(t, token)
@@ -531,7 +532,7 @@ func TestResumeLiveAttachmentParkedResumeRaceFailsClosed(t *testing.T) {
531 }532 }
532 result := make(chan routeResult, 1)533 result := make(chan routeResult, 1)
533 go func() {534 go func() {
534- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), resumeTr)535+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), resumeTr)
535 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}536 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
536 }()537 }()
537 awaitTestCompletion(t, reachedLookup, "live resume did not pause inside resumeParked after parking")538 awaitTestCompletion(t, reachedLookup, "live resume did not pause inside resumeParked after parking")
@@ -554,7 +555,7 @@ func TestResumeLiveAttachmentParkedResumeRaceFailsClosed(t *testing.T) {
554 require.False(t, errors.Is(got.err, errResumeTokenLifecycleRace), "raw lifecycle sentinel must not reach route callers")555 require.False(t, errors.Is(got.err, errResumeTokenLifecycleRace), "raw lifecycle sentinel must not reach route callers")
555 var pe *protoErr556 var pe *protoErr
556 require.ErrorAs(t, got.err, &pe)557 require.ErrorAs(t, got.err, &pe)
557- require.Equal(t, ports.ErrNoSuchSession, pe.code)558+ require.Equal(t, protocol.ErrNoSuchSession, pe.code)
558 require.Contains(t, pe.Error(), "resume token is no longer valid")559 require.Contains(t, pe.Error(), "resume token is no longer valid")
559 require.Empty(t, sess.snapshotAttachments(), "losing live resume must not take over the named session")560 require.Empty(t, sess.snapshotAttachments(), "losing live resume must not take over the named session")
560 require.Empty(t, resumeTr.Sends(), "failed live resume must not complete a Welcome handshake")561 require.Empty(t, resumeTr.Sends(), "failed live resume must not complete a Welcome handshake")
@@ -576,7 +577,7 @@ func TestResumeDuringTeardownBeforeParkRecoversSameAttachment(t *testing.T) {
576 d := newTestDaemon(t, newFactory(t, pty), stubClock{})577 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
577 578
578 oldTr := &closeTrackingTransport{}579 oldTr := &closeTrackingTransport{}
579- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)580+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
580 require.NoError(t, err)581 require.NoError(t, err)
581 token := ac.resumeToken582 token := ac.resumeToken
582 require.NotZero(t, token)583 require.NotZero(t, token)
@@ -608,20 +609,20 @@ func TestResumeDuringTeardownBeforeParkRecoversSameAttachment(t *testing.T) {
608 require.Same(t, sess, parkingInGap.sess)609 require.Same(t, sess, parkingInGap.sess)
609 require.False(t, parkedInGap, "fixture: park must not have published yet")610 require.False(t, parkedInGap, "fixture: park must not have published yet")
610 611
611- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})612+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
612 require.Error(t, err)613 require.Error(t, err)
613 var unknownErr *protoErr614 var unknownErr *protoErr
614 require.ErrorAs(t, err, &unknownErr)615 require.ErrorAs(t, err, &unknownErr)
615- require.Equal(t, ports.ErrNoSuchSession, unknownErr.code)616+ require.Equal(t, protocol.ErrNoSuchSession, unknownErr.code)
616 require.Contains(t, unknownErr.Error(), "resume token is no longer valid")617 require.Contains(t, unknownErr.Error(), "resume token is no longer valid")
617 618
618- wrongClient := helloResumeCapable(ports.IntentResume, "work", token)619+ wrongClient := helloResumeCapable(protocol.IntentResume, "work", token)
619 wrongClient.ClientID = [16]byte{9, 9, 9, 9}620 wrongClient.ClientID = [16]byte{9, 9, 9, 9}
620 _, _, err = d.route(wrongClient, &closeTrackingTransport{})621 _, _, err = d.route(wrongClient, &closeTrackingTransport{})
621 require.Error(t, err)622 require.Error(t, err)
622 var mismatchErr *protoErr623 var mismatchErr *protoErr
623 require.ErrorAs(t, err, &mismatchErr)624 require.ErrorAs(t, err, &mismatchErr)
624- require.Equal(t, ports.ErrNoSuchSession, mismatchErr.code)625+ require.Equal(t, protocol.ErrNoSuchSession, mismatchErr.code)
625 require.Contains(t, mismatchErr.Error(), "resume token is no longer valid")626 require.Contains(t, mismatchErr.Error(), "resume token is no longer valid")
626 627
627 newTr := &closeTrackingTransport{}628 newTr := &closeTrackingTransport{}
@@ -634,7 +635,7 @@ func TestResumeDuringTeardownBeforeParkRecoversSameAttachment(t *testing.T) {
634 d.afterParkingWaitArmed = func() { close(waiterArmed) }635 d.afterParkingWaitArmed = func() { close(waiterArmed) }
635 result := make(chan routeResult, 1)636 result := make(chan routeResult, 1)
636 go func() {637 go func() {
637- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), newTr)638+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), newTr)
638 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}639 result <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
639 }()640 }()
640 awaitTestCompletion(t, waiterArmed, "resume waiter did not arm on parking marker before park")641 awaitTestCompletion(t, waiterArmed, "resume waiter did not arm on parking marker before park")
@@ -675,7 +676,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
675 d := newTestDaemon(t, newFactory(t, pty), stubClock{})676 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
676 677
677 oldTr := &closeTrackingTransport{}678 oldTr := &closeTrackingTransport{}
678- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)679+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
679 require.NoError(t, err)680 require.NoError(t, err)
680 token := ac.resumeToken681 token := ac.resumeToken
681 require.NotZero(t, token)682 require.NotZero(t, token)
@@ -698,7 +699,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
698 firstTr := &closeTrackingTransport{}699 firstTr := &closeTrackingTransport{}
699 firstResult := make(chan routeResult, 1)700 firstResult := make(chan routeResult, 1)
700 go func() {701 go func() {
701- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), firstTr)702+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), firstTr)
702 firstResult <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}703 firstResult <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
703 }()704 }()
704 awaitTestCompletion(t, reachedGap, "first live resume did not pause after detach before park")705 awaitTestCompletion(t, reachedGap, "first live resume did not pause after detach before park")
@@ -712,11 +713,11 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
712 require.Same(t, ac, parkingInGap.ac)713 require.Same(t, ac, parkingInGap.ac)
713 require.False(t, parkedInGap, "fixture: park must not have published yet")714 require.False(t, parkedInGap, "fixture: park must not have published yet")
714 715
715- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})716+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
716 require.Error(t, err)717 require.Error(t, err)
717 var unknownErr *protoErr718 var unknownErr *protoErr
718 require.ErrorAs(t, err, &unknownErr)719 require.ErrorAs(t, err, &unknownErr)
719- require.Equal(t, ports.ErrNoSuchSession, unknownErr.code)720+ require.Equal(t, protocol.ErrNoSuchSession, unknownErr.code)
720 721
721 secondTr := &closeTrackingTransport{}722 secondTr := &closeTrackingTransport{}
722 secondWaiting := make(chan struct{})723 secondWaiting := make(chan struct{})
@@ -725,7 +726,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
725 }726 }
726 secondResult := make(chan routeResult, 1)727 secondResult := make(chan routeResult, 1)
727 go func() {728 go func() {
728- routedSess, routedAC, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), secondTr)729+ routedSess, routedAC, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), secondTr)
729 secondResult <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}730 secondResult <- routeResult{sess: routedSess, ac: routedAC, err: routeErr}
730 }()731 }()
731 awaitTestCompletion(t, secondWaiting, "second live resume did not arm parking wait")732 awaitTestCompletion(t, secondWaiting, "second live resume did not arm parking wait")
@@ -757,7 +758,7 @@ func TestConcurrentLiveResumesWaitParkingMarkerBeforePark(t *testing.T) {
757 758
758 var loserErr *protoErr759 var loserErr *protoErr
759 require.ErrorAs(t, loser.err, &loserErr)760 require.ErrorAs(t, loser.err, &loserErr)
760- require.Equal(t, ports.ErrNoSuchSession, loserErr.code)761+ require.Equal(t, protocol.ErrNoSuchSession, loserErr.code)
761 require.Contains(t, loserErr.Error(), "resume token is no longer valid")762 require.Contains(t, loserErr.Error(), "resume token is no longer valid")
762 require.True(t, loserTr.Closed() || len(loserTr.Sends()) == 0, "losing resume must not complete Welcome")763 require.True(t, loserTr.Closed() || len(loserTr.Sends()) == 0, "losing resume must not complete Welcome")
763 764
@@ -784,13 +785,13 @@ func TestExplicitDetachClearsOrphanedSameAttachmentParkingMarker(t *testing.T) {
784 d := newTestDaemon(t, newFactorySeq(t, workPTY, otherPTY), stubClock{})785 d := newTestDaemon(t, newFactorySeq(t, workPTY, otherPTY), stubClock{})
785 786
786 oldTr := &closeTrackingTransport{}787 oldTr := &closeTrackingTransport{}
787- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)788+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
788 require.NoError(t, err)789 require.NoError(t, err)
789 token := ac.resumeToken790 token := ac.resumeToken
790 require.NotZero(t, token)791 require.NotZero(t, token)
791 792
792 otherTr := &closeTrackingTransport{}793 otherTr := &closeTrackingTransport{}
793- otherSess, otherAC, err := d.route(helloResumeCapable(ports.IntentNew, "other", 0), otherTr)794+ otherSess, otherAC, err := d.route(helloResumeCapable(protocol.IntentNew, "other", 0), otherTr)
794 require.NoError(t, err)795 require.NoError(t, err)
795 otherToken := d.markParkingInFlight(otherSess, otherAC)796 otherToken := d.markParkingInFlight(otherSess, otherAC)
796 require.NotZero(t, otherToken)797 require.NotZero(t, otherToken)
@@ -811,7 +812,7 @@ func TestExplicitDetachClearsOrphanedSameAttachmentParkingMarker(t *testing.T) {
811 d.afterParkingWaitArmed = func() { close(waiterArmed) }812 d.afterParkingWaitArmed = func() { close(waiterArmed) }
812 waiterDone := make(chan bool, 1)813 waiterDone := make(chan bool, 1)
813 go func() {814 go func() {
814- waiterDone <- d.waitParkingInFlight(helloResumeCapable(ports.IntentResume, "work", token))815+ waiterDone <- d.waitParkingInFlight(helloResumeCapable(protocol.IntentResume, "work", token))
815 }()816 }()
816 awaitTestCompletion(t, waiterArmed, "parking waiter did not arm on the orphaned marker")817 awaitTestCompletion(t, waiterArmed, "parking waiter did not arm on the orphaned marker")
817 818
@@ -829,18 +830,18 @@ func TestExplicitDetachClearsOrphanedSameAttachmentParkingMarker(t *testing.T) {
829 require.NotNil(t, otherAfter, "explicit winner must not clear another attachment's marker")830 require.NotNil(t, otherAfter, "explicit winner must not clear another attachment's marker")
830 require.Same(t, otherAC, otherAfter.ac)831 require.Same(t, otherAC, otherAfter.ac)
831 832
832- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})833+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", 0xdecafbad), &closeTrackingTransport{})
833 require.Error(t, err)834 require.Error(t, err)
834 var unknownErr *protoErr835 var unknownErr *protoErr
835 require.ErrorAs(t, err, &unknownErr)836 require.ErrorAs(t, err, &unknownErr)
836- require.Equal(t, ports.ErrNoSuchSession, unknownErr.code)837+ require.Equal(t, protocol.ErrNoSuchSession, unknownErr.code)
837 require.Contains(t, unknownErr.Error(), "resume token is no longer valid")838 require.Contains(t, unknownErr.Error(), "resume token is no longer valid")
838 839
839- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})840+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
840 require.Error(t, err)841 require.Error(t, err)
841 var retiredErr *protoErr842 var retiredErr *protoErr
842 require.ErrorAs(t, err, &retiredErr)843 require.ErrorAs(t, err, &retiredErr)
843- require.Equal(t, ports.ErrNoSuchSession, retiredErr.code)844+ require.Equal(t, protocol.ErrNoSuchSession, retiredErr.code)
844 require.Contains(t, retiredErr.Error(), "resume token is no longer valid")845 require.Contains(t, retiredErr.Error(), "resume token is no longer valid")
845 846
846 // A later parked lifecycle for another attachment must survive a clear847 // A later parked lifecycle for another attachment must survive a clear
@@ -882,7 +883,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
882 name: "session_removal",883 name: "session_removal",
883 win: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, oldTr *closeTrackingTransport, token uint64) {884 win: func(t *testing.T, d *Daemon, sess *session, ac *attachedClient, oldTr *closeTrackingTransport, token uint64) {
884 t.Helper()885 t.Helper()
885- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))886+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
886 d.mu.Lock()887 d.mu.Lock()
887 registered := d.sessions[sess.id] == sess888 registered := d.sessions[sess.id] == sess
888 d.mu.Unlock()889 d.mu.Unlock()
@@ -897,7 +898,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
897 d := newTestDaemon(t, newFactory(t, pty), stubClock{})898 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
898 899
899 oldTr := &closeTrackingTransport{}900 oldTr := &closeTrackingTransport{}
900- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)901+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
901 require.NoError(t, err)902 require.NoError(t, err)
902 token := ac.resumeToken903 token := ac.resumeToken
903 require.NotZero(t, token)904 require.NotZero(t, token)
@@ -917,7 +918,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
917 }918 }
918 result := make(chan routeResult, 1)919 result := make(chan routeResult, 1)
919 go func() {920 go func() {
920- _, _, routeErr := d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})921+ _, _, routeErr := d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
921 result <- routeResult{err: routeErr}922 result <- routeResult{err: routeErr}
922 }()923 }()
923 awaitTestCompletion(t, validated, "live resume did not pause after credential validation before mark")924 awaitTestCompletion(t, validated, "live resume did not pause after credential validation before mark")
@@ -934,7 +935,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
934 require.Error(t, got.err)935 require.Error(t, got.err)
935 var pe *protoErr936 var pe *protoErr
936 require.ErrorAs(t, got.err, &pe)937 require.ErrorAs(t, got.err, &pe)
937- require.Equal(t, ports.ErrNoSuchSession, pe.code)938+ require.Equal(t, protocol.ErrNoSuchSession, pe.code)
938 require.Contains(t, pe.Error(), "resume token is no longer valid")939 require.Contains(t, pe.Error(), "resume token is no longer valid")
939 940
940 d.mu.Lock()941 d.mu.Lock()
@@ -952,7 +953,7 @@ func TestLiveResumeRejectsLateMarkerAfterTerminalCleanupWins(t *testing.T) {
952 d := newTestDaemon(t, newFactory(t, pty), stubClock{})953 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
953 954
954 oldTr := &closeTrackingTransport{}955 oldTr := &closeTrackingTransport{}
955- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)956+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
956 require.NoError(t, err)957 require.NoError(t, err)
957 token := ac.resumeToken958 token := ac.resumeToken
958 require.NotZero(t, token)959 require.NotZero(t, token)
@@ -986,7 +987,7 @@ func TestStaleClientGoneAfterTransportCheckDoesNotDetachReboundAttachment(t *tes
986 d := newTestDaemon(t, newFactory(t, pty), stubClock{})987 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
987 988
988 oldTr := &closeTrackingTransport{}989 oldTr := &closeTrackingTransport{}
989- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)990+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
990 require.NoError(t, err)991 require.NoError(t, err)
991 token := ac.resumeToken992 token := ac.resumeToken
992 require.NotZero(t, token)993 require.NotZero(t, token)
@@ -1008,7 +1009,7 @@ func TestStaleClientGoneAfterTransportCheckDoesNotDetachReboundAttachment(t *tes
1008 awaitTestCompletion(t, passedPrecheck, "stale clientGone did not reach the post-precheck seam")1009 awaitTestCompletion(t, passedPrecheck, "stale clientGone did not reach the post-precheck seam")
1009 1010
1010 newTr := &closeTrackingTransport{}1011 newTr := &closeTrackingTransport{}
1011- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), newTr)1012+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), newTr)
1012 require.NoError(t, err)1013 require.NoError(t, err)
1013 require.Same(t, sess, resumedSess)1014 require.Same(t, sess, resumedSess)
1014 require.Same(t, ac, resumedAC)1015 require.Same(t, ac, resumedAC)
@@ -1032,7 +1033,7 @@ func TestResumeLiveAttachmentParkFailureRetiresOldTransport(t *testing.T) {
1032 d := newTestDaemon(t, newFactory(t, pty), stubClock{})1033 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1033 1034
1034 oldTr := &closeTrackingTransport{}1035 oldTr := &closeTrackingTransport{}
1035- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)1036+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
1036 require.NoError(t, err)1037 require.NoError(t, err)
1037 token := ac.resumeToken1038 token := ac.resumeToken
1038 require.NotZero(t, token)1039 require.NotZero(t, token)
@@ -1043,11 +1044,11 @@ func TestResumeLiveAttachmentParkFailureRetiresOldTransport(t *testing.T) {
1043 d.mu.Unlock()1044 d.mu.Unlock()
1044 }1045 }
1045 1046
1046- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})1047+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
1047 require.Error(t, err)1048 require.Error(t, err)
1048 var pe *protoErr1049 var pe *protoErr
1049 require.ErrorAs(t, err, &pe)1050 require.ErrorAs(t, err, &pe)
1050- require.Equal(t, ports.ErrServerShutdown, pe.code)1051+ require.Equal(t, protocol.ErrServerShutdown, pe.code)
1051 require.Empty(t, sess.snapshotAttachments(), "failed live park must leave the session without an owner")1052 require.Empty(t, sess.snapshotAttachments(), "failed live park must leave the session without an owner")
1052 require.Nil(t, ac.transport(), "failed live park must revoke the captured old transport")1053 require.Nil(t, ac.transport(), "failed live park must revoke the captured old transport")
1053 require.True(t, oldTr.Closed(), "failed live park must close the captured old transport")1054 require.True(t, oldTr.Closed(), "failed live park must close the captured old transport")
@@ -1062,14 +1063,14 @@ func TestResumeRebindsRotatesAndDoesNotOpenPTY(t *testing.T) {
1062 defer release()1063 defer release()
1063 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})1064 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
1064 1065
1065- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1066+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1066- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1067+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1067 require.NoError(t, err)1068 require.NoError(t, err)
1068 oldToken := ac.resumeToken1069 oldToken := ac.resumeToken
1069 d.clientGone(sess, ac, ac.transport(), false)1070 d.clientGone(sess, ac, ac.transport(), false)
1070 1071
1071- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1072+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1072- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", oldToken), tr2)1073+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", oldToken), tr2)
1073 require.NoError(t, err)1074 require.NoError(t, err)
1074 require.Same(t, sess, resumedSess)1075 require.Same(t, sess, resumedSess)
1075 require.Same(t, ac, resumedAC)1076 require.Same(t, ac, resumedAC)
@@ -1114,13 +1115,13 @@ func TestResumeClientIDMismatchDoesNotConsumeParkedToken(t *testing.T) {
1114 defer release()1115 defer release()
1115 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})1116 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
1116 1117
1117- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1118+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1118- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1119+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1119 require.NoError(t, err)1120 require.NoError(t, err)
1120 token := ac.resumeToken1121 token := ac.resumeToken
1121 d.clientGone(sess, ac, ac.transport(), false)1122 d.clientGone(sess, ac, ac.transport(), false)
1122 1123
1123- wrongClient := helloResumeCapable(ports.IntentResume, "work", token)1124+ wrongClient := helloResumeCapable(protocol.IntentResume, "work", token)
1124 wrongClient.ClientID = [16]byte{9, 9, 9, 9}1125 wrongClient.ClientID = [16]byte{9, 9, 9, 9}
1125 _, _, ok, err := d.resumeParked(wrongClient, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})1126 _, _, ok, err := d.resumeParked(wrongClient, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})
1126 d.mu.Lock()1127 d.mu.Lock()
@@ -1132,8 +1133,8 @@ func TestResumeClientIDMismatchDoesNotConsumeParkedToken(t *testing.T) {
1132 require.Equal(t, token, ac.resumeToken)1133 require.Equal(t, token, ac.resumeToken)
1133 require.True(t, ac.parked)1134 require.True(t, ac.parked)
1134 1135
1135- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1136+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1136- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), tr2)1137+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), tr2)
1137 require.NoError(t, err)1138 require.NoError(t, err)
1138 require.Same(t, sess, resumedSess)1139 require.Same(t, sess, resumedSess)
1139 require.Same(t, ac, resumedAC)1140 require.Same(t, ac, resumedAC)
@@ -1145,7 +1146,7 @@ func TestResumeCloseCapturedOldTransportDoesNotCloseReboundTransport(t *testing.
1145 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})1146 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
1146 1147
1147 oldTr := &closeTrackingTransport{}1148 oldTr := &closeTrackingTransport{}
1148- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)1149+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
1149 require.NoError(t, err)1150 require.NoError(t, err)
1150 token := ac.resumeToken1151 token := ac.resumeToken
1151 require.True(t, sess.detachIfCurrent(ac))1152 require.True(t, sess.detachIfCurrent(ac))
@@ -1153,7 +1154,7 @@ func TestResumeCloseCapturedOldTransportDoesNotCloseReboundTransport(t *testing.
1153 generation := ac.lifecycle.generationValue()1154 generation := ac.lifecycle.generationValue()
1154 1155
1155 newTr := &closeTrackingTransport{}1156 newTr := &closeTrackingTransport{}
1156- resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), newTr, domain.Size{Cols: 80, Rows: 24})1157+ resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), newTr, domain.Size{Cols: 80, Rows: 24})
1157 require.NoError(t, err)1158 require.NoError(t, err)
1158 require.True(t, ok)1159 require.True(t, ok)
1159 require.Same(t, sess, resumedSess)1160 require.Same(t, sess, resumedSess)
@@ -1171,7 +1172,7 @@ func TestResumeRebasesFullOutputWindowBeforeFirstPaint(t *testing.T) {
1171 defer release()1172 defer release()
1172 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})1173 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
1173 oldTr := &closeTrackingTransport{}1174 oldTr := &closeTrackingTransport{}
1174- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)1175+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
1175 require.NoError(t, err)1176 require.NoError(t, err)
1176 ac.output.next = maxUnackedOutputStates1177 ac.output.next = maxUnackedOutputStates
1177 token := ac.resumeToken1178 token := ac.resumeToken
@@ -1179,7 +1180,7 @@ func TestResumeRebasesFullOutputWindowBeforeFirstPaint(t *testing.T) {
1179 require.True(t, d.parkAttachment(sess, ac))1180 require.True(t, d.parkAttachment(sess, ac))
1180 1181
1181 newTr := &closeTrackingTransport{}1182 newTr := &closeTrackingTransport{}
1182- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), newTr)1183+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), newTr)
1183 require.NoError(t, err)1184 require.NoError(t, err)
1184 require.Same(t, ac, resumedAC)1185 require.Same(t, ac, resumedAC)
1185 d.paint(resumedSess, resumedAC, true, nil)1186 d.paint(resumedSess, resumedAC, true, nil)
@@ -1206,7 +1207,7 @@ func TestParkingReleasesPaneCapturesBeforeHeadlessCloseAndResume(t *testing.T) {
1206 defer release()1207 defer release()
1207 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})1208 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
1208 oldTransport := &closeTrackingTransport{}1209 oldTransport := &closeTrackingTransport{}
1209- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)1210+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
1210 require.NoError(t, err)1211 require.NoError(t, err)
1211 1212
1212 tb := sess.tabs[0]1213 tb := sess.tabs[0]
@@ -1237,7 +1238,7 @@ func TestParkingReleasesPaneCapturesBeforeHeadlessCloseAndResume(t *testing.T) {
1237 ac.sendMu.Unlock()1238 ac.sendMu.Unlock()
1238 1239
1239 newTransport := &closeTrackingTransport{}1240 newTransport := &closeTrackingTransport{}
1240- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, sess.name, token), newTransport)1241+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, sess.name, token), newTransport)
1241 require.NoError(t, err)1242 require.NoError(t, err)
1242 require.Same(t, sess, resumedSess)1243 require.Same(t, sess, resumedSess)
1243 require.Same(t, ac, resumedAC)1244 require.Same(t, ac, resumedAC)
@@ -1260,8 +1261,8 @@ func TestExplicitDetachDoesNotPark(t *testing.T) {
1260 pty, release := newBlockingPTY(t)1261 pty, release := newBlockingPTY(t)
1261 defer release()1262 defer release()
1262 d := newTestDaemon(t, newFactory(t, pty), stubClock{})1263 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1263- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1264+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1264- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1265+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1265 require.NoError(t, err)1266 require.NoError(t, err)
1266 d.clientGone(sess, ac, ac.transport(), true)1267 d.clientGone(sess, ac, ac.transport(), true)
1267 d.mu.Lock()1268 d.mu.Lock()
@@ -1276,8 +1277,8 @@ func TestResumeParkUsesConfiguredGraceAndExpiresOnlyAfterGrace(t *testing.T) {
1276 defer release()1277 defer release()
1277 d := newTestDaemon(t, newFactory(t, pty), clk)1278 d := newTestDaemon(t, newFactory(t, pty), clk)
1278 WithResumeParkGrace(20 * time.Minute)(d)1279 WithResumeParkGrace(20 * time.Minute)(d)
1279- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1280+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1280- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1281+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1281 require.NoError(t, err)1282 require.NoError(t, err)
1282 token := ac.resumeToken1283 token := ac.resumeToken
1283 1284
@@ -1303,8 +1304,8 @@ func TestParkExpiryAndShutdownCleanup(t *testing.T) {
1303 pty, release := newBlockingPTY(t)1304 pty, release := newBlockingPTY(t)
1304 defer release()1305 defer release()
1305 d := newTestDaemon(t, newFactory(t, pty), clk)1306 d := newTestDaemon(t, newFactory(t, pty), clk)
1306- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1307+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1307- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1308+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1308 require.NoError(t, err)1309 require.NoError(t, err)
1309 token := ac.resumeToken1310 token := ac.resumeToken
1310 d.clientGone(sess, ac, ac.transport(), false)1311 d.clientGone(sess, ac, ac.transport(), false)
@@ -1320,11 +1321,11 @@ func TestParkExpiryAndShutdownCleanup(t *testing.T) {
1320 pty2, release2 := newBlockingPTY(t)1321 pty2, release2 := newBlockingPTY(t)
1321 defer release2()1322 defer release2()
1322 d2 := newTestDaemon(t, newFactory(t, pty2), stubClock{})1323 d2 := newTestDaemon(t, newFactory(t, pty2), stubClock{})
1323- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1324+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1324- sess2, ac2, err := d2.route(helloResumeCapable(ports.IntentNew, "other", 0), tr2)1325+ sess2, ac2, err := d2.route(helloResumeCapable(protocol.IntentNew, "other", 0), tr2)
1325 require.NoError(t, err)1326 require.NoError(t, err)
1326 d2.clientGone(sess2, ac2, ac2.transport(), false)1327 d2.clientGone(sess2, ac2, ac2.transport(), false)
1327- d2.shutdownAll(ports.ReasonServerShutdown)1328+ d2.shutdownAll(protocol.ReasonServerShutdown)
1328 d2.mu.Lock()1329 d2.mu.Lock()
1329 parked := len(d2.parked)1330 parked := len(d2.parked)
1330 d2.mu.Unlock()1331 d2.mu.Unlock()
@@ -1335,8 +1336,8 @@ func TestLiveParkAndResumeRetainsAttachmentWithoutDaemonHistory(t *testing.T) {
1335 pty, release := newBlockingPTY(t)1336 pty, release := newBlockingPTY(t)
1336 defer release()1337 defer release()
1337 d := newTestDaemon(t, newFactory(t, pty), stubClock{})1338 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1338- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1339+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1339- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1340+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1340 require.NoError(t, err)1341 require.NoError(t, err)
1341 1342
1342 d.clientGone(sess, ac, ac.transport(), false)1343 d.clientGone(sess, ac, ac.transport(), false)
@@ -1347,8 +1348,8 @@ func TestLiveParkAndResumeRetainsAttachmentWithoutDaemonHistory(t *testing.T) {
1347 d.mu.Unlock()1348 d.mu.Unlock()
1348 require.NotNil(t, parked)1349 require.NotNil(t, parked)
1349 1350
1350- tr2, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1351+ tr2, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1351- resumedSess, resumedAC, err := d.route(helloResumeCapable(ports.IntentResume, "work", token), tr2)1352+ resumedSess, resumedAC, err := d.route(helloResumeCapable(protocol.IntentResume, "work", token), tr2)
1352 require.NoError(t, err)1353 require.NoError(t, err)
1353 require.Same(t, sess, resumedSess)1354 require.Same(t, sess, resumedSess)
1354 require.Same(t, ac, resumedAC)1355 require.Same(t, ac, resumedAC)
@@ -1377,8 +1378,8 @@ func TestDiscardingParkedAttachmentDoesNotOwnPreviousSession(t *testing.T) {
1377 pty, release := newBlockingPTY(t)1378 pty, release := newBlockingPTY(t)
1378 defer release()1379 defer release()
1379 d := newTestDaemon(t, newFactory(t, pty), stubClock{})1380 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1380- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1381+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1381- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1382+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1382 require.NoError(t, err)1383 require.NoError(t, err)
1383 1384
1384 d.clientGone(sess, ac, ac.transport(), false)1385 d.clientGone(sess, ac, ac.transport(), false)
@@ -1401,7 +1402,7 @@ func TestEphemeralParkExpiryKeepsSession(t *testing.T) {
1401 d := newTestDaemon(t, newFactory(t, pty), clk)1402 d := newTestDaemon(t, newFactory(t, pty), clk)
1402 1403
1403 tr := &closeTrackingTransport{}1404 tr := &closeTrackingTransport{}
1404- sess, ac, err := d.route(helloResumeCapable(ports.IntentEphemeral, "", 0), tr)1405+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentEphemeral, "", 0), tr)
1405 require.NoError(t, err)1406 require.NoError(t, err)
1406 token := ac.resumeToken1407 token := ac.resumeToken
1407 require.NotZero(t, token)1408 require.NotZero(t, token)
@@ -1427,17 +1428,17 @@ func TestKilledSessionPurgesParkedResumeToken(t *testing.T) {
1427 pty, release := newBlockingPTY(t)1428 pty, release := newBlockingPTY(t)
1428 defer release()1429 defer release()
1429 d := newTestDaemon(t, newFactory(t, pty), stubClock{})1430 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1430- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1431+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1431- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1432+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1432 require.NoError(t, err)1433 require.NoError(t, err)
1433 token := ac.resumeToken1434 token := ac.resumeToken
1434 d.clientGone(sess, ac, ac.transport(), false)1435 d.clientGone(sess, ac, ac.transport(), false)
1435 1436
1436- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))1437+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
1437 d.mu.Lock()1438 d.mu.Lock()
1438 _, parked := d.parked[token]1439 _, parked := d.parked[token]
1439 d.mu.Unlock()1440 d.mu.Unlock()
1440- _, _, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})1441+ _, _, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})
1441 require.False(t, parked, "killSession purges parked token")1442 require.False(t, parked, "killSession purges parked token")
1442 require.NoError(t, err)1443 require.NoError(t, err)
1443 require.False(t, ok, "killed session cannot be resumed")1444 require.False(t, ok, "killed session cannot be resumed")
@@ -1458,13 +1459,13 @@ func TestKilledSessionUnblocksParkingWaiterFailsClosed(t *testing.T) {
1458 d := newTestDaemon(t, newFactorySeq(t, workPTY, otherPTY), stubClock{})1459 d := newTestDaemon(t, newFactorySeq(t, workPTY, otherPTY), stubClock{})
1459 1460
1460 oldTr := &closeTrackingTransport{}1461 oldTr := &closeTrackingTransport{}
1461- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)1462+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
1462 require.NoError(t, err)1463 require.NoError(t, err)
1463 token := ac.resumeToken1464 token := ac.resumeToken
1464 require.NotZero(t, token)1465 require.NotZero(t, token)
1465 1466
1466 otherTr := &closeTrackingTransport{}1467 otherTr := &closeTrackingTransport{}
1467- otherSess, otherAC, err := d.route(helloResumeCapable(ports.IntentNew, "other", 0), otherTr)1468+ otherSess, otherAC, err := d.route(helloResumeCapable(protocol.IntentNew, "other", 0), otherTr)
1468 require.NoError(t, err)1469 require.NoError(t, err)
1469 otherToken := d.markParkingInFlight(otherSess, otherAC)1470 otherToken := d.markParkingInFlight(otherSess, otherAC)
1470 require.NotZero(t, otherToken)1471 require.NotZero(t, otherToken)
@@ -1484,11 +1485,11 @@ func TestKilledSessionUnblocksParkingWaiterFailsClosed(t *testing.T) {
1484 d.afterParkingWaitArmed = func() { close(waiterArmed) }1485 d.afterParkingWaitArmed = func() { close(waiterArmed) }
1485 waiterDone := make(chan bool, 1)1486 waiterDone := make(chan bool, 1)
1486 go func() {1487 go func() {
1487- waiterDone <- d.waitParkingInFlight(helloResumeCapable(ports.IntentResume, "work", token))1488+ waiterDone <- d.waitParkingInFlight(helloResumeCapable(protocol.IntentResume, "work", token))
1488 }()1489 }()
1489 awaitTestCompletion(t, waiterArmed, "parking waiter did not arm before session kill")1490 awaitTestCompletion(t, waiterArmed, "parking waiter did not arm before session kill")
1490 1491
1491- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))1492+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
1492 1493
1493 waited := awaitTestValue(t, waiterDone, "parking waiter stayed blocked after session kill")1494 waited := awaitTestValue(t, waiterDone, "parking waiter stayed blocked after session kill")
1494 require.True(t, waited, "waiter must observe the session marker before it is purged")1495 require.True(t, waited, "waiter must observe the session marker before it is purged")
@@ -1505,11 +1506,11 @@ func TestKilledSessionUnblocksParkingWaiterFailsClosed(t *testing.T) {
1505 require.True(t, otherRegistered, "other session must survive work kill when PTYs are isolated")1506 require.True(t, otherRegistered, "other session must survive work kill when PTYs are isolated")
1506 require.False(t, closing, "daemon must not shut down while the other session remains")1507 require.False(t, closing, "daemon must not shut down while the other session remains")
1507 1508
1508- _, _, err = d.route(helloResumeCapable(ports.IntentResume, "work", token), &closeTrackingTransport{})1509+ _, _, err = d.route(helloResumeCapable(protocol.IntentResume, "work", token), &closeTrackingTransport{})
1509 require.Error(t, err)1510 require.Error(t, err)
1510 var killedErr *protoErr1511 var killedErr *protoErr
1511 require.ErrorAs(t, err, &killedErr)1512 require.ErrorAs(t, err, &killedErr)
1512- require.Equal(t, ports.ErrNoSuchSession, killedErr.code)1513+ require.Equal(t, protocol.ErrNoSuchSession, killedErr.code)
1513 require.Contains(t, killedErr.Error(), "resume token is no longer valid")1514 require.Contains(t, killedErr.Error(), "resume token is no longer valid")
1514 }1515 }
1515 1516
@@ -1519,7 +1520,7 @@ func TestStaleClientGoneDoesNotDetachOrCloseFreshTransport(t *testing.T) {
1519 d := newTestDaemon(t, newFactory(t, p), stubClock{})1520 d := newTestDaemon(t, newFactory(t, p), stubClock{})
1520 1521
1521 oldTr := &closeTrackingTransport{}1522 oldTr := &closeTrackingTransport{}
1522- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTr)1523+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTr)
1523 require.NoError(t, err)1524 require.NoError(t, err)
1524 freshTr := &closeTrackingTransport{}1525 freshTr := &closeTrackingTransport{}
1525 ac.replaceTransport(freshTr)1526 ac.replaceTransport(freshTr)
@@ -1557,7 +1558,7 @@ func TestSequencedInputDoesNotPrematurelyEcho(t *testing.T) {
1557 defer release()1558 defer release()
1558 d := newTestDaemon(t, newFactory(t, p), stubClock{})1559 d := newTestDaemon(t, newFactory(t, p), stubClock{})
1559 tr := &closeTrackingTransport{}1560 tr := &closeTrackingTransport{}
1560- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), tr)1561+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), tr)
1561 require.NoError(t, err)1562 require.NoError(t, err)
1562 1563
1563 d.handleSequencedInput(sess, ac, 42, []byte("x"))1564 d.handleSequencedInput(sess, ac, 42, []byte("x"))
@@ -1587,8 +1588,8 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) {
1587 },1588 },
1588 ).Maybe()1589 ).Maybe()
1589 d := newTestDaemon(t, factory, stubClock{})1590 d := newTestDaemon(t, factory, stubClock{})
1590- tr, _, _ := newConn(t, mustHello(ports.IntentAttach, "unused", domain.Size{}))1591+ tr, _, _ := newConn(t, mustHello(protocol.IntentAttach, "unused", domain.Size{}))
1591- hello := helloResumeCapable(ports.IntentNew, "work", 0)1592+ hello := helloResumeCapable(protocol.IntentNew, "work", 0)
1592 hello.Env = []string{"SECRET=before", "SHELL=/bin/sh", "TERM=old", "VEV=old"}1593 hello.Env = []string{"SECRET=before", "SHELL=/bin/sh", "TERM=old", "VEV=old"}
1593 sess, ac, err := d.route(hello, tr)1594 sess, ac, err := d.route(hello, tr)
1594 require.NoError(t, err)1595 require.NoError(t, err)
@@ -1596,7 +1597,7 @@ func TestResumeParkedReplacesFuturePTYEnvironment(t *testing.T) {
1596 require.True(t, sess.detachIfCurrent(ac))1597 require.True(t, sess.detachIfCurrent(ac))
1597 require.True(t, d.parkAttachment(sess, ac))1598 require.True(t, d.parkAttachment(sess, ac))
1598 1599
1599- resumeHello := helloResumeCapable(ports.IntentResume, "work", token)1600+ resumeHello := helloResumeCapable(protocol.IntentResume, "work", token)
1600 resumeHello.Env = []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}1601 resumeHello.Env = []string{"SECRET=after", "PAIR=a=b", "SHELL=/usr/bin/fish", "TERM=old", "COLORTERM=old", "TERM_PROGRAM=old", "VEV=old"}
1601 resumeHello.TrueColor = true1602 resumeHello.TrueColor = true
1602 _, _, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})1603 _, _, ok, err := d.resumeParked(resumeHello, &closeTrackingTransport{}, domain.Size{Cols: 80, Rows: 24})
@@ -1619,7 +1620,7 @@ func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) {
1619 d := newTestDaemon(t, newFactory(t, pty), stubClock{})1620 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
1620 d.baseEnv = []string{"DAEMON=owned"}1621 d.baseEnv = []string{"DAEMON=owned"}
1621 oldTransport := &closeTrackingTransport{}1622 oldTransport := &closeTrackingTransport{}
1622- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)1623+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
1623 require.NoError(t, err)1624 require.NoError(t, err)
1624 sess.mu.Lock()1625 sess.mu.Lock()
1625 sess.env = copyEnvironment(d.baseEnv)1626 sess.env = copyEnvironment(d.baseEnv)
@@ -1628,8 +1629,8 @@ func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) {
1628 require.True(t, sess.detachIfCurrent(ac))1629 require.True(t, sess.detachIfCurrent(ac))
1629 require.True(t, d.parkAttachment(sess, ac))1630 require.True(t, d.parkAttachment(sess, ac))
1630 1631
1631- resume := helloResumeCapable(ports.IntentResume, "work", token)1632+ resume := helloResumeCapable(protocol.IntentResume, "work", token)
1632- resume.EnvironmentPolicy = ports.EnvironmentPolicyDaemonOwned1633+ resume.EnvironmentPolicy = protocol.EnvironmentPolicyDaemonOwned
1633 resume.Env = []string{"UNTRUSTED=client"}1634 resume.Env = []string{"UNTRUSTED=client"}
1634 _, _, ok, err := d.resumeParked(resume, &closeTrackingTransport{}, defaultSize)1635 _, _, ok, err := d.resumeParked(resume, &closeTrackingTransport{}, defaultSize)
1635 require.NoError(t, err)1636 require.NoError(t, err)
@@ -1638,14 +1639,14 @@ func TestResumeParkedDaemonOwnedPreservesSessionEnvironment(t *testing.T) {
1638 require.Equal(t, []string{"DAEMON=owned"}, sess.env)1639 require.Equal(t, []string{"DAEMON=owned"}, sess.env)
1639 sess.mu.Unlock()1640 sess.mu.Unlock()
1640 1641
1641- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))1642+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
1642 }1643 }
1643 1644
1644 func TestResumeRenegotiatesOutputWindowOnReusedStream(t *testing.T) {1645 func TestResumeRenegotiatesOutputWindowOnReusedStream(t *testing.T) {
1645 pty, release := newBlockingPTY(t)1646 pty, release := newBlockingPTY(t)
1646 defer release()1647 defer release()
1647 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})1648 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
1648- firstHello := helloResumeCapable(ports.IntentNew, "work", 0)1649+ firstHello := helloResumeCapable(protocol.IntentNew, "work", 0)
1649 firstHello.MaxOutputInFlight = 81650 firstHello.MaxOutputInFlight = 8
1650 sess, ac, err := d.route(firstHello, &closeTrackingTransport{})1651 sess, ac, err := d.route(firstHello, &closeTrackingTransport{})
1651 require.NoError(t, err)1652 require.NoError(t, err)
@@ -1655,7 +1656,7 @@ func TestResumeRenegotiatesOutputWindowOnReusedStream(t *testing.T) {
1655 require.True(t, sess.detachIfCurrent(ac))1656 require.True(t, sess.detachIfCurrent(ac))
1656 require.True(t, d.parkAttachment(sess, ac))1657 require.True(t, d.parkAttachment(sess, ac))
1657 1658
1658- resumeOne := helloResumeCapable(ports.IntentResume, "work", token)1659+ resumeOne := helloResumeCapable(protocol.IntentResume, "work", token)
1659 resumeOne.MaxOutputInFlight = 11660 resumeOne.MaxOutputInFlight = 1
1660 _, resumed, err := d.route(resumeOne, &closeTrackingTransport{})1661 _, resumed, err := d.route(resumeOne, &closeTrackingTransport{})
1661 require.NoError(t, err)1662 require.NoError(t, err)
@@ -1665,7 +1666,7 @@ func TestResumeRenegotiatesOutputWindowOnReusedStream(t *testing.T) {
1665 token = resumed.resumeToken1666 token = resumed.resumeToken
1666 require.True(t, sess.detachIfCurrent(resumed))1667 require.True(t, sess.detachIfCurrent(resumed))
1667 require.True(t, d.parkAttachment(sess, resumed))1668 require.True(t, d.parkAttachment(sess, resumed))
1668- resumeEight := helloResumeCapable(ports.IntentResume, "work", token)1669+ resumeEight := helloResumeCapable(protocol.IntentResume, "work", token)
1669 resumeEight.MaxOutputInFlight = 81670 resumeEight.MaxOutputInFlight = 8
1670 _, resumed, err = d.route(resumeEight, &closeTrackingTransport{})1671 _, resumed, err = d.route(resumeEight, &closeTrackingTransport{})
1671 require.NoError(t, err)1672 require.NoError(t, err)
M internal/usecase/daemon/resume_wake_incarnation_test.go +3 -3
@@ -6,7 +6,7 @@ import (
6 "github.com/stretchr/testify/require"6 "github.com/stretchr/testify/require"
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 )10 )
11 11
12 // A wake can have passed coordinator scheduling before a link loss parks its12 // A wake can have passed coordinator scheduling before a link loss parks its
@@ -19,7 +19,7 @@ func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) {
19 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})19 d := newTestDaemon(t, newFactorySeq(t, pty), stubClock{})
20 20
21 oldTransport := &closeTrackingTransport{}21 oldTransport := &closeTrackingTransport{}
22- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)22+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
23 require.NoError(t, err)23 require.NoError(t, err)
24 token := ac.resumeToken24 token := ac.resumeToken
25 rc := sess.renderCoordinator()25 rc := sess.renderCoordinator()
@@ -45,7 +45,7 @@ func TestParkedResumeRejectsDispatchedWakeFromPriorAttachment(t *testing.T) {
45 45
46 d.clientGone(sess, ac, oldTransport, false)46 d.clientGone(sess, ac, oldTransport, false)
47 newTransport := &closeTrackingTransport{}47 newTransport := &closeTrackingTransport{}
48- resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(ports.IntentResume, sess.name, token), newTransport, domain.Size{Cols: 80, Rows: 24})48+ resumedSess, resumedAC, ok, err := d.resumeParked(helloResumeCapable(protocol.IntentResume, sess.name, token), newTransport, domain.Size{Cols: 80, Rows: 24})
49 require.NoError(t, err)49 require.NoError(t, err)
50 require.True(t, ok)50 require.True(t, ok)
51 require.Same(t, sess, resumedSess)51 require.Same(t, sess, resumedSess)
M internal/usecase/daemon/route_position_test.go +8 -8
@@ -30,10 +30,10 @@ func TestRouteAppliesPreferredTabPerAttachment(t *testing.T) {
30 sess.mu.Unlock()30 sess.mu.Unlock()
31 tr := &closeTrackingTransport{}31 tr := &closeTrackingTransport{}
32 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}32 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
33- hello := ports.Hello{33+ hello := protocol.Hello{
34- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,34+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name,
35 Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,35 Size: defaultSize, ExactTarget: &target, PreferredTabID: test.preferred,
36- EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,36+ EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
37 }37 }
38 38
39 _, ac, err := d.routeWithContext(context.Background(), hello, tr)39 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
@@ -50,7 +50,7 @@ func TestResumeAppliesPreferredTabToParkedAttachment(t *testing.T) {
50 defer release()50 defer release()
51 d := newTestDaemon(t, newFactory(t, pty), stubClock{})51 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
52 oldTransport := &closeTrackingTransport{}52 oldTransport := &closeTrackingTransport{}
53- sess, ac, err := d.route(helloResumeCapable(ports.IntentNew, "work", 0), oldTransport)53+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentNew, "work", 0), oldTransport)
54 require.NoError(t, err)54 require.NoError(t, err)
55 second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})55 second := newTabWithStableID("tab-2", "pane-2", newQuietPTY(), domain.Size{Cols: 80, Rows: 22})
56 sess.mu.Lock()56 sess.mu.Lock()
@@ -60,7 +60,7 @@ func TestResumeAppliesPreferredTabToParkedAttachment(t *testing.T) {
60 token := ac.resumeToken60 token := ac.resumeToken
61 d.clientGone(sess, ac, oldTransport, false)61 d.clientGone(sess, ac, oldTransport, false)
62 62
63- hello := helloResumeCapable(ports.IntentResume, "work", token)63+ hello := helloResumeCapable(protocol.IntentResume, "work", token)
64 hello.PreferredTabID = "tab-2"64 hello.PreferredTabID = "tab-2"
65 newTransport := &closeTrackingTransport{}65 newTransport := &closeTrackingTransport{}
66 resumedSession, resumed, err := d.route(hello, newTransport)66 resumedSession, resumed, err := d.route(hello, newTransport)
@@ -80,9 +80,9 @@ func TestPaintPublishesChangedAttachmentRoutePosition(t *testing.T) {
80 sess.mu.Unlock()80 sess.mu.Unlock()
81 tr := &closeTrackingTransport{}81 tr := &closeTrackingTransport{}
82 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}82 target := protocol.ExactSessionTarget{LifecycleID: sess.incarnation, SessionName: sess.name}
83- hello := ports.Hello{83+ hello := protocol.Hello{
84- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: sess.name,84+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: sess.name,
85- Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: ports.EnvironmentPolicyClientOwned,85+ Size: defaultSize, ExactTarget: &target, EnvironmentPolicy: protocol.EnvironmentPolicyClientOwned,
86 }86 }
87 _, ac, err := d.routeWithContext(context.Background(), hello, tr)87 _, ac, err := d.routeWithContext(context.Background(), hello, tr)
88 require.NoError(t, err)88 require.NoError(t, err)
M internal/usecase/daemon/session.go +10 -9
@@ -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/layout"21 "github.com/bnema/vev/internal/usecase/layout"
21 )22 )
22 23
@@ -243,7 +244,7 @@ func (d *Daemon) createSessionLockedWithModeAndInactiveFence(name string, epheme
243 return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't read session catalogue", err)244 return nil, domain.UserErr(domain.NoticeSessionSpawn, "couldn't read session catalogue", err)
244 }245 }
245 if d.closing {246 if d.closing {
246- return nil, &protoErr{ports.ErrServerShutdown, "daemon is shutting down"}247+ return nil, &protoErr{protocol.ErrServerShutdown, "daemon is shutting down"}
247 }248 }
248 stopped, resuming = d.inactive[name]249 stopped, resuming = d.inactive[name]
249 if !inactiveResumeFenceAllows(stopped, resuming, expectedInactive, authoritative, authoritativeExists, validateInactive) {250 if !inactiveResumeFenceAllows(stopped, resuming, expectedInactive, authoritative, authoritativeExists, validateInactive) {
@@ -498,7 +499,7 @@ func (d *Daemon) createSessionAndSwitch(from *session, ac *attachedClient, name
498 ready: true,499 ready: true,
499 })500 })
500 if err != nil {501 if err != nil {
501- _ = d.killSession(newSess, ports.ReasonSessionKilled, true)502+ _ = d.killSession(newSess, protocol.ReasonSessionKilled, true)
502 return err503 return err
503 }504 }
504 505
@@ -552,7 +553,7 @@ func (d *Daemon) createSessionAndSwitchForAttachment(effect *attachmentEffect, n
552 })553 })
553 if err != nil {554 if err != nil {
554 if created != nil {555 if created != nil {
555- _ = d.killSession(created, ports.ReasonSessionKilled, true)556+ _ = d.killSession(created, protocol.ReasonSessionKilled, true)
556 }557 }
557 return err558 return err
558 }559 }
@@ -590,7 +591,7 @@ func (d *Daemon) createEphemeralSessionAndSwitch(from *session, ac *attachedClie
590 expectedTransport: ac.transportSnapshot(), ready: true,591 expectedTransport: ac.transportSnapshot(), ready: true,
591 })592 })
592 if err != nil {593 if err != nil {
593- _ = d.killSession(newSess, ports.ReasonSessionKilled, true)594+ _ = d.killSession(newSess, protocol.ReasonSessionKilled, true)
594 return err595 return err
595 }596 }
596 d.touchMRU(newSess)597 d.touchMRU(newSess)
@@ -629,7 +630,7 @@ func (d *Daemon) createEphemeralSessionAndSwitchForAttachment(effect *attachment
629 })630 })
630 if err != nil {631 if err != nil {
631 if created != nil {632 if created != nil {
632- _ = d.killSession(created, ports.ReasonSessionKilled, true)633+ _ = d.killSession(created, protocol.ReasonSessionKilled, true)
633 }634 }
634 return err635 return err
635 }636 }
@@ -1164,9 +1165,9 @@ func (d *Daemon) closeTabLockedWithEffect(sess *session, tb *tab, repaint bool,
1164 var err error1165 var err error
1165 if effect != nil {1166 if effect != nil {
1166 effect.bindActionEnd(d, "close-tab")1167 effect.bindActionEnd(d, "close-tab")
1167- err = d.killSessionForAttachment(sess, ports.ReasonSessionKilled, false, effect, "close-tab")1168+ err = d.killSessionForAttachment(sess, protocol.ReasonSessionKilled, false, effect, "close-tab")
1168 } else {1169 } else {
1169- err = d.killSession(sess, ports.ReasonSessionKilled, false)1170+ err = d.killSession(sess, protocol.ReasonSessionKilled, false)
1170 }1171 }
1171 if err != nil {1172 if err != nil {
1172 d.log.Warn("closing last tab failed", "session", name, "err", err)1173 d.log.Warn("closing last tab failed", "session", name, "err", err)
@@ -1645,7 +1646,7 @@ func (d *Daemon) killSessionWithSnapshotDeadlineAndCondition(sess *session, reas
1645 name := sess.name1646 name := sess.name
1646 sess.mu.Unlock()1647 sess.mu.Unlock()
1647 terminalSnapshotErr = fmt.Errorf("retain final snapshot for session %q: snapshot worker unavailable, saturated, or timed out", name)1648 terminalSnapshotErr = fmt.Errorf("retain final snapshot for session %q: snapshot worker unavailable, saturated, or timed out", name)
1648- if reason != ports.ReasonServerShutdown {1649+ if reason != protocol.ReasonServerShutdown {
1649 return terminalSnapshotErr1650 return terminalSnapshotErr
1650 }1651 }
1651 // Preserve any already-queued routine capture until the blocked1652 // Preserve any already-queued routine capture until the blocked
@@ -1741,7 +1742,7 @@ func (d *Daemon) killSessionWithSnapshotDeadlineAndCondition(sess *session, reas
1741 unlockSessions()1742 unlockSessions()
1742 d.notices.routingMu.Unlock()1743 d.notices.routingMu.Unlock()
1743 if !ephemeral {1744 if !ephemeral {
1744- stopped := inactiveSessionFromRecord(stoppedRecord, ports.SessionDown, nil)1745+ stopped := inactiveSessionFromRecord(stoppedRecord, protocol.SessionDown, nil)
1745 stopped.purging = purge1746 stopped.purging = purge
1746 d.inactive[stoppedName] = stopped1747 d.inactive[stoppedName] = stopped
1747 }1748 }
M internal/usecase/daemon/session_core_test.go +2 -2
@@ -5,7 +5,7 @@ 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/stretchr/testify/require"9 "github.com/stretchr/testify/require"
10 )10 )
11 11
@@ -103,7 +103,7 @@ func TestLocalCreateThenKillRemovesLiveRegistryEntry(t *testing.T) {
103 d.mu.Unlock()103 d.mu.Unlock()
104 require.Same(t, sess, registered)104 require.Same(t, sess, registered)
105 105
106- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))106+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
107 d.mu.Lock()107 d.mu.Lock()
108 _, live := d.sessions[sess.id]108 _, live := d.sessions[sess.id]
109 d.mu.Unlock()109 d.mu.Unlock()
M internal/usecase/daemon/session_purge_retry_test.go +2 -2
@@ -8,8 +8,8 @@ import (
8 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
9 9
10 "github.com/bnema/vev/internal/domain"10 "github.com/bnema/vev/internal/domain"
11- "github.com/bnema/vev/internal/ports"
12 portsmocks "github.com/bnema/vev/internal/ports/mocks"11 portsmocks "github.com/bnema/vev/internal/ports/mocks"
12+ "github.com/bnema/vev/internal/protocol"
13 )13 )
14 14
15 type retryablePurgeRepository struct {15 type retryablePurgeRepository struct {
@@ -40,7 +40,7 @@ func TestLivePurgeLeavesFailedDirectoryDeletionForStartupGarbageCollection(t *te
40 pty.EXPECT().Close().Return(nil).Once()40 pty.EXPECT().Close().Return(nil).Once()
41 d.sessions = map[domain.SessionID]*session{sess.id: sess}41 d.sessions = map[domain.SessionID]*session{sess.id: sess}
42 42
43- require.Error(t, d.killSession(sess, ports.ReasonSessionKilled, true))43+ require.Error(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
44 require.Equal(t, []string{"delete incarnation"}, repository.calls)44 require.Equal(t, []string{"delete incarnation"}, repository.calls)
45 _, exists, err := d.catalogue.Record("work")45 _, exists, err := d.catalogue.Record("work")
46 require.NoError(t, err)46 require.NoError(t, err)
M internal/usecase/daemon/session_test.go +73 -72
@@ -19,6 +19,7 @@ import (
19 "github.com/bnema/vev/internal/persist"19 "github.com/bnema/vev/internal/persist"
20 "github.com/bnema/vev/internal/ports"20 "github.com/bnema/vev/internal/ports"
21 portsmocks "github.com/bnema/vev/internal/ports/mocks"21 portsmocks "github.com/bnema/vev/internal/ports/mocks"
22+ "github.com/bnema/vev/internal/protocol"
22 "github.com/bnema/vev/internal/usecase/picker"23 "github.com/bnema/vev/internal/usecase/picker"
23 themeui "github.com/bnema/vev/internal/usecase/theme"24 themeui "github.com/bnema/vev/internal/usecase/theme"
24 )25 )
@@ -36,7 +37,7 @@ func expectFloatingPrewarmOpen(factory *portsmocks.MockPTYFactory, normalSize do
36 func TestHandshakeEphemeralHappy(t *testing.T) {37 func TestHandshakeEphemeralHappy(t *testing.T) {
37 p, releasePTY := newBlockingPTY(t)38 p, releasePTY := newBlockingPTY(t)
38 d := newTestDaemon(t, newFactory(t, p), stubClock{})39 d := newTestDaemon(t, newFactory(t, p), stubClock{})
39- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))40+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
40 41
41 var hg sync.WaitGroup42 var hg sync.WaitGroup
42 hg.Go(func() { d.handleConn(tr) })43 hg.Go(func() { d.handleConn(tr) })
@@ -58,7 +59,7 @@ func TestHandshakeEphemeralHappy(t *testing.T) {
58 func TestHandshakeNewHappy(t *testing.T) {59 func TestHandshakeNewHappy(t *testing.T) {
59 p, releasePTY := newBlockingPTY(t)60 p, releasePTY := newBlockingPTY(t)
60 d := newTestDaemon(t, newFactory(t, p), stubClock{})61 d := newTestDaemon(t, newFactory(t, p), stubClock{})
61- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))62+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
62 63
63 var hg sync.WaitGroup64 var hg sync.WaitGroup
64 hg.Go(func() { d.handleConn(tr) })65 hg.Go(func() { d.handleConn(tr) })
@@ -73,7 +74,7 @@ func TestHandshakeNewHappy(t *testing.T) {
73 require.NotNil(t, sess)74 require.NotNil(t, sess)
74 75
75 releaseConn()76 releaseConn()
76- _ = d.killSession(sess, ports.ReasonServerShutdown, false)77+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
77 releasePTY()78 releasePTY()
78 hg.Wait()79 hg.Wait()
79 d.sessWg.Wait()80 d.sessWg.Wait()
@@ -83,8 +84,8 @@ func TestHandshakeNewHappy(t *testing.T) {
83 func TestHandshakeVersionMismatch(t *testing.T) {84 func TestHandshakeVersionMismatch(t *testing.T) {
84 // No Open expectation: the factory must never be asked to spawn a PTY.85 // No Open expectation: the factory must never be asked to spawn a PTY.
85 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})86 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
86- bad := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{87+ bad := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(protocol.Hello{
87- Version: ports.ProtocolVersion + 99, Intent: ports.IntentEphemeral, Size: domain.Size{Cols: 80, Rows: 24},88+ Version: protocol.Version + 99, Intent: protocol.IntentEphemeral, Size: domain.Size{Cols: 80, Rows: 24},
88 })}89 })}
89 tr, sends, _ := newConn(t, bad)90 tr, sends, _ := newConn(t, bad)
90 91
@@ -93,15 +94,15 @@ func TestHandshakeVersionMismatch(t *testing.T) {
93 e := awaitFrame(t, sends, ports.MsgError)94 e := awaitFrame(t, sends, ports.MsgError)
94 em, err := ports.UnmarshalErrorMsg(e.Payload)95 em, err := ports.UnmarshalErrorMsg(e.Payload)
95 require.NoError(t, err)96 require.NoError(t, err)
96- require.Equal(t, ports.ErrVersionMismatch, em.Code)97+ require.Equal(t, protocol.ErrVersionMismatch, em.Code)
97 require.Equal(t, 0, sessionCount(d))98 require.Equal(t, 0, sessionCount(d))
98 }99 }
99 100
100 func TestHandshakeOldHelloLayoutReportsVersionMismatch(t *testing.T) {101 func TestHandshakeOldHelloLayoutReportsVersionMismatch(t *testing.T) {
101 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})102 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
102 oldLayout := []byte{103 oldLayout := []byte{
103- 0x00, byte(ports.ProtocolVersion - 1),104+ 0x00, byte(protocol.Version - 1),
104- ports.IntentEphemeral,105+ protocol.IntentEphemeral,
105 0x00, 0x00,106 0x00, 0x00,
106 0x00, 0x50,107 0x00, 0x50,
107 0x00, 0x18,108 0x00, 0x18,
@@ -113,7 +114,7 @@ func TestHandshakeOldHelloLayoutReportsVersionMismatch(t *testing.T) {
113 e := awaitFrame(t, sends, ports.MsgError)114 e := awaitFrame(t, sends, ports.MsgError)
114 em, err := ports.UnmarshalErrorMsg(e.Payload)115 em, err := ports.UnmarshalErrorMsg(e.Payload)
115 require.NoError(t, err)116 require.NoError(t, err)
116- require.Equal(t, ports.ErrVersionMismatch, em.Code)117+ require.Equal(t, protocol.ErrVersionMismatch, em.Code)
117 require.Equal(t, 0, sessionCount(d))118 require.Equal(t, 0, sessionCount(d))
118 }119 }
119 120
@@ -122,27 +123,27 @@ func TestHandshakeNameTaken(t *testing.T) {
122 // Pre-populate a bare session (no goroutines) to collide with.123 // Pre-populate a bare session (no goroutines) to collide with.
123 d.sessions[domain.SessionID("x")] = &session{sessionCore: sessionCore{id: "x", name: "work"}}124 d.sessions[domain.SessionID("x")] = &session{sessionCore: sessionCore{id: "x", name: "work"}}
124 125
125- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))126+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
126 d.handleConn(tr)127 d.handleConn(tr)
127 128
128 e := awaitFrame(t, sends, ports.MsgError)129 e := awaitFrame(t, sends, ports.MsgError)
129 em, _ := ports.UnmarshalErrorMsg(e.Payload)130 em, _ := ports.UnmarshalErrorMsg(e.Payload)
130- require.Equal(t, ports.ErrNameTaken, em.Code)131+ require.Equal(t, protocol.ErrNameTaken, em.Code)
131 }132 }
132 133
133 func TestHandshakeNoSuchSession(t *testing.T) {134 func TestHandshakeNoSuchSession(t *testing.T) {
134 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})135 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
135- tr, sends, _ := newConn(t, mustHello(ports.IntentAttach, "ghost", domain.Size{Cols: 80, Rows: 24}))136+ tr, sends, _ := newConn(t, mustHello(protocol.IntentAttach, "ghost", domain.Size{Cols: 80, Rows: 24}))
136 d.handleConn(tr)137 d.handleConn(tr)
137 138
138 e := awaitFrame(t, sends, ports.MsgError)139 e := awaitFrame(t, sends, ports.MsgError)
139 em, _ := ports.UnmarshalErrorMsg(e.Payload)140 em, _ := ports.UnmarshalErrorMsg(e.Payload)
140- require.Equal(t, ports.ErrNoSuchSession, em.Code)141+ require.Equal(t, protocol.ErrNoSuchSession, em.Code)
141 }142 }
142 143
143 func TestKillAllEmptyDaemonSignalsShutdown(t *testing.T) {144 func TestKillAllEmptyDaemonSignalsShutdown(t *testing.T) {
144 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})145 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
145- tr, _, _ := newConn(t, ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{All: true})})146+ tr, _, _ := newConn(t, ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(protocol.Kill{All: true})})
146 d.handleConn(tr)147 d.handleConn(tr)
147 148
148 select {149 select {
@@ -157,18 +158,18 @@ func TestCreateTabForAttachmentUsesRequestingAttachmentViewportAndTheme(t *testi
157 defer release()158 defer release()
158 d := newTestDaemon(t, newFactory(t, p), stubClock{})159 d := newTestDaemon(t, newFactory(t, p), stubClock{})
159 firstTransport := &closeTrackingTransport{}160 firstTransport := &closeTrackingTransport{}
160- sess, first, err := d.route(ports.Hello{161+ sess, first, err := d.route(protocol.Hello{
161- Version: ports.ProtocolVersion,162+ Version: protocol.Version,
162- Intent: ports.IntentNew,163+ Intent: protocol.IntentNew,
163 Name: "work",164 Name: "work",
164 ClientID: [16]byte{1},165 ClientID: [16]byte{1},
165 Size: domain.Size{Cols: 80, Rows: 24},166 Size: domain.Size{Cols: 80, Rows: 24},
166 }, firstTransport)167 }, firstTransport)
167 require.NoError(t, err)168 require.NoError(t, err)
168 secondTransport := &closeTrackingTransport{}169 secondTransport := &closeTrackingTransport{}
169- _, second, err := d.route(ports.Hello{170+ _, second, err := d.route(protocol.Hello{
170- Version: ports.ProtocolVersion,171+ Version: protocol.Version,
171- Intent: ports.IntentAttach,172+ Intent: protocol.IntentAttach,
172 Name: "work",173 Name: "work",
173 ClientID: [16]byte{2},174 ClientID: [16]byte{2},
174 Size: domain.Size{Cols: 80, Rows: 24},175 Size: domain.Size{Cols: 80, Rows: 24},
@@ -194,7 +195,7 @@ func TestCreateTabForAttachmentUsesRequestingAttachmentViewportAndTheme(t *testi
194 require.Equal(t, contentSize(domain.Size{Cols: 80, Rows: 24}), gotSize, "sizing must come from the requesting attachment")195 require.Equal(t, contentSize(domain.Size{Cols: 80, Rows: 24}), gotSize, "sizing must come from the requesting attachment")
195 assertPaneDefaultColors(t, pane, requesterTheme.Foreground, requesterTheme.Background)196 assertPaneDefaultColors(t, pane, requesterTheme.Foreground, requesterTheme.Background)
196 197
197- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))198+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
198 d.sessWg.Wait()199 d.sessWg.Wait()
199 }200 }
200 201
@@ -228,7 +229,7 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) {
228 ).Once()229 ).Once()
229 230
230 d := newTestDaemon(t, f, stubClock{})231 d := newTestDaemon(t, f, stubClock{})
231- tr, sends, releaseConn := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))232+ tr, sends, releaseConn := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
232 var hg sync.WaitGroup233 var hg sync.WaitGroup
233 hg.Go(func() { d.handleConn(tr) })234 hg.Go(func() { d.handleConn(tr) })
234 awaitFrame(t, sends, ports.MsgWelcome)235 awaitFrame(t, sends, ports.MsgWelcome)
@@ -239,7 +240,7 @@ func TestCreateTabClosesPTYIfSessionKilledDuringOpen(t *testing.T) {
239 errCh := make(chan error, 1)240 errCh := make(chan error, 1)
240 go func() { errCh <- d.createTab(sess, domain.Size{Cols: 80, Rows: 24}) }()241 go func() { errCh <- d.createTab(sess, domain.Size{Cols: 80, Rows: 24}) }()
241 ctx := <-openCtx242 ctx := <-openCtx
242- _ = d.killSession(sess, ports.ReasonSessionKilled, false)243+ _ = d.killSession(sess, protocol.ReasonSessionKilled, false)
243 244
244 cancelled := true245 cancelled := true
245 select {246 select {
@@ -287,8 +288,8 @@ func TestDetachKeepsEphemeralHeadless(t *testing.T) {
287 p, releasePTY := newBlockingPTY(t)288 p, releasePTY := newBlockingPTY(t)
288 d := newTestDaemon(t, newFactory(t, p), stubClock{})289 d := newTestDaemon(t, newFactory(t, p), stubClock{})
289 tr, sends, _ := newConn(t,290 tr, sends, _ := newConn(t,
290- mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}),291+ mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}),
291- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})},292+ ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})},
292 )293 )
293 294
294 var hg sync.WaitGroup295 var hg sync.WaitGroup
@@ -305,7 +306,7 @@ func TestDetachKeepsEphemeralHeadless(t *testing.T) {
305 require.Empty(t, sess.snapshotAttachmentsLocked(), "ephemeral session is headless after detach")306 require.Empty(t, sess.snapshotAttachmentsLocked(), "ephemeral session is headless after detach")
306 sess.mu.Unlock()307 sess.mu.Unlock()
307 308
308- _ = d.killSession(sess, ports.ReasonServerShutdown, false)309+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
309 releasePTY()310 releasePTY()
310 d.sessWg.Wait()311 d.sessWg.Wait()
311 d.waitNotifies()312 d.waitNotifies()
@@ -317,7 +318,7 @@ func TestListShowsDetachedEphemeralSession(t *testing.T) {
317 d := newTestDaemon(t, newFactory(t, p), stubClock{})318 d := newTestDaemon(t, newFactory(t, p), stubClock{})
318 319
319 tr := &closeTrackingTransport{}320 tr := &closeTrackingTransport{}
320- sess, ac, err := d.route(helloResumeCapable(ports.IntentEphemeral, "", 0), tr)321+ sess, ac, err := d.route(helloResumeCapable(protocol.IntentEphemeral, "", 0), tr)
321 require.NoError(t, err)322 require.NoError(t, err)
322 d.clientGone(sess, ac, ac.transport(), true)323 d.clientGone(sess, ac, ac.transport(), true)
323 324
@@ -327,7 +328,7 @@ func TestListShowsDetachedEphemeralSession(t *testing.T) {
327 require.True(t, got.Sessions[0].Ephemeral)328 require.True(t, got.Sessions[0].Ephemeral)
328 require.False(t, got.Sessions[0].Attached)329 require.False(t, got.Sessions[0].Attached)
329 330
330- _ = d.killSession(sess, ports.ReasonServerShutdown, false)331+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
331 d.sessWg.Wait()332 d.sessWg.Wait()
332 d.waitNotifies()333 d.waitNotifies()
333 }334 }
@@ -336,8 +337,8 @@ func TestDetachKeepsNamed(t *testing.T) {
336 p, releasePTY := newBlockingPTY(t)337 p, releasePTY := newBlockingPTY(t)
337 d := newTestDaemon(t, newFactory(t, p), stubClock{})338 d := newTestDaemon(t, newFactory(t, p), stubClock{})
338 tr, sends, _ := newConn(t,339 tr, sends, _ := newConn(t,
339- mustHello(ports.IntentNew, "keep", domain.Size{Cols: 80, Rows: 24}),340+ mustHello(protocol.IntentNew, "keep", domain.Size{Cols: 80, Rows: 24}),
340- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})},341+ ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})},
341 )342 )
342 343
343 var hg sync.WaitGroup344 var hg sync.WaitGroup
@@ -352,7 +353,7 @@ func TestDetachKeepsNamed(t *testing.T) {
352 require.Empty(t, sess.snapshotAttachmentsLocked(), "named session is headless after detach")353 require.Empty(t, sess.snapshotAttachmentsLocked(), "named session is headless after detach")
353 sess.mu.Unlock()354 sess.mu.Unlock()
354 355
355- _ = d.killSession(sess, ports.ReasonServerShutdown, false)356+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
356 releasePTY()357 releasePTY()
357 d.sessWg.Wait()358 d.sessWg.Wait()
358 d.waitNotifies()359 d.waitNotifies()
@@ -368,7 +369,7 @@ func TestReaderEOFRemovesSessionAndSignalsShutdown(t *testing.T) {
368 p.EXPECT().Pid().Return(1).Maybe()369 p.EXPECT().Pid().Return(1).Maybe()
369 370
370 d := newTestDaemon(t, newFactory(t, p), stubClock{})371 d := newTestDaemon(t, newFactory(t, p), stubClock{})
371- tr, sends, _ := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))372+ tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
372 373
373 var hg sync.WaitGroup374 var hg sync.WaitGroup
374 hg.Go(func() { d.handleConn(tr) })375 hg.Go(func() { d.handleConn(tr) })
@@ -376,7 +377,7 @@ func TestReaderEOFRemovesSessionAndSignalsShutdown(t *testing.T) {
376 // The client is detached with ReasonSessionKilled when the child exits.377 // The client is detached with ReasonSessionKilled when the child exits.
377 det := awaitFrame(t, sends, ports.MsgDetached)378 det := awaitFrame(t, sends, ports.MsgDetached)
378 dm, _ := ports.UnmarshalDetached(det.Payload)379 dm, _ := ports.UnmarshalDetached(det.Payload)
379- require.Equal(t, ports.ReasonSessionKilled, dm.Reason)380+ require.Equal(t, protocol.ReasonSessionKilled, dm.Reason)
380 381
381 select {382 select {
382 case <-d.done:383 case <-d.done:
@@ -394,7 +395,7 @@ func TestReaderEOFRemovesSessionAndSignalsShutdown(t *testing.T) {
394 395
395 func TestServeReturnsWhenLastSessionExits(t *testing.T) {396 func TestServeReturnsWhenLastSessionExits(t *testing.T) {
396 p, releasePTY := newBlockingPTY(t)397 p, releasePTY := newBlockingPTY(t)
397- tr, sends, _ := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))398+ tr, sends, _ := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
398 399
399 l := portsmocks.NewMockListener(t)400 l := portsmocks.NewMockListener(t)
400 connCh := make(chan ports.Transport, 1)401 connCh := make(chan ports.Transport, 1)
@@ -434,7 +435,7 @@ func TestServeReturnsWhenLastSessionExits(t *testing.T) {
434 func TestServeGracefulShutdownOnContextCancel(t *testing.T) {435 func TestServeGracefulShutdownOnContextCancel(t *testing.T) {
435 p, releasePTY := newBlockingPTY(t)436 p, releasePTY := newBlockingPTY(t)
436 defer releasePTY()437 defer releasePTY()
437- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "long", domain.Size{Cols: 80, Rows: 24}))438+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "long", domain.Size{Cols: 80, Rows: 24}))
438 439
439 l := portsmocks.NewMockListener(t)440 l := portsmocks.NewMockListener(t)
440 connCh := make(chan ports.Transport, 1)441 connCh := make(chan ports.Transport, 1)
@@ -465,7 +466,7 @@ func TestServeGracefulShutdownOnContextCancel(t *testing.T) {
465 466
466 det := awaitFrame(t, sends, ports.MsgDetached)467 det := awaitFrame(t, sends, ports.MsgDetached)
467 dm, _ := ports.UnmarshalDetached(det.Payload)468 dm, _ := ports.UnmarshalDetached(det.Payload)
468- require.Equal(t, ports.ReasonServerShutdown, dm.Reason)469+ require.Equal(t, protocol.ReasonServerShutdown, dm.Reason)
469 470
470 select {471 select {
471 case err := <-served:472 case err := <-served:
@@ -511,7 +512,7 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
511 },512 },
512 ).Maybe()513 ).Maybe()
513 514
514- tr1, sends1, release1 := newConn(t, mustHello(ports.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))515+ tr1, sends1, release1 := newConn(t, mustHello(protocol.IntentEphemeral, "", domain.Size{Cols: 80, Rows: 24}))
515 var hg sync.WaitGroup516 var hg sync.WaitGroup
516 hg.Go(func() { d.handleConn(tr1) })517 hg.Go(func() { d.handleConn(tr1) })
517 awaitFrame(t, sends1, ports.MsgWelcome)518 awaitFrame(t, sends1, ports.MsgWelcome)
@@ -528,7 +529,7 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
528 require.NotNil(t, sess)529 require.NotNil(t, sess)
529 530
530 // The last session dies: shutdown begins irreversibly.531 // The last session dies: shutdown begins irreversibly.
531- _ = d.killSession(sess, ports.ReasonSessionKilled, false)532+ _ = d.killSession(sess, protocol.ReasonSessionKilled, false)
532 select {533 select {
533 case <-d.done:534 case <-d.done:
534 default:535 default:
@@ -542,15 +543,15 @@ func TestHelloRacingShutdownIsRejected(t *testing.T) {
542 intent uint8543 intent uint8
543 sess string544 sess string
544 }{545 }{
545- {"ephemeral", ports.IntentEphemeral, ""},546+ {"ephemeral", protocol.IntentEphemeral, ""},
546- {"named", ports.IntentNew, "latecomer"},547+ {"named", protocol.IntentNew, "latecomer"},
547 } {548 } {
548 tr2, sends2, _ := newConn(t, mustHello(intent.intent, intent.sess, domain.Size{Cols: 80, Rows: 24}))549 tr2, sends2, _ := newConn(t, mustHello(intent.intent, intent.sess, domain.Size{Cols: 80, Rows: 24}))
549 d.handleConn(tr2)550 d.handleConn(tr2)
550 e := awaitFrame(t, sends2, ports.MsgError)551 e := awaitFrame(t, sends2, ports.MsgError)
551 em, err := ports.UnmarshalErrorMsg(e.Payload)552 em, err := ports.UnmarshalErrorMsg(e.Payload)
552 require.NoError(t, err)553 require.NoError(t, err)
553- require.Equal(t, ports.ErrServerShutdown, em.Code, "%s hello racing shutdown must be rejected", intent.name)554+ require.Equal(t, protocol.ErrServerShutdown, em.Code, "%s hello racing shutdown must be rejected", intent.name)
554 }555 }
555 require.Equal(t, 0, sessionCount(d), "no session may be inserted after shutdown began")556 require.Equal(t, 0, sessionCount(d), "no session may be inserted after shutdown began")
556 require.Zero(t, opensAfterShutdown.Load(), "no PTYFactory.Open may start after d.done/shutdown completion")557 require.Zero(t, opensAfterShutdown.Load(), "no PTYFactory.Open may start after d.done/shutdown completion")
@@ -574,7 +575,7 @@ func TestServeReturnsDespiteWedgedClientOnShutdown(t *testing.T) {
574 tr := portsmocks.NewMockTransport(t)575 tr := portsmocks.NewMockTransport(t)
575 sends := make(chan ports.Frame, 64)576 sends := make(chan ports.Frame, 64)
576 recvCh := make(chan ports.Frame, 1)577 recvCh := make(chan ports.Frame, 1)
577- recvCh <- mustHello(ports.IntentNew, "wedge", domain.Size{Cols: 80, Rows: 24})578+ recvCh <- mustHello(protocol.IntentNew, "wedge", domain.Size{Cols: 80, Rows: 24})
578 connDone := make(chan struct{})579 connDone := make(chan struct{})
579 var connOnce sync.Once580 var connOnce sync.Once
580 closeConn := func() { connOnce.Do(func() { close(connDone) }) }581 closeConn := func() { connOnce.Do(func() { close(connDone) }) }
@@ -779,7 +780,7 @@ func TestDaemonLoadsPersistedSessionsAsStopped(t *testing.T) {
779 require.Equal(t, int64(7), stopped.createdAt)780 require.Equal(t, int64(7), stopped.createdAt)
780 require.Equal(t, incarnation, stopped.incarnation)781 require.Equal(t, incarnation, stopped.incarnation)
781 require.Equal(t, uint64(9), stopped.lastUsedSeq)782 require.Equal(t, uint64(9), stopped.lastUsedSeq)
782- require.Equal(t, ports.SessionDown, stopped.state)783+ require.Equal(t, protocol.SessionDown, stopped.state)
783 require.NotNil(t, stopped.restoreDone)784 require.NotNil(t, stopped.restoreDone)
784 require.Equal(t, uint64(9), d.mruSeq.Load())785 require.Equal(t, uint64(9), d.mruSeq.Load())
785 }786 }
@@ -905,7 +906,7 @@ func TestCreateRenameKillPersistenceLifecycle(t *testing.T) {
905 require.Equal(t, created.IncarnationID, renamed.IncarnationID)906 require.Equal(t, created.IncarnationID, renamed.IncarnationID)
906 require.NoError(t, renamed.Validate())907 require.NoError(t, renamed.Validate())
907 908
908- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, true))909+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, true))
909 require.False(t, state.has("renamed"))910 require.False(t, state.has("renamed"))
910 }911 }
911 912
@@ -1018,12 +1019,12 @@ func TestAttachRestoresPersistedTabNames(t *testing.T) {
1018 record, ok, err := d.catalogueRecord("work")1019 record, ok, err := d.catalogueRecord("work")
1019 require.NoError(t, err)1020 require.NoError(t, err)
1020 require.True(t, ok)1021 require.True(t, ok)
1021- d.inactive["work"] = inactiveSessionFromRecord(record, ports.SessionDown, nil)1022+ d.inactive["work"] = inactiveSessionFromRecord(record, protocol.SessionDown, nil)
1022 tr := portsmocks.NewMockTransport(t)1023 tr := portsmocks.NewMockTransport(t)
1023 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1024 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1024 tr.EXPECT().Close().Return(nil).Maybe()1025 tr.EXPECT().Close().Return(nil).Maybe()
1025 1026
1026- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz}, tr)1027+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr)
1027 require.NoError(t, err)1028 require.NoError(t, err)
1028 require.NotNil(t, ac)1029 require.NotNil(t, ac)
1029 require.Len(t, sess.tabs, 2)1030 require.Len(t, sess.tabs, 2)
@@ -1088,7 +1089,7 @@ func TestEphemeralPromotionLifecyclePreventsStaleSameNamePaletteTarget(t *testin
1088 require.NoError(t, d.renameSession(first, "named"))1089 require.NoError(t, d.renameSession(first, "named"))
1089 staleCreatedAt := first.createdAt1090 staleCreatedAt := first.createdAt
1090 require.NotZero(t, staleCreatedAt)1091 require.NotZero(t, staleCreatedAt)
1091- require.NoError(t, d.killSession(first, ports.ReasonSessionKilled, true))1092+ require.NoError(t, d.killSession(first, protocol.ReasonSessionKilled, true))
1092 d.sessWg.Wait()1093 d.sessWg.Wait()
1093 1094
1094 second, err := createSessionForTest(d, "0", true, "/tmp", ac.size, terminalEnv{}, d.baseEnv)1095 second, err := createSessionForTest(d, "0", true, "/tmp", ac.size, terminalEnv{}, d.baseEnv)
@@ -1192,12 +1193,12 @@ func TestAttachResumesStoppedSessionFromStoredCwd(t *testing.T) {
1192 d := newTestDaemon(t, newFactory(t, p), stubClock{})1193 d := newTestDaemon(t, newFactory(t, p), stubClock{})
1193 WithStore(t, store)(d)1194 WithStore(t, store)(d)
1194 cwd := t.TempDir()1195 cwd := t.TempDir()
1195- d.inactive["work"] = inactiveSession{name: "work", cwd: cwd, createdAt: 1, state: ports.SessionDown}1196+ d.inactive["work"] = inactiveSession{name: "work", cwd: cwd, createdAt: 1, state: protocol.SessionDown}
1196 tr := portsmocks.NewMockTransport(t)1197 tr := portsmocks.NewMockTransport(t)
1197 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1198 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1198 tr.EXPECT().Close().Return(nil).Maybe()1199 tr.EXPECT().Close().Return(nil).Maybe()
1199 1200
1200- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz}, tr)1201+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr)
1201 require.NoError(t, err)1202 require.NoError(t, err)
1202 require.NotNil(t, ac)1203 require.NotNil(t, ac)
1203 require.Equal(t, "work", sess.name)1204 require.Equal(t, "work", sess.name)
@@ -1215,12 +1216,12 @@ func TestAttachStoppedMissingCwdFallsBackToHome(t *testing.T) {
1215 store, _ := newMockStore(t)1216 store, _ := newMockStore(t)
1216 d := newTestDaemon(t, newFactory(t, p), stubClock{})1217 d := newTestDaemon(t, newFactory(t, p), stubClock{})
1217 WithStore(t, store)(d)1218 WithStore(t, store)(d)
1218- d.inactive["work"] = inactiveSession{name: "work", cwd: "/definitely/missing/vev", createdAt: 1, state: ports.SessionDown}1219+ d.inactive["work"] = inactiveSession{name: "work", cwd: "/definitely/missing/vev", createdAt: 1, state: protocol.SessionDown}
1219 tr := portsmocks.NewMockTransport(t)1220 tr := portsmocks.NewMockTransport(t)
1220 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1221 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1221 tr.EXPECT().Close().Return(nil).Maybe()1222 tr.EXPECT().Close().Return(nil).Maybe()
1222 1223
1223- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz}, tr)1224+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz}, tr)
1224 require.NoError(t, err)1225 require.NoError(t, err)
1225 require.NotEqual(t, "/definitely/missing/vev", sess.cwd)1226 require.NotEqual(t, "/definitely/missing/vev", sess.cwd)
1226 }1227 }
@@ -1266,7 +1267,7 @@ func TestNewSessionAssignsStableIDsAndChildEnv(t *testing.T) {
1266 sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{TrueColor: true}, d.baseEnv)1267 sess, err := createSessionForTest(d, "work", false, "/tmp/work", sz, terminalEnv{TrueColor: true}, d.baseEnv)
1267 require.NoError(t, err)1268 require.NoError(t, err)
1268 defer func() {1269 defer func() {
1269- _ = d.killSession(sess, ports.ReasonServerShutdown, false)1270+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
1270 releasePTY()1271 releasePTY()
1271 d.sessWg.Wait()1272 d.sessWg.Wait()
1272 }()1273 }()
@@ -1298,14 +1299,14 @@ func TestIntentNewStoppedNameRejected(t *testing.T) {
1298 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})1299 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
1299 d.inactive["taken"] = inactiveSession{name: "taken", cwd: "/tmp", createdAt: 1}1300 d.inactive["taken"] = inactiveSession{name: "taken", cwd: "/tmp", createdAt: 1}
1300 tr := portsmocks.NewMockTransport(t)1301 tr := portsmocks.NewMockTransport(t)
1301- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "taken", Size: domain.Size{Cols: 80, Rows: 24}}, tr)1302+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "taken", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
1302 require.ErrorContains(t, err, "name already in use")1303 require.ErrorContains(t, err, "name already in use")
1303 }1304 }
1304 1305
1305 func TestIntentNewUnsafeNameRejected(t *testing.T) {1306 func TestIntentNewUnsafeNameRejected(t *testing.T) {
1306 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})1307 d := newTestDaemon(t, portsmocks.NewMockPTYFactory(t), stubClock{})
1307 tr := portsmocks.NewMockTransport(t)1308 tr := portsmocks.NewMockTransport(t)
1308- _, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "my work", Size: domain.Size{Cols: 80, Rows: 24}}, tr)1309+ _, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "my work", Size: domain.Size{Cols: 80, Rows: 24}}, tr)
1309 require.ErrorContains(t, err, domain.ErrInvalidSessionName.Error())1310 require.ErrorContains(t, err, domain.ErrInvalidSessionName.Error())
1310 }1311 }
1311 1312
@@ -1363,14 +1364,14 @@ func TestNaturalExitStoppedButExplicitKillPurges(t *testing.T) {
1363 require.NoError(t, err)1364 require.NoError(t, err)
1364 other, err := createSessionForTest(d, "other", false, "/tmp/other", sz, terminalEnv{}, d.baseEnv)1365 other, err := createSessionForTest(d, "other", false, "/tmp/other", sz, terminalEnv{}, d.baseEnv)
1365 require.NoError(t, err)1366 require.NoError(t, err)
1366- _ = d.killSession(natural, ports.ReasonSessionKilled, false)1367+ _ = d.killSession(natural, protocol.ReasonSessionKilled, false)
1367 require.True(t, state.has("natural"))1368 require.True(t, state.has("natural"))
1368 d.mu.Lock()1369 d.mu.Lock()
1369 stopped := d.inactive["natural"]1370 stopped := d.inactive["natural"]
1370 d.mu.Unlock()1371 d.mu.Unlock()
1371 require.Equal(t, "/tmp/latest", stopped.cwd)1372 require.Equal(t, "/tmp/latest", stopped.cwd)
1372 1373
1373- _ = d.killSession(other, ports.ReasonSessionKilled, true)1374+ _ = d.killSession(other, protocol.ReasonSessionKilled, true)
1374 require.False(t, state.has("other"))1375 require.False(t, state.has("other"))
1375 }1376 }
1376 1377
@@ -1447,10 +1448,10 @@ func TestAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
1447 tr := portsmocks.NewMockTransport(t)1448 tr := portsmocks.NewMockTransport(t)
1448 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1449 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1449 tr.EXPECT().Close().Return(nil).Maybe()1450 tr.EXPECT().Close().Return(nil).Maybe()
1450- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)1451+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
1451 require.NoError(t, err)1452 require.NoError(t, err)
1452 defer func() {1453 defer func() {
1453- _ = d.killSession(sess, ports.ReasonServerShutdown, false)1454+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
1454 d.sessWg.Wait()1455 d.sessWg.Wait()
1455 }()1456 }()
1456 1457
@@ -1492,12 +1493,12 @@ func TestLiveAttachUpdatesFutureChildEnvTrueColor(t *testing.T) {
1492 tr2 := portsmocks.NewMockTransport(t)1493 tr2 := portsmocks.NewMockTransport(t)
1493 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()1494 tr2.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1494 tr2.EXPECT().Close().Return(nil).Maybe()1495 tr2.EXPECT().Close().Return(nil).Maybe()
1495- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1)1496+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: false}, tr1)
1496 require.NoError(t, err)1497 require.NoError(t, err)
1497- _, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz, TrueColor: true}, tr2)1498+ _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz, TrueColor: true}, tr2)
1498 require.NoError(t, err)1499 require.NoError(t, err)
1499 defer func() {1500 defer func() {
1500- _ = d.killSession(sess, ports.ReasonServerShutdown, false)1501+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
1501 d.sessWg.Wait()1502 d.sessWg.Wait()
1502 }()1503 }()
1503 1504
@@ -1533,7 +1534,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) {
1533 tr := portsmocks.NewMockTransport(t)1534 tr := portsmocks.NewMockTransport(t)
1534 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()1535 tr.EXPECT().Send(mock.Anything).Return(nil).Maybe()
1535 tr.EXPECT().Close().Return(nil).Maybe()1536 tr.EXPECT().Close().Return(nil).Maybe()
1536- sess, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)1537+ sess, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, TrueColor: true}, tr)
1537 require.NoError(t, err)1538 require.NoError(t, err)
1538 // The source coordinator is deliberately made pending so the switch must1539 // The source coordinator is deliberately made pending so the switch must
1539 // invalidate it rather than letting its stale callback render for ac.1540 // invalidate it rather than letting its stale callback render for ac.
@@ -1569,7 +1570,7 @@ func TestCreateSessionAndSwitchInheritsTerminalEnv(t *testing.T) {
1569 require.Contains(t, opens[1], "TERM=xterm-256color")1570 require.Contains(t, opens[1], "TERM=xterm-256color")
1570 require.Contains(t, opens[1], "COLORTERM=truecolor")1571 require.Contains(t, opens[1], "COLORTERM=truecolor")
1571 require.Contains(t, opens[1], "TERM_PROGRAM=vev")1572 require.Contains(t, opens[1], "TERM_PROGRAM=vev")
1572- _ = d.killSession(got, ports.ReasonSessionKilled, false)1573+ _ = d.killSession(got, protocol.ReasonSessionKilled, false)
1573 release1()1574 release1()
1574 release2()1575 release2()
1575 d.sessWg.Wait()1576 d.sessWg.Wait()
@@ -1607,13 +1608,13 @@ func TestAttachEnvironmentReplacesFuturePTYInputs(t *testing.T) {
1607 tr2.EXPECT().Close().Return(nil).Maybe()1608 tr2.EXPECT().Close().Return(nil).Maybe()
1608 1609
1609 first := []string{"SECRET=first", "TERM=bad", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b"}1610 first := []string{"SECRET=first", "TERM=bad", "TERM_PROGRAM_extra=keep", "SHELL=/usr/bin/fish", "A=a=b"}
1610- sess, _, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work", Size: sz, Env: first}, tr1)1611+ sess, _, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "work", Size: sz, Env: first}, tr1)
1611 require.NoError(t, err)1612 require.NoError(t, err)
1612 second := []string{"SECRET=second", "TERM=bad", "TERM_PROGRAM_extra=keep", "SHELL=/bin/bash", "A=a=b"}1613 second := []string{"SECRET=second", "TERM=bad", "TERM_PROGRAM_extra=keep", "SHELL=/bin/bash", "A=a=b"}
1613- _, ac, err := d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work", Size: sz, Env: second}, tr2)1614+ _, ac, err := d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work", Size: sz, Env: second}, tr2)
1614 require.NoError(t, err)1615 require.NoError(t, err)
1615 defer func() {1616 defer func() {
1616- _ = d.killSession(sess, ports.ReasonServerShutdown, false)1617+ _ = d.killSession(sess, protocol.ReasonServerShutdown, false)
1617 d.sessWg.Wait()1618 d.sessWg.Wait()
1618 }()1619 }()
1619 1620
@@ -1702,7 +1703,7 @@ func TestNamedSessionLifecycleExhaustionDoesNotMutateSessionState(t *testing.T)
1702 require.Equal(t, int64(math.MaxInt64), d.lastAllocatedCreatedAt)1703 require.Equal(t, int64(math.MaxInt64), d.lastAllocatedCreatedAt)
1703 require.Equal(t, inactiveSession{name: "retained", cwd: "/tmp", createdAt: 9}, d.inactive["retained"])1704 require.Equal(t, inactiveSession{name: "retained", cwd: "/tmp", createdAt: 9}, d.inactive["retained"])
1704 1705
1705- _, _, err = d.route(ports.Hello{Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "routed", Size: domain.Size{Cols: 80, Rows: 24}}, nil)1706+ _, _, err = d.route(protocol.Hello{Version: protocol.Version, Intent: protocol.IntentNew, Name: "routed", Size: domain.Size{Cols: 80, Rows: 24}}, nil)
1706 require.ErrorContains(t, err, "lifecycle identities exhausted")1707 require.ErrorContains(t, err, "lifecycle identities exhausted")
1707 require.Empty(t, d.sessions)1708 require.Empty(t, d.sessions)
1708 require.Equal(t, uint64(17), d.nextID)1709 require.Equal(t, uint64(17), d.nextID)
@@ -1721,7 +1722,7 @@ func TestCatalogueRecordsConstructExpectedSessionRegistry(t *testing.T) {
1721 1722
1722 alpha := d.inactive["alpha"]1723 alpha := d.inactive["alpha"]
1723 require.Equal(t, records[0], alpha.record)1724 require.Equal(t, records[0], alpha.record)
1724- require.Equal(t, ports.SessionDown, alpha.state)1725+ require.Equal(t, protocol.SessionDown, alpha.state)
1725 require.NotNil(t, alpha.restoreDone)1726 require.NotNil(t, alpha.restoreDone)
1726 select {1727 select {
1727 case <-alpha.restoreDone:1728 case <-alpha.restoreDone:
@@ -1730,7 +1731,7 @@ func TestCatalogueRecordsConstructExpectedSessionRegistry(t *testing.T) {
1730 }1731 }
1731 work := d.inactive["work"]1732 work := d.inactive["work"]
1732 require.Equal(t, records[1], work.record)1733 require.Equal(t, records[1], work.record)
1733- require.Equal(t, ports.SessionBroken, work.state)1734+ require.Equal(t, protocol.SessionBroken, work.state)
1734 require.NotNil(t, work.restoreDone)1735 require.NotNil(t, work.restoreDone)
1735 select {1736 select {
1736 case <-work.restoreDone:1737 case <-work.restoreDone:
@@ -1768,7 +1769,7 @@ func TestResumingStoppedSessionPreservesLifecycleIdentityInPersistence(t *testin
1768 store, state := newMockStore(t)1769 store, state := newMockStore(t)
1769 WithStore(t, store)(d)1770 WithStore(t, store)(d)
1770 d.ptys = newFactory(t, targetPTY)1771 d.ptys = newFactory(t, targetPTY)
1771- d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 77, state: ports.SessionDown}1772+ d.inactive["stopped"] = inactiveSession{name: "stopped", cwd: "/tmp", createdAt: 77, state: protocol.SessionDown}
1772 1773
1773 require.True(t, d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "stopped", Stopped: true}))1774 require.True(t, d.resumeStoppedAndSwitch(from, ac, picker.Target{Name: "stopped", Stopped: true}))
1774 resumed := ac.currentSession()1775 resumed := ac.currentSession()
@@ -1796,7 +1797,7 @@ func TestLifecycleExpectedTargetChecksAreAtomicAcrossStateTransitions(t *testing
1796 d, from, ac, _ := newManualSessionWithPTYs(t, fromPTY)1797 d, from, ac, _ := newManualSessionWithPTYs(t, fromPTY)
1797 d.ptys = newFactory(t, targetPTY)1798 d.ptys = newFactory(t, targetPTY)
1798 expected := int64(31)1799 expected := int64(31)
1799- d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: expected, state: ports.SessionDown}1800+ d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: expected, state: protocol.SessionDown}
1800 1801
1801 require.NoError(t, d.switchToTarget(from, ac, picker.Target{Session: "old-active-id", Name: "target", TabIndex: 0, ExpectedCreatedAt: &expected}))1802 require.NoError(t, d.switchToTarget(from, ac, picker.Target{Session: "old-active-id", Name: "target", TabIndex: 0, ExpectedCreatedAt: &expected}))
1802 require.Equal(t, "target", ac.currentSession().name)1803 require.Equal(t, "target", ac.currentSession().name)
@@ -1818,7 +1819,7 @@ func TestLifecycleExpectedTargetChecksAreAtomicAcrossStateTransitions(t *testing
1818 fromPTY, releaseFrom := newBlockingPTY(t)1819 fromPTY, releaseFrom := newBlockingPTY(t)
1819 defer releaseFrom()1820 defer releaseFrom()
1820 d, from, ac, _ := newManualSessionWithPTYs(t, fromPTY)1821 d, from, ac, _ := newManualSessionWithPTYs(t, fromPTY)
1821- d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: 52, state: ports.SessionDown}1822+ d.inactive["target"] = inactiveSession{name: "target", cwd: "/tmp", createdAt: 52, state: protocol.SessionDown}
1822 expected := int64(51)1823 expected := int64(51)
1823 1824
1824 require.Error(t, d.switchToTarget(from, ac, picker.Target{Name: "target", TabIndex: 0, Stopped: true, ExpectedCreatedAt: &expected}))1825 require.Error(t, d.switchToTarget(from, ac, picker.Target{Name: "target", TabIndex: 0, Stopped: true, ExpectedCreatedAt: &expected}))
M internal/usecase/daemon/shared_pty_geometry_test.go +5 -5
@@ -4,7 +4,7 @@ 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 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
9 )9 )
10 10
@@ -45,14 +45,14 @@ func TestSharedPTYGeometryCharacterization(t *testing.T) {
45 t.Cleanup(releasePTY)45 t.Cleanup(releasePTY)
46 d := newTestDaemon(t, newFactory(t, pty), stubClock{})46 d := newTestDaemon(t, newFactory(t, pty), stubClock{})
47 firstTransport, _ := newCapturingTransport(t)47 firstTransport, _ := newCapturingTransport(t)
48- sess, first, err := d.route(ports.Hello{48+ sess, first, err := d.route(protocol.Hello{
49- Version: ports.ProtocolVersion, Intent: ports.IntentNew, Name: "work",49+ Version: protocol.Version, Intent: protocol.IntentNew, Name: "work",
50 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},50 Size: domain.Size{Cols: 80, Rows: 24}, ClientID: [16]byte{1},
51 }, firstTransport)51 }, firstTransport)
52 require.NoError(t, err)52 require.NoError(t, err)
53 secondTransport, _ := newCapturingTransport(t)53 secondTransport, _ := newCapturingTransport(t)
54- _, second, err := d.route(ports.Hello{54+ _, second, err := d.route(protocol.Hello{
55- Version: ports.ProtocolVersion, Intent: ports.IntentAttach, Name: "work",55+ Version: protocol.Version, Intent: protocol.IntentAttach, Name: "work",
56 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},56 Size: domain.Size{Cols: 100, Rows: 40}, ClientID: [16]byte{2},
57 }, secondTransport)57 }, secondTransport)
58 require.NoError(t, err)58 require.NoError(t, err)
M internal/usecase/daemon/snapshot_restore.go +2 -2
@@ -9,7 +9,7 @@ import (
9 9
10 vt "github.com/bnema/vev-vt"10 vt "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/protocol"
13 "github.com/bnema/vev/internal/usecase/layout"13 "github.com/bnema/vev/internal/usecase/layout"
14 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"14 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
15 )15 )
@@ -25,7 +25,7 @@ func (d *Daemon) closeRestoreDone() {
25 continue25 continue
26 default:26 default:
27 }27 }
28- entry.state = ports.SessionBroken28+ entry.state = protocol.SessionBroken
29 if entry.record.DegradedReason == "" {29 if entry.record.DegradedReason == "" {
30 entry.record.DegradedReason = "restore unavailable"30 entry.record.DegradedReason = "restore unavailable"
31 }31 }
M internal/usecase/daemon/snapshot_restore_incremental.go +11 -10
@@ -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 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"16 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
16 )17 )
17 18
@@ -19,11 +20,11 @@ const catalogueRestoreConcurrency = 8
19 20
20 var errRetryableRestoreLoad = errors.New("snapshot: retryable checkpoint load")21 var errRetryableRestoreLoad = errors.New("snapshot: retryable checkpoint load")
21 22
22-func initialSessionState(record domain.CatalogueRecord) (ports.SessionState, chan struct{}) {23+func initialSessionState(record domain.CatalogueRecord) (protocol.SessionState, chan struct{}) {
23 done := make(chan struct{})24 done := make(chan struct{})
24- state := ports.SessionDown25+ state := protocol.SessionDown
25 if record.DegradedReason != "" {26 if record.DegradedReason != "" {
26- state = ports.SessionBroken27+ state = protocol.SessionBroken
27 close(done)28 close(done)
28 } else if record.Committed == nil {29 } else if record.Committed == nil {
29 close(done)30 close(done)
@@ -68,7 +69,7 @@ func (d *Daemon) restoreCatalogue(ctx context.Context, records []domain.Catalogu
68 wg.Wait()69 wg.Wait()
69 }70 }
70 71
71-func inactiveSessionFromRecord(record domain.CatalogueRecord, state ports.SessionState, done chan struct{}) inactiveSession {72+func inactiveSessionFromRecord(record domain.CatalogueRecord, state protocol.SessionState, done chan struct{}) inactiveSession {
72 tabRecords := append([]domain.CatalogueTabRecord(nil), record.TabRecords...)73 tabRecords := append([]domain.CatalogueTabRecord(nil), record.TabRecords...)
73 tabNames := append([]string(nil), record.TabNames...)74 tabNames := append([]string(nil), record.TabNames...)
74 if len(tabRecords) != 0 {75 if len(tabRecords) != 0 {
@@ -123,7 +124,7 @@ func closeRuntimeRestoreDoneLocked(done chan struct{}) {
123 }124 }
124 }125 }
125 126
126-func (d *Daemon) setStoppedRecovery(record domain.CatalogueRecord, state ports.SessionState) {127+func (d *Daemon) setStoppedRecovery(record domain.CatalogueRecord, state protocol.SessionState) {
127 d.mu.Lock()128 d.mu.Lock()
128 defer d.mu.Unlock()129 defer d.mu.Unlock()
129 entry, ok := d.inactive[record.Name]130 entry, ok := d.inactive[record.Name]
@@ -156,7 +157,7 @@ func (d *Daemon) finishRecordRestore(record domain.CatalogueRecord, restoreErr e
156 157
157 d.mu.Lock()158 d.mu.Lock()
158 if entry, ok := d.inactive[record.Name]; ok && degraded {159 if entry, ok := d.inactive[record.Name]; ok && degraded {
159- d.inactive[record.Name] = inactiveSessionFromRecord(record, ports.SessionBroken, entry.restoreDone)160+ d.inactive[record.Name] = inactiveSessionFromRecord(record, protocol.SessionBroken, entry.restoreDone)
160 }161 }
161 closeRuntimeRestoreDoneLocked(done)162 closeRuntimeRestoreDoneLocked(done)
162 d.mu.Unlock()163 d.mu.Unlock()
@@ -174,12 +175,12 @@ func (d *Daemon) finishRecordRestore(record domain.CatalogueRecord, restoreErr e
174 175
175 func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecord) error {176 func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecord) error {
176 if record.DegradedReason != "" {177 if record.DegradedReason != "" {
177- d.setStoppedRecovery(record, ports.SessionBroken)178+ d.setStoppedRecovery(record, protocol.SessionBroken)
178 d.logSessionDegraded(record, "persisted-broken")179 d.logSessionDegraded(record, "persisted-broken")
179 return nil180 return nil
180 }181 }
181 if record.Committed == nil {182 if record.Committed == nil {
182- d.setStoppedRecovery(record, ports.SessionDown)183+ d.setStoppedRecovery(record, protocol.SessionDown)
183 d.logSessionRestoreComplete(record, 0, false)184 d.logSessionRestoreComplete(record, 0, false)
184 return nil185 return nil
185 }186 }
@@ -231,7 +232,7 @@ func (d *Daemon) restoreRecord(ctx context.Context, record domain.CatalogueRecor
231 if err := d.restoreSession(ctx, selectedSnapshot, selectedGeneration.Generation, selected); err != nil {232 if err := d.restoreSession(ctx, selectedSnapshot, selectedGeneration.Generation, selected); err != nil {
232 return err233 return err
233 }234 }
234- d.setStoppedRecovery(record, ports.SessionDown)235+ d.setStoppedRecovery(record, protocol.SessionDown)
235 d.logSessionRestoreComplete(record, selected.Generation, false)236 d.logSessionRestoreComplete(record, selected.Generation, false)
236 return nil237 return nil
237 }238 }
@@ -252,7 +253,7 @@ func (d *Daemon) resetIncompatibleCheckpoint(ctx context.Context, record domain.
252 253
253 d.mu.Lock()254 d.mu.Lock()
254 if entry, ok := d.inactive[record.Name]; ok {255 if entry, ok := d.inactive[record.Name]; ok {
255- d.inactive[record.Name] = inactiveSessionFromRecord(fresh, ports.SessionDown, entry.restoreDone)256+ d.inactive[record.Name] = inactiveSessionFromRecord(fresh, protocol.SessionDown, entry.restoreDone)
256 }257 }
257 d.mu.Unlock()258 d.mu.Unlock()
258 259
M internal/usecase/daemon/snapshot_resume_publication_test.go +8 -7
@@ -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 snapshotLifecycleRepository struct {18 type snapshotLifecycleRepository struct {
@@ -159,9 +160,9 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
159 d.sessWg.Wait()160 d.sessWg.Wait()
160 })161 })
161 162
162- _, _, err := d.route(snapshotLifecycleHello(ports.IntentEphemeral, ""), &closeTrackingTransport{})163+ _, _, err := d.route(snapshotLifecycleHello(protocol.IntentEphemeral, ""), &closeTrackingTransport{})
163 require.NoError(t, err, "keeper session setup failed")164 require.NoError(t, err, "keeper session setup failed")
164- sess, _, err := d.route(snapshotLifecycleHello(ports.IntentNew, "work"), &closeTrackingTransport{})165+ sess, _, err := d.route(snapshotLifecycleHello(protocol.IntentNew, "work"), &closeTrackingTransport{})
165 require.NoError(t, err)166 require.NoError(t, err)
166 167
167 testAttachmentTab(sess).focusedPane().screen.Write([]byte("first checkpoint"))168 testAttachmentTab(sess).focusedPane().screen.Write([]byte("first checkpoint"))
@@ -172,7 +173,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
172 require.NotNil(t, firstRecord.Committed)173 require.NotNil(t, firstRecord.Committed)
173 require.Equal(t, uint64(1), firstRecord.Committed.Generation)174 require.Equal(t, uint64(1), firstRecord.Committed.Generation)
174 175
175- require.NoError(t, d.killSession(sess, ports.ReasonSessionKilled, false))176+ require.NoError(t, d.killSession(sess, protocol.ReasonSessionKilled, false))
176 d.mu.Lock()177 d.mu.Lock()
177 stopped, retained := d.inactive[sess.name]178 stopped, retained := d.inactive[sess.name]
178 d.mu.Unlock()179 d.mu.Unlock()
@@ -183,7 +184,7 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
183 require.Equal(t, firstRecord.Committed.Generation+1, closedRecord.Committed.Generation,184 require.Equal(t, firstRecord.Committed.Generation+1, closedRecord.Committed.Generation,
184 "terminal checkpoint must be authoritative before same-daemon resume")185 "terminal checkpoint must be authoritative before same-daemon resume")
185 186
186- resumed, _, err := d.route(snapshotLifecycleHello(ports.IntentAttach, sess.name), &closeTrackingTransport{})187+ resumed, _, err := d.route(snapshotLifecycleHello(protocol.IntentAttach, sess.name), &closeTrackingTransport{})
187 require.NoError(t, err)188 require.NoError(t, err)
188 require.NotSame(t, sess, resumed, "stopped-session attach must create a new live runtime")189 require.NotSame(t, sess, resumed, "stopped-session attach must create a new live runtime")
189 require.Equal(t, sess.incarnation, resumed.incarnation, "resume must retain durable incarnation authority")190 require.Equal(t, sess.incarnation, resumed.incarnation, "resume must retain durable incarnation authority")
@@ -211,9 +212,9 @@ func TestStoppedNamedSessionResumePublishesNextCheckpointInSameDaemon(t *testing
211 "same-daemon resume must use the committed terminal checkpoint as parent")212 "same-daemon resume must use the committed terminal checkpoint as parent")
212 }213 }
213 214
214-func snapshotLifecycleHello(intent uint8, name string) ports.Hello {215+func snapshotLifecycleHello(intent uint8, name string) protocol.Hello {
215- return ports.Hello{216+ return protocol.Hello{
216- Version: ports.ProtocolVersion,217+ Version: protocol.Version,
217 Intent: intent,218 Intent: intent,
218 Name: name,219 Name: name,
219 Cwd: "/tmp",220 Cwd: "/tmp",
M internal/usecase/daemon/snapshot_scheduler_test.go +7 -6
@@ -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 )18 )
18 19
19 func TestSnapshotCoordinatorQuarantineCancelsPublicationBeforeDelete(t *testing.T) {20 func TestSnapshotCoordinatorQuarantineCancelsPublicationBeforeDelete(t *testing.T) {
@@ -247,7 +248,7 @@ func TestForcedSnapshotShutdownTimeoutRetainsRetryableStateAndNotice(t *testing.
247 require.True(t, d.scheduleSnapshot(sess), "routine capture should occupy the bounded queue")248 require.True(t, d.scheduleSnapshot(sess), "routine capture should occupy the bounded queue")
248 249
249 killed := make(chan error, 1)250 killed := make(chan error, 1)
250- go func() { killed <- d.killSession(sess, ports.ReasonServerShutdown, false) }()251+ go func() { killed <- d.killSession(sess, protocol.ReasonServerShutdown, false) }()
251 timer := clock.nextTimer(t)252 timer := clock.nextTimer(t)
252 timer.fire()253 timer.fire()
253 require.Error(t, <-killed)254 require.Error(t, <-killed)
@@ -303,7 +304,7 @@ func TestServeShutdownDeadlineStillJoinsPaneExitWriter(t *testing.T) {
303 return nil304 return nil
304 }).Once()305 }).Once()
305 306
306- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))307+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
307 listener := serveSnapshotListener(t, tr)308 listener := serveSnapshotListener(t, tr)
308 ctx, cancel := context.WithCancel(t.Context())309 ctx, cancel := context.WithCancel(t.Context())
309 defer cancel()310 defer cancel()
@@ -402,7 +403,7 @@ func TestConcurrentSessionTeardownPublishesOneFinalSnapshot(t *testing.T) {
402 d.sessions[sess.id] = sess403 d.sessions[sess.id] = sess
403 404
404 first := make(chan error, 1)405 first := make(chan error, 1)
405- go func() { first <- d.killSession(sess, ports.ReasonSessionKilled, false) }()406+ go func() { first <- d.killSession(sess, protocol.ReasonSessionKilled, false) }()
406 select {407 select {
407 case <-publicationStarted:408 case <-publicationStarted:
408 case <-time.After(testWaitTimeout):409 case <-time.After(testWaitTimeout):
@@ -418,7 +419,7 @@ func TestConcurrentSessionTeardownPublishesOneFinalSnapshot(t *testing.T) {
418 duplicates := make(chan error, duplicateCallers)419 duplicates := make(chan error, duplicateCallers)
419 for range duplicateCallers {420 for range duplicateCallers {
420 go func() {421 go func() {
421- duplicates <- d.killSession(sess, ports.ReasonSessionKilled, false)422+ duplicates <- d.killSession(sess, protocol.ReasonSessionKilled, false)
422 }()423 }()
423 }424 }
424 // Synchronize on the ownership state itself: every duplicate is waiting425 // Synchronize on the ownership state itself: every duplicate is waiting
@@ -468,7 +469,7 @@ func TestServeShutdownCheckpointsBeforeStoppingSnapshotWorker(t *testing.T) {
468 return nil469 return nil
469 }).Twice()470 }).Twice()
470 471
471- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))472+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
472 l := serveSnapshotListener(t, tr)473 l := serveSnapshotListener(t, tr)
473 ctx, cancel := context.WithCancel(t.Context())474 ctx, cancel := context.WithCancel(t.Context())
474 defer cancel()475 defer cancel()
@@ -525,7 +526,7 @@ func TestServeShutdownDeadlineStillJoinsUncooperativeSnapshotRepository(t *testi
525 return nil526 return nil
526 }).Twice()527 }).Twice()
527 528
528- tr, sends, _ := newConn(t, mustHello(ports.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))529+ tr, sends, _ := newConn(t, mustHello(protocol.IntentNew, "work", domain.Size{Cols: 80, Rows: 24}))
529 l := serveSnapshotListener(t, tr)530 l := serveSnapshotListener(t, tr)
530 ctx, cancel := context.WithCancel(t.Context())531 ctx, cancel := context.WithCancel(t.Context())
531 defer cancel()532 defer cancel()
M internal/usecase/daemon/status_test.go +1 -1
@@ -1441,7 +1441,7 @@ func TestStatusCoalescesCreateSwitchAndResize(t *testing.T) {
1441 clock := newCoordinatorMockClock(t, 64)1441 clock := newCoordinatorMockClock(t, 64)
1442 d := newTestDaemon(t, newFactorySeq(t, p1, p2), clock.clock)1442 d := newTestDaemon(t, newFactorySeq(t, p1, p2), clock.clock)
1443 tr, sends, releaseConn := newConn(t,1443 tr, sends, releaseConn := newConn(t,
1444- mustHello(ports.IntentNew, "work", domain.Size{Cols: 20, Rows: 5}),1444+ mustHello(protocol.IntentNew, "work", domain.Size{Cols: 20, Rows: 5}),
1445 frameInput([]byte("\x1b ")),1445 frameInput([]byte("\x1b ")),
1446 frameInput([]byte("CNT\r")),1446 frameInput([]byte("CNT\r")),
1447 frameInput([]byte("\x1b1")),1447 frameInput([]byte("\x1b1")),
M internal/usecase/daemon/wire.go +7 -7
@@ -16,11 +16,11 @@ func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
16 Ephemeral: s.ephemeral,16 Ephemeral: s.ephemeral,
17 }17 }
18 }18 }
19- w := ports.Welcome{19+ w := protocol.Welcome{
20 SessionID: string(s.id),20 SessionID: string(s.id),
21 SessionName: s.name,21 SessionName: s.name,
22 Ephemeral: s.ephemeral,22 Ephemeral: s.ephemeral,
23- Capabilities: ports.CapabilityResume,23+ Capabilities: protocol.CapabilityResume,
24 CommittedIdentity: identity,24 CommittedIdentity: identity,
25 }25 }
26 s.mu.Unlock()26 s.mu.Unlock()
@@ -33,7 +33,7 @@ func frameWelcome(s *session, resumeToken uint64) (ports.Frame, error) {
33 }33 }
34 34
35 func frameError(code uint16, text string) ports.Frame {35 func frameError(code uint16, text string) ports.Frame {
36- return ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(ports.ErrorMsg{Code: code, Text: text})}36+ return ports.Frame{Type: ports.MsgError, Payload: ports.MarshalErrorMsg(protocol.ErrorMsg{Code: code, Text: text})}
37 }37 }
38 38
39 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) {
@@ -48,13 +48,13 @@ func frameOutputState(b []byte, baseState uint64, state uint64, echoAck uint64)
48 }48 }
49 49
50 func frameDetached(reason uint8) ports.Frame {50 func frameDetached(reason uint8) ports.Frame {
51- return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(ports.Detached{Reason: reason})}51+ return ports.Frame{Type: ports.MsgDetached, Payload: ports.MarshalDetached(protocol.Detached{Reason: reason})}
52 }52 }
53 53
54 func framePong() ports.Frame {54 func framePong() ports.Frame {
55- return ports.Frame{Type: ports.MsgPong, Payload: ports.MarshalPong(ports.Pong{})}55+ return ports.Frame{Type: ports.MsgPong, Payload: ports.MarshalPong(protocol.Pong{})}
56 }56 }
57 57
58-func frameSessions(infos []ports.SessionInfo) ports.Frame {58+func frameSessions(infos []protocol.SessionInfo) ports.Frame {
59- return ports.Frame{Type: ports.MsgSessions, Payload: ports.MarshalSessions(ports.Sessions{Sessions: infos})}59+ return ports.Frame{Type: ports.MsgSessions, Payload: ports.MarshalSessions(protocol.Sessions{Sessions: infos})}
60 }60 }
M scripts/remote-picker-harness/main.go +25 -25
@@ -86,8 +86,8 @@ func runLiveStdioPhase(ctx context.Context, target string, catalog ports.RemoteC
86 return fmt.Errorf("open stdio transport: %w", err)86 return fmt.Errorf("open stdio transport: %w", err)
87 }87 }
88 defer func() { _ = stdio.Close() }()88 defer func() { _ = stdio.Close() }()
89- if err := attachAndCheck(ctx, stdio, ports.Hello{89+ if err := attachAndCheck(ctx, stdio, protocol.Hello{
90- Intent: ports.IntentNew, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",90+ Intent: protocol.IntentNew, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
91 TermEnv: "xterm-256color", TrueColor: true,91 TermEnv: "xterm-256color", TrueColor: true,
92 }); err != nil {92 }); err != nil {
93 return fmt.Errorf("stdio live attach: %w", err)93 return fmt.Errorf("stdio live attach: %w", err)
@@ -177,8 +177,8 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports
177 err = errors.Join(err, fmt.Errorf("destroy remote picker session: %w", cleanupErr))177 err = errors.Join(err, fmt.Errorf("destroy remote picker session: %w", cleanupErr))
178 }178 }
179 }()179 }()
180- if err := attachAndCheck(ctx, remote, ports.Hello{180+ if err := attachAndCheck(ctx, remote, protocol.Hello{
181- Intent: ports.IntentNew, Name: remoteSession, Env: localEnvironment(), Cwd: "/tmp/remote-cwd",181+ Intent: protocol.IntentNew, Name: remoteSession, Env: localEnvironment(), Cwd: "/tmp/remote-cwd",
182 TermEnv: "xterm-256color", TrueColor: true,182 TermEnv: "xterm-256color", TrueColor: true,
183 }); err != nil {183 }); err != nil {
184 return fmt.Errorf("attach remote picker source: %w", err)184 return fmt.Errorf("attach remote picker source: %w", err)
@@ -247,8 +247,8 @@ func runLocalPickerUnitedPhase(ctx context.Context, target string, catalog ports
247 // or MsgAttachTarget to this transport.247 // or MsgAttachTarget to this transport.
248 local.probe.configure("local-picker")248 local.probe.configure("local-picker")
249 defer func() { _ = local.Close() }()249 defer func() { _ = local.Close() }()
250- if err := attachAndCheck(ctx, local, ports.Hello{250+ if err := attachAndCheck(ctx, local, protocol.Hello{
251- Intent: ports.IntentNew, Name: localSession, Env: localEnvironment(), Cwd: "/tmp/local-cwd",251+ Intent: protocol.IntentNew, Name: localSession, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
252 TermEnv: "xterm-256color", TrueColor: true,252 TermEnv: "xterm-256color", TrueColor: true,
253 }); err != nil {253 }); err != nil {
254 return fmt.Errorf("attach local picker session: %w", err)254 return fmt.Errorf("attach local picker session: %w", err)
@@ -351,10 +351,10 @@ func runRestartResumePhase(ctx context.Context, target string, catalog ports.Rem
351 return domain.RemoteSessionTarget{}, fmt.Errorf("open restart stdio transport: %w", err)351 return domain.RemoteSessionTarget{}, fmt.Errorf("open restart stdio transport: %w", err)
352 }352 }
353 defer func() { _ = resumed.Close() }()353 defer func() { _ = resumed.Close() }()
354- if err := attachAndCheck(ctx, resumed, ports.Hello{354+ if err := attachAndCheck(ctx, resumed, protocol.Hello{
355- Intent: ports.IntentResume, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",355+ Intent: protocol.IntentResume, Name: sessionName, Env: localEnvironment(), Cwd: "/tmp/local-cwd",
356 TermEnv: "xterm-256color", TrueColor: true, RemoteTarget: &resumedTarget,356 TermEnv: "xterm-256color", TrueColor: true, RemoteTarget: &resumedTarget,
357- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,357+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
358 }); err != nil {358 }); err != nil {
359 return domain.RemoteSessionTarget{}, fmt.Errorf("exact restart resume: %w", err)359 return domain.RemoteSessionTarget{}, fmt.Errorf("exact restart resume: %w", err)
360 }360 }
@@ -385,8 +385,8 @@ func runUDPPhase(ctx context.Context, target string) error {
385 return fmt.Errorf("open UDP transport: %w", err)385 return fmt.Errorf("open UDP transport: %w", err)
386 }386 }
387 defer func() { _ = udp.Close() }()387 defer func() { _ = udp.Close() }()
388- if err := attachAndCheck(ctx, udp, ports.Hello{388+ if err := attachAndCheck(ctx, udp, protocol.Hello{
389- Intent: ports.IntentNew, Name: "udp-harness", Env: localEnvironment(), Cwd: "/tmp/local-cwd",389+ Intent: protocol.IntentNew, Name: "udp-harness", Env: localEnvironment(), Cwd: "/tmp/local-cwd",
390 TermEnv: "xterm-256color", TrueColor: true,390 TermEnv: "xterm-256color", TrueColor: true,
391 }); err != nil {391 }); err != nil {
392 return fmt.Errorf("UDP attach: %w", err)392 return fmt.Errorf("UDP attach: %w", err)
@@ -636,11 +636,11 @@ func openLocalTransport(ctx context.Context) (*harnessTransport, error) {
636 return h, nil636 return h, nil
637 }637 }
638 638
639-func attachAndCheck(ctx context.Context, tr *harnessTransport, hello ports.Hello) error {639+func attachAndCheck(ctx context.Context, tr *harnessTransport, hello protocol.Hello) error {
640- hello.Version = ports.ProtocolVersion640+ hello.Version = protocol.Version
641 hello.Size = domain.Size{Cols: 80, Rows: 24}641 hello.Size = domain.Size{Cols: 80, Rows: 24}
642 if hello.EnvironmentPolicy == 0 {642 if hello.EnvironmentPolicy == 0 {
643- hello.EnvironmentPolicy = ports.EnvironmentPolicyClientOwned643+ hello.EnvironmentPolicy = protocol.EnvironmentPolicyClientOwned
644 }644 }
645 payload := ports.MarshalHello(hello)645 payload := ports.MarshalHello(hello)
646 if err := tr.Send(ports.Frame{Type: ports.MsgHello, Payload: payload}); err != nil {646 if err := tr.Send(ports.Frame{Type: ports.MsgHello, Payload: payload}); err != nil {
@@ -668,7 +668,7 @@ func attachAndCheck(ctx context.Context, tr *harnessTransport, hello ports.Hello
668 }668 }
669 669
670 func sendRawInput(tr *harnessTransport, data string) error {670 func sendRawInput(tr *harnessTransport, data string) error {
671- return tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(data)})})671+ return tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(data)})})
672 }672 }
673 673
674 func processOutputFrame(tr *harnessTransport, frame ports.Frame) error {674 func processOutputFrame(tr *harnessTransport, frame ports.Frame) error {
@@ -679,7 +679,7 @@ func processOutputFrame(tr *harnessTransport, frame ports.Frame) error {
679 result := tr.probe.apply(output)679 result := tr.probe.apply(output)
680 if !result.Accepted {680 if !result.Accepted {
681 if !tr.probe.resetPending {681 if !tr.probe.resetPending {
682- if err := tr.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(ports.OutputResetRequest{})}); err != nil {682+ if err := tr.Send(ports.Frame{Type: ports.MsgOutputResetRequest, Payload: ports.MarshalOutputResetRequest(protocol.OutputResetRequest{})}); err != nil {
683 return err683 return err
684 }684 }
685 tr.probe.resetPending = true685 tr.probe.resetPending = true
@@ -768,7 +768,7 @@ func remotePreviewContains(preview protocol.RemotePreview, want string) bool {
768 }768 }
769 769
770 func sendInputAndAwait(ctx context.Context, tr *harnessTransport, command, want string) error {770 func sendInputAndAwait(ctx context.Context, tr *harnessTransport, command, want string) error {
771- if err := tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(command + "\n")})}); err != nil {771+ if err := tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(protocol.Input{Data: []byte(command + "\n")})}); err != nil {
772 return err772 return err
773 }773 }
774 deadline, cancel := context.WithTimeout(ctx, 5*time.Second)774 deadline, cancel := context.WithTimeout(ctx, 5*time.Second)
@@ -795,8 +795,8 @@ func sendInputAndAwait(ctx context.Context, tr *harnessTransport, command, want
795 795
796 func sendCommand(ctx context.Context, tr *harnessTransport, slug string) error {796 func sendCommand(ctx context.Context, tr *harnessTransport, slug string) error {
797 id := requestID.Add(1)797 id := requestID.Add(1)
798- request := ports.CommandRequest{798+ request := protocol.CommandRequest{
799- Version: ports.ProtocolVersion,799+ Version: protocol.Version,
800 RequestID: id,800 RequestID: id,
801 Attached: true,801 Attached: true,
802 Slug: slug,802 Slug: slug,
@@ -836,7 +836,7 @@ func sendCommand(ctx context.Context, tr *harnessTransport, slug string) error {
836 }836 }
837 837
838 func detach(ctx context.Context, tr *harnessTransport) error {838 func detach(ctx context.Context, tr *harnessTransport) error {
839- if err := tr.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}); err != nil {839+ if err := tr.Send(ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(protocol.Detach{})}); err != nil {
840 return err840 return err
841 }841 }
842 deadline, cancel := context.WithTimeout(ctx, 5*time.Second)842 deadline, cancel := context.WithTimeout(ctx, 5*time.Second)
@@ -859,9 +859,9 @@ func expectNoSuchTarget(ctx context.Context, tr *harnessTransport, target domain
859 if err := target.Validate(); err != nil {859 if err := target.Validate(); err != nil {
860 return err860 return err
861 }861 }
862- hello := ports.Hello{862+ hello := protocol.Hello{
863- Version: ports.ProtocolVersion,863+ Version: protocol.Version,
864- Intent: ports.IntentResume,864+ Intent: protocol.IntentResume,
865 Name: sessionName,865 Name: sessionName,
866 Size: domain.Size{Cols: 80, Rows: 24},866 Size: domain.Size{Cols: 80, Rows: 24},
867 TermEnv: "xterm-256color",867 TermEnv: "xterm-256color",
@@ -869,7 +869,7 @@ func expectNoSuchTarget(ctx context.Context, tr *harnessTransport, target domain
869 Env: localEnvironment(),869 Env: localEnvironment(),
870 Cwd: "/tmp/local-cwd",870 Cwd: "/tmp/local-cwd",
871 RemoteTarget: &target,871 RemoteTarget: &target,
872- EnvironmentPolicy: ports.EnvironmentPolicyDaemonOwned,872+ EnvironmentPolicy: protocol.EnvironmentPolicyDaemonOwned,
873 }873 }
874 payload := ports.MarshalHello(hello)874 payload := ports.MarshalHello(hello)
875 if err := tr.Send(ports.Frame{Type: ports.MsgHello, Payload: payload}); err != nil {875 if err := tr.Send(ports.Frame{Type: ports.MsgHello, Payload: payload}); err != nil {
@@ -886,7 +886,7 @@ func expectNoSuchTarget(ctx context.Context, tr *harnessTransport, target domain
886 if err != nil {886 if err != nil {
887 return err887 return err
888 }888 }
889- if message.Code != ports.ErrNoSuchTarget {889+ if message.Code != protocol.ErrNoSuchTarget {
890 return fmt.Errorf("stale target returned error code %d", message.Code)890 return fmt.Errorf("stale target returned error code %d", message.Code)
891 }891 }
892 return nil892 return nil

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