detached 31ab707

refactor(client): use typed session connections

commit
31ab707a5fb7e03572f33eacfc7d0d194be7d01e
author
brice · · 2026-08-28 07:08 UTC
committer
brice · · 2026-08-28 14:26 UTC
parent
c130909
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
24 files · +1703 -356
A internal/adapters/sessionwire/client.go +266 -0
@@ -0,0 +1,266 @@
1+package sessionwire
2+
3+import (
4+ "context"
5+ "errors"
6+
7+ "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
9+ "github.com/bnema/vev/internal/protocol/wire"
10+)
11+
12+type clientConnection struct{ raw ports.Transport }
13+
14+var _ ports.ClientConnection = (*clientConnection)(nil)
15+
16+// NewClientConnection wraps one raw client connection incarnation.
17+func NewClientConnection(raw ports.Transport) ports.ClientConnection {
18+ if raw == nil {
19+ return nil
20+ }
21+ return &clientConnection{raw: raw}
22+}
23+
24+func (c *clientConnection) SendClient(message protocol.ClientMessage) error {
25+ frame, err := encodeClient(message)
26+ if err != nil {
27+ return err
28+ }
29+ return c.raw.Send(frame)
30+}
31+
32+func (c *clientConnection) ReceiveServer() (protocol.ServerMessage, error) {
33+ frame, err := c.raw.Recv()
34+ if err != nil {
35+ return nil, err
36+ }
37+ message, err := decodeServer(frame)
38+ if err == nil {
39+ return message, nil
40+ }
41+ failure := &protocol.DecodeFailure{Category: protocol.DecodeMalformed, Type: uint8(frame.Type), Err: err}
42+ if errors.Is(err, ErrUnknownMessageType) {
43+ failure.Category = protocol.DecodeUnknownType
44+ } else if errors.Is(err, ErrWrongDirection) {
45+ failure.Category = protocol.DecodeWrongDirection
46+ }
47+ return nil, failure
48+}
49+
50+func (c *clientConnection) Capabilities() protocol.ConnectionCapabilities {
51+ return rawCapabilities(c.raw)
52+}
53+
54+func (c *clientConnection) LinkState() ports.LinkState { return rawLinkState(c.raw) }
55+func (c *clientConnection) LinkEvents() <-chan ports.LinkEvent { return rawLinkEvents(c.raw) }
56+func (c *clientConnection) Close() error { return c.raw.Close() }
57+
58+type clientDialer struct{ raw ports.Dialer }
59+
60+var _ ports.ClientDialer = (*clientDialer)(nil)
61+
62+// NewClientDialer wraps every dialed raw connection in a stable typed adapter.
63+func NewClientDialer(raw ports.Dialer) ports.ClientDialer {
64+ if raw == nil {
65+ return nil
66+ }
67+ return &clientDialer{raw: raw}
68+}
69+
70+func (d *clientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
71+ raw, err := d.raw.Dial(ctx)
72+ if err != nil {
73+ return nil, err
74+ }
75+ return NewClientConnection(raw), nil
76+}
77+
78+func decodeServer(frame wire.Frame) (protocol.ServerMessage, error) {
79+ switch frame.Type {
80+ case wire.MsgWelcome:
81+ return wire.UnmarshalWelcome(frame.Payload)
82+ case wire.MsgError:
83+ return wire.UnmarshalErrorMsg(frame.Payload)
84+ case wire.MsgOutput:
85+ return wire.UnmarshalOutput(frame.Payload)
86+ case wire.MsgDetached:
87+ return wire.UnmarshalDetached(frame.Payload)
88+ case wire.MsgPong:
89+ return wire.UnmarshalPong(frame.Payload)
90+ case wire.MsgSessions:
91+ return wire.UnmarshalSessions(frame.Payload)
92+ case wire.MsgCommandResult:
93+ return wire.UnmarshalCommandResult(frame.Payload)
94+ case wire.MsgNavigationAction:
95+ return wire.UnmarshalNavigationDirective(frame.Payload)
96+ case wire.MsgAttachTarget:
97+ return wire.UnmarshalAttachTarget(frame.Payload)
98+ case wire.MsgRemotePreviewResponse:
99+ return wire.UnmarshalRemotePreview(frame.Payload)
100+ case wire.MsgCommittedRouteIdentity:
101+ return wire.UnmarshalCommittedRouteIdentity(frame.Payload)
102+ case wire.MsgNavigateRecentRoute:
103+ return wire.UnmarshalRouteNavigationAction(frame.Payload)
104+ case wire.MsgRouteNavigationFailure:
105+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
106+ case wire.MsgRoutePosition:
107+ return wire.UnmarshalRoutePosition(frame.Payload)
108+ case wire.MsgSamePeerSwitchFailure:
109+ return wire.UnmarshalSamePeerSwitchFailure(frame.Payload)
110+ case wire.MsgParkedRouteResponse:
111+ return wire.UnmarshalParkedRouteResponse(frame.Payload)
112+ case wire.MsgHello, wire.MsgInput, wire.MsgResize, wire.MsgDetach, wire.MsgPing,
113+ wire.MsgList, wire.MsgKill, wire.MsgTheme, wire.MsgAck, wire.MsgImagePush,
114+ wire.MsgClientNotice, wire.MsgCommand, wire.MsgOutputResetRequest,
115+ wire.MsgRemotePreviewRequest, wire.MsgRouteAttentionSubscription,
116+ wire.MsgSamePeerSwitchRequest, wire.MsgParkedRouteRequest,
117+ wire.MsgRecentRouteSnapshot:
118+ return nil, ErrWrongDirection
119+ default:
120+ return nil, ErrUnknownMessageType
121+ }
122+}
123+
124+func encodeClient(message protocol.ClientMessage) (wire.Frame, error) {
125+ switch m := message.(type) {
126+ case protocol.Hello:
127+ payload := wire.MarshalHello(m)
128+ if payload == nil {
129+ return wire.Frame{}, ErrInvalidMessage
130+ }
131+ return wire.Frame{Type: wire.MsgHello, Payload: payload}, nil
132+ case protocol.Input:
133+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(m)}, nil
134+ case protocol.Resize:
135+ payload, err := wire.MarshalResize(m)
136+ return wire.Frame{Type: wire.MsgResize, Payload: payload}, err
137+ case protocol.Detach:
138+ return wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(m)}, nil
139+ case protocol.Ping:
140+ return wire.Frame{Type: wire.MsgPing, Payload: wire.MarshalPing(m)}, nil
141+ case protocol.List:
142+ return wire.Frame{Type: wire.MsgList, Payload: wire.MarshalList(m)}, nil
143+ case protocol.Kill:
144+ return wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(m)}, nil
145+ case protocol.Theme:
146+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(m)}, nil
147+ case protocol.Ack:
148+ payload, err := wire.MarshalAck(m)
149+ return wire.Frame{Type: wire.MsgAck, Payload: payload}, err
150+ case protocol.ImagePush:
151+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(m)}, nil
152+ case protocol.ClientNotice:
153+ if err := protocol.ValidateClientNotice(m); err != nil {
154+ return wire.Frame{}, err
155+ }
156+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(m)}, nil
157+ case protocol.CommandRequest:
158+ payload, err := wire.MarshalCommandRequest(m)
159+ return wire.Frame{Type: wire.MsgCommand, Payload: payload}, err
160+ case protocol.OutputResetRequest:
161+ return wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(m)}, nil
162+ case protocol.RemotePreviewRequest:
163+ payload := wire.MarshalRemotePreviewRequest(m)
164+ if payload == nil {
165+ return wire.Frame{}, ErrInvalidMessage
166+ }
167+ return wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: payload}, nil
168+ case protocol.RouteAttentionSubscription:
169+ payload, err := wire.MarshalRouteAttentionSubscription(m)
170+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: payload}, err
171+ case protocol.SamePeerSwitchRequest:
172+ payload, err := wire.MarshalSamePeerSwitchRequest(m)
173+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: payload}, err
174+ case protocol.ParkedRouteRequest:
175+ payload := wire.MarshalParkedRouteRequest(m)
176+ if payload == nil {
177+ return wire.Frame{}, ErrInvalidMessage
178+ }
179+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: payload}, nil
180+ case protocol.RecentRouteSnapshot:
181+ payload, err := wire.MarshalRecentRouteSnapshot(m)
182+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: payload}, err
183+ case protocol.RouteNavigationFailure:
184+ payload, err := wire.MarshalRouteNavigationFailure(m)
185+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}, err
186+ case *protocol.Hello:
187+ if m != nil {
188+ return encodeClient(*m)
189+ }
190+ case *protocol.Input:
191+ if m != nil {
192+ return encodeClient(*m)
193+ }
194+ case *protocol.Resize:
195+ if m != nil {
196+ return encodeClient(*m)
197+ }
198+ case *protocol.Detach:
199+ if m != nil {
200+ return encodeClient(*m)
201+ }
202+ case *protocol.Ping:
203+ if m != nil {
204+ return encodeClient(*m)
205+ }
206+ case *protocol.List:
207+ if m != nil {
208+ return encodeClient(*m)
209+ }
210+ case *protocol.Kill:
211+ if m != nil {
212+ return encodeClient(*m)
213+ }
214+ case *protocol.Theme:
215+ if m != nil {
216+ return encodeClient(*m)
217+ }
218+ case *protocol.Ack:
219+ if m != nil {
220+ return encodeClient(*m)
221+ }
222+ case *protocol.ImagePush:
223+ if m != nil {
224+ return encodeClient(*m)
225+ }
226+ case *protocol.ClientNotice:
227+ if m != nil {
228+ return encodeClient(*m)
229+ }
230+ case *protocol.CommandRequest:
231+ if m != nil {
232+ return encodeClient(*m)
233+ }
234+ case *protocol.OutputResetRequest:
235+ if m != nil {
236+ return encodeClient(*m)
237+ }
238+ case *protocol.RemotePreviewRequest:
239+ if m != nil {
240+ return encodeClient(*m)
241+ }
242+ case *protocol.RouteAttentionSubscription:
243+ if m != nil {
244+ return encodeClient(*m)
245+ }
246+ case *protocol.SamePeerSwitchRequest:
247+ if m != nil {
248+ return encodeClient(*m)
249+ }
250+ case *protocol.ParkedRouteRequest:
251+ if m != nil {
252+ return encodeClient(*m)
253+ }
254+ case *protocol.RecentRouteSnapshot:
255+ if m != nil {
256+ return encodeClient(*m)
257+ }
258+ case *protocol.RouteNavigationFailure:
259+ if m != nil {
260+ return encodeClient(*m)
261+ }
262+ default:
263+ return wire.Frame{}, ErrWrongDirection
264+ }
265+ return wire.Frame{}, ErrInvalidMessage
266+}
@@ -0,0 +1,266 @@
1+package sessionwire
2+
3+import (
4+ "context"
5+ "errors"
6+
7+ "github.com/bnema/vev/internal/ports"
8+ "github.com/bnema/vev/internal/protocol"
9+ "github.com/bnema/vev/internal/protocol/wire"
10+)
11+
12+type clientConnection struct{ raw ports.Transport }
13+
14+var _ ports.ClientConnection = (*clientConnection)(nil)
15+
16+// NewClientConnection wraps one raw client connection incarnation.
17+func NewClientConnection(raw ports.Transport) ports.ClientConnection {
18+ if raw == nil {
19+ return nil
20+ }
21+ return &clientConnection{raw: raw}
22+}
23+
24+func (c *clientConnection) SendClient(message protocol.ClientMessage) error {
25+ frame, err := encodeClient(message)
26+ if err != nil {
27+ return err
28+ }
29+ return c.raw.Send(frame)
30+}
31+
32+func (c *clientConnection) ReceiveServer() (protocol.ServerMessage, error) {
33+ frame, err := c.raw.Recv()
34+ if err != nil {
35+ return nil, err
36+ }
37+ message, err := decodeServer(frame)
38+ if err == nil {
39+ return message, nil
40+ }
41+ failure := &protocol.DecodeFailure{Category: protocol.DecodeMalformed, Type: uint8(frame.Type), Err: err}
42+ if errors.Is(err, ErrUnknownMessageType) {
43+ failure.Category = protocol.DecodeUnknownType
44+ } else if errors.Is(err, ErrWrongDirection) {
45+ failure.Category = protocol.DecodeWrongDirection
46+ }
47+ return nil, failure
48+}
49+
50+func (c *clientConnection) Capabilities() protocol.ConnectionCapabilities {
51+ return rawCapabilities(c.raw)
52+}
53+
54+func (c *clientConnection) LinkState() ports.LinkState { return rawLinkState(c.raw) }
55+func (c *clientConnection) LinkEvents() <-chan ports.LinkEvent { return rawLinkEvents(c.raw) }
56+func (c *clientConnection) Close() error { return c.raw.Close() }
57+
58+type clientDialer struct{ raw ports.Dialer }
59+
60+var _ ports.ClientDialer = (*clientDialer)(nil)
61+
62+// NewClientDialer wraps every dialed raw connection in a stable typed adapter.
63+func NewClientDialer(raw ports.Dialer) ports.ClientDialer {
64+ if raw == nil {
65+ return nil
66+ }
67+ return &clientDialer{raw: raw}
68+}
69+
70+func (d *clientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
71+ raw, err := d.raw.Dial(ctx)
72+ if err != nil {
73+ return nil, err
74+ }
75+ return NewClientConnection(raw), nil
76+}
77+
78+func decodeServer(frame wire.Frame) (protocol.ServerMessage, error) {
79+ switch frame.Type {
80+ case wire.MsgWelcome:
81+ return wire.UnmarshalWelcome(frame.Payload)
82+ case wire.MsgError:
83+ return wire.UnmarshalErrorMsg(frame.Payload)
84+ case wire.MsgOutput:
85+ return wire.UnmarshalOutput(frame.Payload)
86+ case wire.MsgDetached:
87+ return wire.UnmarshalDetached(frame.Payload)
88+ case wire.MsgPong:
89+ return wire.UnmarshalPong(frame.Payload)
90+ case wire.MsgSessions:
91+ return wire.UnmarshalSessions(frame.Payload)
92+ case wire.MsgCommandResult:
93+ return wire.UnmarshalCommandResult(frame.Payload)
94+ case wire.MsgNavigationAction:
95+ return wire.UnmarshalNavigationDirective(frame.Payload)
96+ case wire.MsgAttachTarget:
97+ return wire.UnmarshalAttachTarget(frame.Payload)
98+ case wire.MsgRemotePreviewResponse:
99+ return wire.UnmarshalRemotePreview(frame.Payload)
100+ case wire.MsgCommittedRouteIdentity:
101+ return wire.UnmarshalCommittedRouteIdentity(frame.Payload)
102+ case wire.MsgNavigateRecentRoute:
103+ return wire.UnmarshalRouteNavigationAction(frame.Payload)
104+ case wire.MsgRouteNavigationFailure:
105+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
106+ case wire.MsgRoutePosition:
107+ return wire.UnmarshalRoutePosition(frame.Payload)
108+ case wire.MsgSamePeerSwitchFailure:
109+ return wire.UnmarshalSamePeerSwitchFailure(frame.Payload)
110+ case wire.MsgParkedRouteResponse:
111+ return wire.UnmarshalParkedRouteResponse(frame.Payload)
112+ case wire.MsgHello, wire.MsgInput, wire.MsgResize, wire.MsgDetach, wire.MsgPing,
113+ wire.MsgList, wire.MsgKill, wire.MsgTheme, wire.MsgAck, wire.MsgImagePush,
114+ wire.MsgClientNotice, wire.MsgCommand, wire.MsgOutputResetRequest,
115+ wire.MsgRemotePreviewRequest, wire.MsgRouteAttentionSubscription,
116+ wire.MsgSamePeerSwitchRequest, wire.MsgParkedRouteRequest,
117+ wire.MsgRecentRouteSnapshot:
118+ return nil, ErrWrongDirection
119+ default:
120+ return nil, ErrUnknownMessageType
121+ }
122+}
123+
124+func encodeClient(message protocol.ClientMessage) (wire.Frame, error) {
125+ switch m := message.(type) {
126+ case protocol.Hello:
127+ payload := wire.MarshalHello(m)
128+ if payload == nil {
129+ return wire.Frame{}, ErrInvalidMessage
130+ }
131+ return wire.Frame{Type: wire.MsgHello, Payload: payload}, nil
132+ case protocol.Input:
133+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(m)}, nil
134+ case protocol.Resize:
135+ payload, err := wire.MarshalResize(m)
136+ return wire.Frame{Type: wire.MsgResize, Payload: payload}, err
137+ case protocol.Detach:
138+ return wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(m)}, nil
139+ case protocol.Ping:
140+ return wire.Frame{Type: wire.MsgPing, Payload: wire.MarshalPing(m)}, nil
141+ case protocol.List:
142+ return wire.Frame{Type: wire.MsgList, Payload: wire.MarshalList(m)}, nil
143+ case protocol.Kill:
144+ return wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(m)}, nil
145+ case protocol.Theme:
146+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(m)}, nil
147+ case protocol.Ack:
148+ payload, err := wire.MarshalAck(m)
149+ return wire.Frame{Type: wire.MsgAck, Payload: payload}, err
150+ case protocol.ImagePush:
151+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(m)}, nil
152+ case protocol.ClientNotice:
153+ if err := protocol.ValidateClientNotice(m); err != nil {
154+ return wire.Frame{}, err
155+ }
156+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(m)}, nil
157+ case protocol.CommandRequest:
158+ payload, err := wire.MarshalCommandRequest(m)
159+ return wire.Frame{Type: wire.MsgCommand, Payload: payload}, err
160+ case protocol.OutputResetRequest:
161+ return wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(m)}, nil
162+ case protocol.RemotePreviewRequest:
163+ payload := wire.MarshalRemotePreviewRequest(m)
164+ if payload == nil {
165+ return wire.Frame{}, ErrInvalidMessage
166+ }
167+ return wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: payload}, nil
168+ case protocol.RouteAttentionSubscription:
169+ payload, err := wire.MarshalRouteAttentionSubscription(m)
170+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: payload}, err
171+ case protocol.SamePeerSwitchRequest:
172+ payload, err := wire.MarshalSamePeerSwitchRequest(m)
173+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: payload}, err
174+ case protocol.ParkedRouteRequest:
175+ payload := wire.MarshalParkedRouteRequest(m)
176+ if payload == nil {
177+ return wire.Frame{}, ErrInvalidMessage
178+ }
179+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: payload}, nil
180+ case protocol.RecentRouteSnapshot:
181+ payload, err := wire.MarshalRecentRouteSnapshot(m)
182+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: payload}, err
183+ case protocol.RouteNavigationFailure:
184+ payload, err := wire.MarshalRouteNavigationFailure(m)
185+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}, err
186+ case *protocol.Hello:
187+ if m != nil {
188+ return encodeClient(*m)
189+ }
190+ case *protocol.Input:
191+ if m != nil {
192+ return encodeClient(*m)
193+ }
194+ case *protocol.Resize:
195+ if m != nil {
196+ return encodeClient(*m)
197+ }
198+ case *protocol.Detach:
199+ if m != nil {
200+ return encodeClient(*m)
201+ }
202+ case *protocol.Ping:
203+ if m != nil {
204+ return encodeClient(*m)
205+ }
206+ case *protocol.List:
207+ if m != nil {
208+ return encodeClient(*m)
209+ }
210+ case *protocol.Kill:
211+ if m != nil {
212+ return encodeClient(*m)
213+ }
214+ case *protocol.Theme:
215+ if m != nil {
216+ return encodeClient(*m)
217+ }
218+ case *protocol.Ack:
219+ if m != nil {
220+ return encodeClient(*m)
221+ }
222+ case *protocol.ImagePush:
223+ if m != nil {
224+ return encodeClient(*m)
225+ }
226+ case *protocol.ClientNotice:
227+ if m != nil {
228+ return encodeClient(*m)
229+ }
230+ case *protocol.CommandRequest:
231+ if m != nil {
232+ return encodeClient(*m)
233+ }
234+ case *protocol.OutputResetRequest:
235+ if m != nil {
236+ return encodeClient(*m)
237+ }
238+ case *protocol.RemotePreviewRequest:
239+ if m != nil {
240+ return encodeClient(*m)
241+ }
242+ case *protocol.RouteAttentionSubscription:
243+ if m != nil {
244+ return encodeClient(*m)
245+ }
246+ case *protocol.SamePeerSwitchRequest:
247+ if m != nil {
248+ return encodeClient(*m)
249+ }
250+ case *protocol.ParkedRouteRequest:
251+ if m != nil {
252+ return encodeClient(*m)
253+ }
254+ case *protocol.RecentRouteSnapshot:
255+ if m != nil {
256+ return encodeClient(*m)
257+ }
258+ case *protocol.RouteNavigationFailure:
259+ if m != nil {
260+ return encodeClient(*m)
261+ }
262+ default:
263+ return wire.Frame{}, ErrWrongDirection
264+ }
265+ return wire.Frame{}, ErrInvalidMessage
266+}
A internal/adapters/sessionwire/client_test.go +125 -0
@@ -0,0 +1,125 @@
1+package sessionwire
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
11+ "github.com/bnema/vev/internal/protocol/wire"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+func TestClientConnectionEncodesEveryClientMessage(t *testing.T) {
16+ target := testTarget()
17+ exact := protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}
18+ lease := protocol.ParkedRouteLeaseID{1}
19+ tests := []struct {
20+ name string
21+ message protocol.ClientMessage
22+ typeID wire.MsgType
23+ }{
24+ {name: "hello", message: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}, typeID: wire.MsgHello},
25+ {name: "input", message: protocol.Input{InputSeq: 1, Data: []byte("x")}, typeID: wire.MsgInput},
26+ {name: "resize", message: protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}, typeID: wire.MsgResize},
27+ {name: "detach", message: protocol.Detach{}, typeID: wire.MsgDetach},
28+ {name: "ping", message: protocol.Ping{}, typeID: wire.MsgPing},
29+ {name: "list", message: protocol.List{}, typeID: wire.MsgList},
30+ {name: "kill", message: protocol.Kill{Name: "work"}, typeID: wire.MsgKill},
31+ {name: "theme", message: protocol.Theme{TrueColor: true}, typeID: wire.MsgTheme},
32+ {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}, typeID: wire.MsgAck},
33+ {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}, typeID: wire.MsgImagePush},
34+ {name: "notice", message: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}, typeID: wire.MsgClientNotice},
35+ {name: "command", message: protocol.CommandRequest{Version: protocol.Version, RequestID: 1, Slug: "list-sessions"}, typeID: wire.MsgCommand},
36+ {name: "reset", message: protocol.OutputResetRequest{}, typeID: wire.MsgOutputResetRequest},
37+ {name: "preview", message: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}, typeID: wire.MsgRemotePreviewRequest},
38+ {name: "attention", message: protocol.RouteAttentionSubscription{}, typeID: wire.MsgRouteAttentionSubscription},
39+ {name: "same peer", message: protocol.SamePeerSwitchRequest{RequestID: 1, Target: exact}, typeID: wire.MsgSamePeerSwitchRequest},
40+ {name: "parked", message: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRoutePrepare}, typeID: wire.MsgParkedRouteRequest},
41+ {name: "snapshot", message: protocol.RecentRouteSnapshot{}, typeID: wire.MsgRecentRouteSnapshot},
42+ {name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}, typeID: wire.MsgRouteNavigationFailure},
43+ }
44+ for _, tt := range tests {
45+ t.Run(tt.name, func(t *testing.T) {
46+ raw := &scriptedTransport{}
47+ require.NoError(t, NewClientConnection(raw).SendClient(tt.message))
48+ require.Equal(t, tt.typeID, raw.sent.Type)
49+ })
50+ }
51+}
52+
53+func TestClientConnectionDecodesEveryServerMessage(t *testing.T) {
54+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
55+ lease := protocol.ParkedRouteLeaseID{1}
56+ tests := []struct {
57+ name string
58+ message protocol.ServerMessage
59+ }{
60+ {name: "welcome", message: protocol.Welcome{SessionID: "session"}},
61+ {name: "error", message: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"}},
62+ {name: "output", message: protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}},
63+ {name: "detached", message: protocol.Detached{Reason: protocol.ReasonDetach}},
64+ {name: "pong", message: protocol.Pong{}},
65+ {name: "sessions", message: protocol.Sessions{Sessions: []protocol.SessionInfo{}}},
66+ {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}},
67+ {name: "navigation", message: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}},
68+ {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},
69+ {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},
70+ {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},
71+ {name: "route action", message: protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 1, Generation: 1}},
72+ {name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
73+ {name: "route position", message: protocol.RoutePosition{Target: exact, ActiveTabID: "tab-1"}},
74+ {name: "switch failure", message: protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchUnavailable}},
75+ {name: "parked response", message: protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}},
76+ }
77+ for _, tt := range tests {
78+ t.Run(tt.name, func(t *testing.T) {
79+ frame, err := encodeServer(tt.message)
80+ require.NoError(t, err)
81+ got, err := NewClientConnection(&scriptedTransport{recv: frame}).ReceiveServer()
82+ require.NoError(t, err)
83+ require.Equal(t, tt.message, got)
84+ })
85+ }
86+}
87+
88+func TestClientConnectionClassifiesFailuresAndPreservesCapabilities(t *testing.T) {
89+ connection := NewClientConnection(&scriptedTransport{recv: wire.Frame{Type: wire.MsgInput}})
90+ _, err := connection.ReceiveServer()
91+ var failure *protocol.DecodeFailure
92+ require.ErrorAs(t, err, &failure)
93+ require.Equal(t, protocol.DecodeWrongDirection, failure.Category)
94+
95+ events := make(chan ports.LinkEvent, 1)
96+ raw := &capableTransport{events: events}
97+ connection = NewClientConnection(raw)
98+ require.Equal(t, uint8(1), connection.Capabilities().PreferredOutputWindow)
99+ require.True(t, connection.Capabilities().LinkState)
100+ require.Equal(t, ports.LinkStateDegraded, connection.LinkState())
101+ require.Equal(t, (<-chan ports.LinkEvent)(events), connection.LinkEvents())
102+}
103+
104+func TestClientDialerWrapsEachConnectionAndPreservesErrors(t *testing.T) {
105+ dialErr := errors.New("dial failed")
106+ dialer := NewClientDialer(&scriptedDialer{err: dialErr})
107+ _, err := dialer.Dial(context.Background())
108+ require.ErrorIs(t, err, dialErr)
109+
110+ raw := &scriptedTransport{}
111+ dialer = NewClientDialer(&scriptedDialer{transport: raw})
112+ connection, err := dialer.Dial(context.Background())
113+ require.NoError(t, err)
114+ require.NoError(t, connection.SendClient(protocol.Ping{}))
115+ require.Equal(t, wire.MsgPing, raw.sent.Type)
116+}
117+
118+type scriptedDialer struct {
119+ transport ports.Transport
120+ err error
121+}
122+
123+func (d *scriptedDialer) Dial(context.Context) (ports.Transport, error) {
124+ return d.transport, d.err
125+}
@@ -0,0 +1,125 @@
1+package sessionwire
2+
3+import (
4+ "context"
5+ "errors"
6+ "testing"
7+
8+ "github.com/bnema/vev/internal/domain"
9+ "github.com/bnema/vev/internal/ports"
10+ "github.com/bnema/vev/internal/protocol"
11+ "github.com/bnema/vev/internal/protocol/wire"
12+ "github.com/stretchr/testify/require"
13+)
14+
15+func TestClientConnectionEncodesEveryClientMessage(t *testing.T) {
16+ target := testTarget()
17+ exact := protocol.ExactSessionTarget{LifecycleID: target.LifecycleID, SessionName: target.SessionName}
18+ lease := protocol.ParkedRouteLeaseID{1}
19+ tests := []struct {
20+ name string
21+ message protocol.ClientMessage
22+ typeID wire.MsgType
23+ }{
24+ {name: "hello", message: protocol.Hello{Version: protocol.Version, Intent: protocol.IntentAttach, Size: domain.Size{Cols: 80, Rows: 24}}, typeID: wire.MsgHello},
25+ {name: "input", message: protocol.Input{InputSeq: 1, Data: []byte("x")}, typeID: wire.MsgInput},
26+ {name: "resize", message: protocol.Resize{Size: domain.Size{Cols: 80, Rows: 24}}, typeID: wire.MsgResize},
27+ {name: "detach", message: protocol.Detach{}, typeID: wire.MsgDetach},
28+ {name: "ping", message: protocol.Ping{}, typeID: wire.MsgPing},
29+ {name: "list", message: protocol.List{}, typeID: wire.MsgList},
30+ {name: "kill", message: protocol.Kill{Name: "work"}, typeID: wire.MsgKill},
31+ {name: "theme", message: protocol.Theme{TrueColor: true}, typeID: wire.MsgTheme},
32+ {name: "ack", message: protocol.Ack{Epoch: 1, State: 1}, typeID: wire.MsgAck},
33+ {name: "image", message: protocol.ImagePush{InputSeq: 1, Mime: "image/png", Data: []byte{1}}, typeID: wire.MsgImagePush},
34+ {name: "notice", message: protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}, typeID: wire.MsgClientNotice},
35+ {name: "command", message: protocol.CommandRequest{Version: protocol.Version, RequestID: 1, Slug: "list-sessions"}, typeID: wire.MsgCommand},
36+ {name: "reset", message: protocol.OutputResetRequest{}, typeID: wire.MsgOutputResetRequest},
37+ {name: "preview", message: protocol.RemotePreviewRequest{Version: protocol.RemotePreviewSchemaVersion, Target: target, Width: 1, Height: 1}, typeID: wire.MsgRemotePreviewRequest},
38+ {name: "attention", message: protocol.RouteAttentionSubscription{}, typeID: wire.MsgRouteAttentionSubscription},
39+ {name: "same peer", message: protocol.SamePeerSwitchRequest{RequestID: 1, Target: exact}, typeID: wire.MsgSamePeerSwitchRequest},
40+ {name: "parked", message: protocol.ParkedRouteRequest{RequestID: 1, LeaseID: lease, Action: protocol.ParkedRoutePrepare}, typeID: wire.MsgParkedRouteRequest},
41+ {name: "snapshot", message: protocol.RecentRouteSnapshot{}, typeID: wire.MsgRecentRouteSnapshot},
42+ {name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}, typeID: wire.MsgRouteNavigationFailure},
43+ }
44+ for _, tt := range tests {
45+ t.Run(tt.name, func(t *testing.T) {
46+ raw := &scriptedTransport{}
47+ require.NoError(t, NewClientConnection(raw).SendClient(tt.message))
48+ require.Equal(t, tt.typeID, raw.sent.Type)
49+ })
50+ }
51+}
52+
53+func TestClientConnectionDecodesEveryServerMessage(t *testing.T) {
54+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
55+ lease := protocol.ParkedRouteLeaseID{1}
56+ tests := []struct {
57+ name string
58+ message protocol.ServerMessage
59+ }{
60+ {name: "welcome", message: protocol.Welcome{SessionID: "session"}},
61+ {name: "error", message: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"}},
62+ {name: "output", message: protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}},
63+ {name: "detached", message: protocol.Detached{Reason: protocol.ReasonDetach}},
64+ {name: "pong", message: protocol.Pong{}},
65+ {name: "sessions", message: protocol.Sessions{Sessions: []protocol.SessionInfo{}}},
66+ {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}},
67+ {name: "navigation", message: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}},
68+ {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},
69+ {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},
70+ {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},
71+ {name: "route action", message: protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 1, Generation: 1}},
72+ {name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
73+ {name: "route position", message: protocol.RoutePosition{Target: exact, ActiveTabID: "tab-1"}},
74+ {name: "switch failure", message: protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchUnavailable}},
75+ {name: "parked response", message: protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}},
76+ }
77+ for _, tt := range tests {
78+ t.Run(tt.name, func(t *testing.T) {
79+ frame, err := encodeServer(tt.message)
80+ require.NoError(t, err)
81+ got, err := NewClientConnection(&scriptedTransport{recv: frame}).ReceiveServer()
82+ require.NoError(t, err)
83+ require.Equal(t, tt.message, got)
84+ })
85+ }
86+}
87+
88+func TestClientConnectionClassifiesFailuresAndPreservesCapabilities(t *testing.T) {
89+ connection := NewClientConnection(&scriptedTransport{recv: wire.Frame{Type: wire.MsgInput}})
90+ _, err := connection.ReceiveServer()
91+ var failure *protocol.DecodeFailure
92+ require.ErrorAs(t, err, &failure)
93+ require.Equal(t, protocol.DecodeWrongDirection, failure.Category)
94+
95+ events := make(chan ports.LinkEvent, 1)
96+ raw := &capableTransport{events: events}
97+ connection = NewClientConnection(raw)
98+ require.Equal(t, uint8(1), connection.Capabilities().PreferredOutputWindow)
99+ require.True(t, connection.Capabilities().LinkState)
100+ require.Equal(t, ports.LinkStateDegraded, connection.LinkState())
101+ require.Equal(t, (<-chan ports.LinkEvent)(events), connection.LinkEvents())
102+}
103+
104+func TestClientDialerWrapsEachConnectionAndPreservesErrors(t *testing.T) {
105+ dialErr := errors.New("dial failed")
106+ dialer := NewClientDialer(&scriptedDialer{err: dialErr})
107+ _, err := dialer.Dial(context.Background())
108+ require.ErrorIs(t, err, dialErr)
109+
110+ raw := &scriptedTransport{}
111+ dialer = NewClientDialer(&scriptedDialer{transport: raw})
112+ connection, err := dialer.Dial(context.Background())
113+ require.NoError(t, err)
114+ require.NoError(t, connection.SendClient(protocol.Ping{}))
115+ require.Equal(t, wire.MsgPing, raw.sent.Type)
116+}
117+
118+type scriptedDialer struct {
119+ transport ports.Transport
120+ err error
121+}
122+
123+func (d *scriptedDialer) Dial(context.Context) (ports.Transport, error) {
124+ return d.transport, d.err
125+}
M internal/adapters/sessionwire/server.go +15 -8
@@ -132,10 +132,17 @@ func encodeOutput(output protocol.Output) (wire.Frame, error) {
132132 }
133133
134134 func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities {
135- _, datagram := c.raw.(ports.DatagramTransport)
136- _, async := c.raw.(ports.AsyncTransport)
137- _, synchronous := c.raw.(ports.OwnedSynchronousTransport)
138- _, linkState := c.raw.(ports.LinkStateReporter)
135+ return rawCapabilities(c.raw)
136+}
137+
138+func (c *serverConnection) LinkState() ports.LinkState { return rawLinkState(c.raw) }
139+func (c *serverConnection) LinkEvents() <-chan ports.LinkEvent { return rawLinkEvents(c.raw) }
140+
141+func rawCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
142+ _, datagram := raw.(ports.DatagramTransport)
143+ _, async := raw.(ports.AsyncTransport)
144+ _, synchronous := raw.(ports.OwnedSynchronousTransport)
145+ _, linkState := raw.(ports.LinkStateReporter)
139146 window := uint8(protocol.MaxOutputWindow)
140147 if datagram {
141148 window = 1
@@ -149,15 +156,15 @@ func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities {
149156 }
150157 }
151158
152-func (c *serverConnection) LinkState() ports.LinkState {
153- if reporter, ok := c.raw.(ports.LinkStateReporter); ok {
159+func rawLinkState(raw ports.Transport) ports.LinkState {
160+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
154161 return reporter.LinkState()
155162 }
156163 return ports.LinkStateConnected
157164 }
158165
159-func (c *serverConnection) LinkEvents() <-chan ports.LinkEvent {
160- if reporter, ok := c.raw.(ports.LinkStateReporter); ok {
166+func rawLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
167+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
161168 return reporter.LinkEvents()
162169 }
163170 return nil
@@ -132,10 +132,17 @@ func encodeOutput(output protocol.Output) (wire.Frame, error) {
132 }132 }
133 133
134 func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities {134 func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities {
135- _, datagram := c.raw.(ports.DatagramTransport)135+ return rawCapabilities(c.raw)
136- _, async := c.raw.(ports.AsyncTransport)136+}
137- _, synchronous := c.raw.(ports.OwnedSynchronousTransport)137+
138- _, linkState := c.raw.(ports.LinkStateReporter)138+func (c *serverConnection) LinkState() ports.LinkState { return rawLinkState(c.raw) }
139+func (c *serverConnection) LinkEvents() <-chan ports.LinkEvent { return rawLinkEvents(c.raw) }
140+
141+func rawCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
142+ _, datagram := raw.(ports.DatagramTransport)
143+ _, async := raw.(ports.AsyncTransport)
144+ _, synchronous := raw.(ports.OwnedSynchronousTransport)
145+ _, linkState := raw.(ports.LinkStateReporter)
139 window := uint8(protocol.MaxOutputWindow)146 window := uint8(protocol.MaxOutputWindow)
140 if datagram {147 if datagram {
141 window = 1148 window = 1
@@ -149,15 +156,15 @@ func (c *serverConnection) Capabilities() protocol.ConnectionCapabilities {
149 }156 }
150 }157 }
151 158
152-func (c *serverConnection) LinkState() ports.LinkState {159+func rawLinkState(raw ports.Transport) ports.LinkState {
153- if reporter, ok := c.raw.(ports.LinkStateReporter); ok {160+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
154 return reporter.LinkState()161 return reporter.LinkState()
155 }162 }
156 return ports.LinkStateConnected163 return ports.LinkStateConnected
157 }164 }
158 165
159-func (c *serverConnection) LinkEvents() <-chan ports.LinkEvent {166+func rawLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
160- if reporter, ok := c.raw.(ports.LinkStateReporter); ok {167+ if reporter, ok := raw.(ports.LinkStateReporter); ok {
161 return reporter.LinkEvents()168 return reporter.LinkEvents()
162 }169 }
163 return nil170 return nil
M internal/app/run.go +4 -4
@@ -914,7 +914,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
914914 }
915915 return policy
916916 }
917- handoff := func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
917+ handoff := func(target protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
918918 if err := validateRemoteAttachHandoff(target); err != nil {
919919 return nil, client.AttachRequest{}, fmt.Errorf("vev: invalid remote attach handoff: %w", err)
920920 }
@@ -938,7 +938,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
938938 if selection != nil {
939939 displayOrigin = selection.DisplayOrigin
940940 }
941- return dialer, client.AttachRequest{
941+ return sessionwire.NewClientDialer(dialer), client.AttachRequest{
942942 Intent: target.Intent,
943943 SessionName: target.Session,
944944 Remote: true,
@@ -962,7 +962,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
962962 return dialErr
963963 }
964964 err = runClient(ctx, client.Dependencies{
965- Dialer: dialer,
965+ Dialer: sessionwire.NewClientDialer(dialer),
966966 Terminal: term.New(),
967967 Clock: clock.New(),
968968 Clipboard: deps.clipboard,
@@ -988,7 +988,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
988988 log.Info("attaching to local session", "intent", intent, "name", name)
989989 }
990990 err = runClient(ctx, client.Dependencies{
991- Dialer: localDialer(),
991+ Dialer: sessionwire.NewClientDialer(localDialer()),
992992 Terminal: term.New(),
993993 Clock: clock.New(),
994994 Logger: log,
@@ -914,7 +914,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
914 }914 }
915 return policy915 return policy
916 }916 }
917- handoff := func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {917+ handoff := func(target protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
918 if err := validateRemoteAttachHandoff(target); err != nil {918 if err := validateRemoteAttachHandoff(target); err != nil {
919 return nil, client.AttachRequest{}, fmt.Errorf("vev: invalid remote attach handoff: %w", err)919 return nil, client.AttachRequest{}, fmt.Errorf("vev: invalid remote attach handoff: %w", err)
920 }920 }
@@ -938,7 +938,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
938 if selection != nil {938 if selection != nil {
939 displayOrigin = selection.DisplayOrigin939 displayOrigin = selection.DisplayOrigin
940 }940 }
941- return dialer, client.AttachRequest{941+ return sessionwire.NewClientDialer(dialer), client.AttachRequest{
942 Intent: target.Intent,942 Intent: target.Intent,
943 SessionName: target.Session,943 SessionName: target.Session,
944 Remote: true,944 Remote: true,
@@ -962,7 +962,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
962 return dialErr962 return dialErr
963 }963 }
964 err = runClient(ctx, client.Dependencies{964 err = runClient(ctx, client.Dependencies{
965- Dialer: dialer,965+ Dialer: sessionwire.NewClientDialer(dialer),
966 Terminal: term.New(),966 Terminal: term.New(),
967 Clock: clock.New(),967 Clock: clock.New(),
968 Clipboard: deps.clipboard,968 Clipboard: deps.clipboard,
@@ -988,7 +988,7 @@ func runAttachWithDeps(ctx context.Context, intent uint8, name, remoteTarget, ac
988 log.Info("attaching to local session", "intent", intent, "name", name)988 log.Info("attaching to local session", "intent", intent, "name", name)
989 }989 }
990 err = runClient(ctx, client.Dependencies{990 err = runClient(ctx, client.Dependencies{
991- Dialer: localDialer(),991+ Dialer: sessionwire.NewClientDialer(localDialer()),
992 Terminal: term.New(),992 Terminal: term.New(),
993 Clock: clock.New(),993 Clock: clock.New(),
994 Logger: log,994 Logger: log,
M internal/app/run_test.go +14 -12
@@ -6,6 +6,7 @@ import (
66 "context"
77 "encoding/json"
88 "errors"
9+ "fmt"
910 "io"
1011 "log/slog"
1112 "net"
@@ -18,6 +19,7 @@ import (
1819
1920 "github.com/bnema/vev/internal/adapters/lifecycle"
2021 remoteadapter "github.com/bnema/vev/internal/adapters/remote"
22+ "github.com/bnema/vev/internal/adapters/sessionwire"
2123 "github.com/bnema/vev/internal/domain"
2224 "github.com/bnema/vev/internal/persist"
2325 "github.com/bnema/vev/internal/ports"
@@ -575,7 +577,7 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
575577 t.Run(tt.name, func(t *testing.T) {
576578 cmd, err := parseArgs([]string{"attach", tt.arg})
577579 require.NoError(t, err)
578- connection, err := client.NewSessionConnection(portsmocks.NewMockTransport(t), client.SessionTarget{
580+ connection, err := client.NewSessionConnection(sessionwire.NewClientConnection(portsmocks.NewMockTransport(t)), client.SessionTarget{
579581 Intent: cmd.intent,
580582 SessionName: cmd.name,
581583 })
@@ -807,7 +809,13 @@ func TestDetachedLocalHelloIncludesTrueColor(t *testing.T) {
807809 type namedDialer struct{ name string }
808810
809811 func (d namedDialer) Dial(context.Context) (ports.Transport, error) {
810- return nil, errors.New("not used")
812+ return nil, fmt.Errorf("not used: %s", d.name)
813+}
814+
815+func requireNamedClientDialer(t *testing.T, dialer ports.ClientDialer, name string) {
816+ t.Helper()
817+ _, err := dialer.Dial(context.Background())
818+ require.EqualError(t, err, "not used: "+name)
811819 }
812820
813821 // fakeClipboardReader is a distinguishable ports.ClipboardReader used only to
@@ -839,11 +847,8 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
839847 selectedRemoteTransport: tt.selectedTransport,
840848 clipboard: clip,
841849 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
842- nd, ok := deps.Dialer.(namedDialer)
843- if !ok {
844- t.Fatalf("dialer type = %T, want namedDialer", deps.Dialer)
845- }
846- gotDialer = nd.name
850+ requireNamedClientDialer(t, deps.Dialer, "remote")
851+ gotDialer = "remote"
847852 gotRemote = deps.Remote
848853 if !request.Remote || request.EnvironmentPolicy != protocol.EnvironmentPolicyClientOwned {
849854 t.Fatal("direct CLI remote attach must preserve client-owned environment policy")
@@ -1065,11 +1070,8 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
10651070 remoteDialerFactory: factory,
10661071 clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach
10671072 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
1068- nd, ok := deps.Dialer.(namedDialer)
1069- if !ok {
1070- t.Fatalf("dialer type = %T, want namedDialer", deps.Dialer)
1071- }
1072- gotDialer = nd.name
1073+ requireNamedClientDialer(t, deps.Dialer, "local")
1074+ gotDialer = "local"
10731075 gotRemote = deps.Remote
10741076 if request.Remote {
10751077 t.Fatal("local carriage metadata must not be in the attach request")
@@ -6,6 +6,7 @@ import (
6 "context"6 "context"
7 "encoding/json"7 "encoding/json"
8 "errors"8 "errors"
9+ "fmt"
9 "io"10 "io"
10 "log/slog"11 "log/slog"
11 "net"12 "net"
@@ -18,6 +19,7 @@ import (
18 19
19 "github.com/bnema/vev/internal/adapters/lifecycle"20 "github.com/bnema/vev/internal/adapters/lifecycle"
20 remoteadapter "github.com/bnema/vev/internal/adapters/remote"21 remoteadapter "github.com/bnema/vev/internal/adapters/remote"
22+ "github.com/bnema/vev/internal/adapters/sessionwire"
21 "github.com/bnema/vev/internal/domain"23 "github.com/bnema/vev/internal/domain"
22 "github.com/bnema/vev/internal/persist"24 "github.com/bnema/vev/internal/persist"
23 "github.com/bnema/vev/internal/ports"25 "github.com/bnema/vev/internal/ports"
@@ -575,7 +577,7 @@ func TestAttachTargetsCreateCommonSessionRequest(t *testing.T) {
575 t.Run(tt.name, func(t *testing.T) {577 t.Run(tt.name, func(t *testing.T) {
576 cmd, err := parseArgs([]string{"attach", tt.arg})578 cmd, err := parseArgs([]string{"attach", tt.arg})
577 require.NoError(t, err)579 require.NoError(t, err)
578- connection, err := client.NewSessionConnection(portsmocks.NewMockTransport(t), client.SessionTarget{580+ connection, err := client.NewSessionConnection(sessionwire.NewClientConnection(portsmocks.NewMockTransport(t)), client.SessionTarget{
579 Intent: cmd.intent,581 Intent: cmd.intent,
580 SessionName: cmd.name,582 SessionName: cmd.name,
581 })583 })
@@ -807,7 +809,13 @@ func TestDetachedLocalHelloIncludesTrueColor(t *testing.T) {
807 type namedDialer struct{ name string }809 type namedDialer struct{ name string }
808 810
809 func (d namedDialer) Dial(context.Context) (ports.Transport, error) {811 func (d namedDialer) Dial(context.Context) (ports.Transport, error) {
810- return nil, errors.New("not used")812+ return nil, fmt.Errorf("not used: %s", d.name)
813+}
814+
815+func requireNamedClientDialer(t *testing.T, dialer ports.ClientDialer, name string) {
816+ t.Helper()
817+ _, err := dialer.Dial(context.Background())
818+ require.EqualError(t, err, "not used: "+name)
811 }819 }
812 820
813 // fakeClipboardReader is a distinguishable ports.ClipboardReader used only to821 // fakeClipboardReader is a distinguishable ports.ClipboardReader used only to
@@ -839,11 +847,8 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
839 selectedRemoteTransport: tt.selectedTransport,847 selectedRemoteTransport: tt.selectedTransport,
840 clipboard: clip,848 clipboard: clip,
841 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {849 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
842- nd, ok := deps.Dialer.(namedDialer)850+ requireNamedClientDialer(t, deps.Dialer, "remote")
843- if !ok {851+ gotDialer = "remote"
844- t.Fatalf("dialer type = %T, want namedDialer", deps.Dialer)
845- }
846- gotDialer = nd.name
847 gotRemote = deps.Remote852 gotRemote = deps.Remote
848 if !request.Remote || request.EnvironmentPolicy != protocol.EnvironmentPolicyClientOwned {853 if !request.Remote || request.EnvironmentPolicy != protocol.EnvironmentPolicyClientOwned {
849 t.Fatal("direct CLI remote attach must preserve client-owned environment policy")854 t.Fatal("direct CLI remote attach must preserve client-owned environment policy")
@@ -1065,11 +1070,8 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
1065 remoteDialerFactory: factory,1070 remoteDialerFactory: factory,
1066 clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach1071 clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach
1067 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {1072 runClient: func(_ context.Context, deps client.Dependencies, request client.AttachRequest) error {
1068- nd, ok := deps.Dialer.(namedDialer)1073+ requireNamedClientDialer(t, deps.Dialer, "local")
1069- if !ok {1074+ gotDialer = "local"
1070- t.Fatalf("dialer type = %T, want namedDialer", deps.Dialer)
1071- }
1072- gotDialer = nd.name
1073 gotRemote = deps.Remote1075 gotRemote = deps.Remote
1074 if request.Remote {1076 if request.Remote {
1075 t.Fatal("local carriage metadata must not be in the attach request")1077 t.Fatal("local carriage metadata must not be in the attach request")
M internal/app/transport_observability_test.go +1 -3
@@ -240,9 +240,7 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) {
240240 if deps.RuntimeObserver != observer {
241241 t.Fatalf("client transport observer = %v, want process observer %v", deps.RuntimeObserver, observer)
242242 }
243- if deps.Dialer != (namedDialer{name: "remote"}) {
244- t.Fatalf("remote transport dialer = %v, want configured factory dialer", deps.Dialer)
245- }
243+ requireNamedClientDialer(t, deps.Dialer, "remote")
246244 return nil
247245 }
248246 t.Cleanup(func() { runClientWithDeps = originalRunClient })
@@ -240,9 +240,7 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) {
240 if deps.RuntimeObserver != observer {240 if deps.RuntimeObserver != observer {
241 t.Fatalf("client transport observer = %v, want process observer %v", deps.RuntimeObserver, observer)241 t.Fatalf("client transport observer = %v, want process observer %v", deps.RuntimeObserver, observer)
242 }242 }
243- if deps.Dialer != (namedDialer{name: "remote"}) {243+ requireNamedClientDialer(t, deps.Dialer, "remote")
244- t.Fatalf("remote transport dialer = %v, want configured factory dialer", deps.Dialer)
245- }
246 return nil244 return nil
247 }245 }
248 t.Cleanup(func() { runClientWithDeps = originalRunClient })246 t.Cleanup(func() { runClientWithDeps = originalRunClient })
M internal/ports/mocks/session_mock.go +402 -0
@@ -5,11 +5,413 @@
55 package portsmocks
66
77 import (
8+ "context"
9+
810 "github.com/bnema/vev/internal/ports"
911 "github.com/bnema/vev/internal/protocol"
1012 mock "github.com/stretchr/testify/mock"
1113 )
1214
15+// NewMockClientConnection creates a new instance of MockClientConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewMockClientConnection(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *MockClientConnection {
21+ mock := &MockClientConnection{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() { mock.AssertExpectations(t) })
25+
26+ return mock
27+}
28+
29+// MockClientConnection is an autogenerated mock type for the ClientConnection type
30+type MockClientConnection struct {
31+ mock.Mock
32+}
33+
34+type MockClientConnection_Expecter struct {
35+ mock *mock.Mock
36+}
37+
38+func (_m *MockClientConnection) EXPECT() *MockClientConnection_Expecter {
39+ return &MockClientConnection_Expecter{mock: &_m.Mock}
40+}
41+
42+// Capabilities provides a mock function for the type MockClientConnection
43+func (_mock *MockClientConnection) Capabilities() protocol.ConnectionCapabilities {
44+ ret := _mock.Called()
45+
46+ if len(ret) == 0 {
47+ panic("no return value specified for Capabilities")
48+ }
49+
50+ var r0 protocol.ConnectionCapabilities
51+ if returnFunc, ok := ret.Get(0).(func() protocol.ConnectionCapabilities); ok {
52+ r0 = returnFunc()
53+ } else {
54+ r0 = ret.Get(0).(protocol.ConnectionCapabilities)
55+ }
56+ return r0
57+}
58+
59+// MockClientConnection_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities'
60+type MockClientConnection_Capabilities_Call struct {
61+ *mock.Call
62+}
63+
64+// Capabilities is a helper method to define mock.On call
65+func (_e *MockClientConnection_Expecter) Capabilities() *MockClientConnection_Capabilities_Call {
66+ return &MockClientConnection_Capabilities_Call{Call: _e.mock.On("Capabilities")}
67+}
68+
69+func (_c *MockClientConnection_Capabilities_Call) Run(run func()) *MockClientConnection_Capabilities_Call {
70+ _c.Call.Run(func(args mock.Arguments) {
71+ run()
72+ })
73+ return _c
74+}
75+
76+func (_c *MockClientConnection_Capabilities_Call) Return(connectionCapabilities protocol.ConnectionCapabilities) *MockClientConnection_Capabilities_Call {
77+ _c.Call.Return(connectionCapabilities)
78+ return _c
79+}
80+
81+func (_c *MockClientConnection_Capabilities_Call) RunAndReturn(run func() protocol.ConnectionCapabilities) *MockClientConnection_Capabilities_Call {
82+ _c.Call.Return(run)
83+ return _c
84+}
85+
86+// Close provides a mock function for the type MockClientConnection
87+func (_mock *MockClientConnection) Close() error {
88+ ret := _mock.Called()
89+
90+ if len(ret) == 0 {
91+ panic("no return value specified for Close")
92+ }
93+
94+ var r0 error
95+ if returnFunc, ok := ret.Get(0).(func() error); ok {
96+ r0 = returnFunc()
97+ } else {
98+ r0 = ret.Error(0)
99+ }
100+ return r0
101+}
102+
103+// MockClientConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
104+type MockClientConnection_Close_Call struct {
105+ *mock.Call
106+}
107+
108+// Close is a helper method to define mock.On call
109+func (_e *MockClientConnection_Expecter) Close() *MockClientConnection_Close_Call {
110+ return &MockClientConnection_Close_Call{Call: _e.mock.On("Close")}
111+}
112+
113+func (_c *MockClientConnection_Close_Call) Run(run func()) *MockClientConnection_Close_Call {
114+ _c.Call.Run(func(args mock.Arguments) {
115+ run()
116+ })
117+ return _c
118+}
119+
120+func (_c *MockClientConnection_Close_Call) Return(err error) *MockClientConnection_Close_Call {
121+ _c.Call.Return(err)
122+ return _c
123+}
124+
125+func (_c *MockClientConnection_Close_Call) RunAndReturn(run func() error) *MockClientConnection_Close_Call {
126+ _c.Call.Return(run)
127+ return _c
128+}
129+
130+// LinkEvents provides a mock function for the type MockClientConnection
131+func (_mock *MockClientConnection) LinkEvents() <-chan ports.LinkEvent {
132+ ret := _mock.Called()
133+
134+ if len(ret) == 0 {
135+ panic("no return value specified for LinkEvents")
136+ }
137+
138+ var r0 <-chan ports.LinkEvent
139+ if returnFunc, ok := ret.Get(0).(func() <-chan ports.LinkEvent); ok {
140+ r0 = returnFunc()
141+ } else {
142+ if ret.Get(0) != nil {
143+ r0 = ret.Get(0).(<-chan ports.LinkEvent)
144+ }
145+ }
146+ return r0
147+}
148+
149+// MockClientConnection_LinkEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkEvents'
150+type MockClientConnection_LinkEvents_Call struct {
151+ *mock.Call
152+}
153+
154+// LinkEvents is a helper method to define mock.On call
155+func (_e *MockClientConnection_Expecter) LinkEvents() *MockClientConnection_LinkEvents_Call {
156+ return &MockClientConnection_LinkEvents_Call{Call: _e.mock.On("LinkEvents")}
157+}
158+
159+func (_c *MockClientConnection_LinkEvents_Call) Run(run func()) *MockClientConnection_LinkEvents_Call {
160+ _c.Call.Run(func(args mock.Arguments) {
161+ run()
162+ })
163+ return _c
164+}
165+
166+func (_c *MockClientConnection_LinkEvents_Call) Return(linkEventCh <-chan ports.LinkEvent) *MockClientConnection_LinkEvents_Call {
167+ _c.Call.Return(linkEventCh)
168+ return _c
169+}
170+
171+func (_c *MockClientConnection_LinkEvents_Call) RunAndReturn(run func() <-chan ports.LinkEvent) *MockClientConnection_LinkEvents_Call {
172+ _c.Call.Return(run)
173+ return _c
174+}
175+
176+// LinkState provides a mock function for the type MockClientConnection
177+func (_mock *MockClientConnection) LinkState() ports.LinkState {
178+ ret := _mock.Called()
179+
180+ if len(ret) == 0 {
181+ panic("no return value specified for LinkState")
182+ }
183+
184+ var r0 ports.LinkState
185+ if returnFunc, ok := ret.Get(0).(func() ports.LinkState); ok {
186+ r0 = returnFunc()
187+ } else {
188+ r0 = ret.Get(0).(ports.LinkState)
189+ }
190+ return r0
191+}
192+
193+// MockClientConnection_LinkState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkState'
194+type MockClientConnection_LinkState_Call struct {
195+ *mock.Call
196+}
197+
198+// LinkState is a helper method to define mock.On call
199+func (_e *MockClientConnection_Expecter) LinkState() *MockClientConnection_LinkState_Call {
200+ return &MockClientConnection_LinkState_Call{Call: _e.mock.On("LinkState")}
201+}
202+
203+func (_c *MockClientConnection_LinkState_Call) Run(run func()) *MockClientConnection_LinkState_Call {
204+ _c.Call.Run(func(args mock.Arguments) {
205+ run()
206+ })
207+ return _c
208+}
209+
210+func (_c *MockClientConnection_LinkState_Call) Return(linkState ports.LinkState) *MockClientConnection_LinkState_Call {
211+ _c.Call.Return(linkState)
212+ return _c
213+}
214+
215+func (_c *MockClientConnection_LinkState_Call) RunAndReturn(run func() ports.LinkState) *MockClientConnection_LinkState_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
220+// ReceiveServer provides a mock function for the type MockClientConnection
221+func (_mock *MockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
222+ ret := _mock.Called()
223+
224+ if len(ret) == 0 {
225+ panic("no return value specified for ReceiveServer")
226+ }
227+
228+ var r0 protocol.ServerMessage
229+ var r1 error
230+ if returnFunc, ok := ret.Get(0).(func() (protocol.ServerMessage, error)); ok {
231+ return returnFunc()
232+ }
233+ if returnFunc, ok := ret.Get(0).(func() protocol.ServerMessage); ok {
234+ r0 = returnFunc()
235+ } else {
236+ if ret.Get(0) != nil {
237+ r0 = ret.Get(0).(protocol.ServerMessage)
238+ }
239+ }
240+ if returnFunc, ok := ret.Get(1).(func() error); ok {
241+ r1 = returnFunc()
242+ } else {
243+ r1 = ret.Error(1)
244+ }
245+ return r0, r1
246+}
247+
248+// MockClientConnection_ReceiveServer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveServer'
249+type MockClientConnection_ReceiveServer_Call struct {
250+ *mock.Call
251+}
252+
253+// ReceiveServer is a helper method to define mock.On call
254+func (_e *MockClientConnection_Expecter) ReceiveServer() *MockClientConnection_ReceiveServer_Call {
255+ return &MockClientConnection_ReceiveServer_Call{Call: _e.mock.On("ReceiveServer")}
256+}
257+
258+func (_c *MockClientConnection_ReceiveServer_Call) Run(run func()) *MockClientConnection_ReceiveServer_Call {
259+ _c.Call.Run(func(args mock.Arguments) {
260+ run()
261+ })
262+ return _c
263+}
264+
265+func (_c *MockClientConnection_ReceiveServer_Call) Return(serverMessage protocol.ServerMessage, err error) *MockClientConnection_ReceiveServer_Call {
266+ _c.Call.Return(serverMessage, err)
267+ return _c
268+}
269+
270+func (_c *MockClientConnection_ReceiveServer_Call) RunAndReturn(run func() (protocol.ServerMessage, error)) *MockClientConnection_ReceiveServer_Call {
271+ _c.Call.Return(run)
272+ return _c
273+}
274+
275+// SendClient provides a mock function for the type MockClientConnection
276+func (_mock *MockClientConnection) SendClient(clientMessage protocol.ClientMessage) error {
277+ ret := _mock.Called(clientMessage)
278+
279+ if len(ret) == 0 {
280+ panic("no return value specified for SendClient")
281+ }
282+
283+ var r0 error
284+ if returnFunc, ok := ret.Get(0).(func(protocol.ClientMessage) error); ok {
285+ r0 = returnFunc(clientMessage)
286+ } else {
287+ r0 = ret.Error(0)
288+ }
289+ return r0
290+}
291+
292+// MockClientConnection_SendClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendClient'
293+type MockClientConnection_SendClient_Call struct {
294+ *mock.Call
295+}
296+
297+// SendClient is a helper method to define mock.On call
298+// - clientMessage protocol.ClientMessage
299+func (_e *MockClientConnection_Expecter) SendClient(clientMessage any) *MockClientConnection_SendClient_Call {
300+ return &MockClientConnection_SendClient_Call{Call: _e.mock.On("SendClient", clientMessage)}
301+}
302+
303+func (_c *MockClientConnection_SendClient_Call) Run(run func(clientMessage protocol.ClientMessage)) *MockClientConnection_SendClient_Call {
304+ _c.Call.Run(func(args mock.Arguments) {
305+ var arg0 protocol.ClientMessage
306+ if args[0] != nil {
307+ arg0 = args[0].(protocol.ClientMessage)
308+ }
309+ run(
310+ arg0,
311+ )
312+ })
313+ return _c
314+}
315+
316+func (_c *MockClientConnection_SendClient_Call) Return(err error) *MockClientConnection_SendClient_Call {
317+ _c.Call.Return(err)
318+ return _c
319+}
320+
321+func (_c *MockClientConnection_SendClient_Call) RunAndReturn(run func(clientMessage protocol.ClientMessage) error) *MockClientConnection_SendClient_Call {
322+ _c.Call.Return(run)
323+ return _c
324+}
325+
326+// NewMockClientDialer creates a new instance of MockClientDialer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
327+// The first argument is typically a *testing.T value.
328+func NewMockClientDialer(t interface {
329+ mock.TestingT
330+ Cleanup(func())
331+}) *MockClientDialer {
332+ mock := &MockClientDialer{}
333+ mock.Mock.Test(t)
334+
335+ t.Cleanup(func() { mock.AssertExpectations(t) })
336+
337+ return mock
338+}
339+
340+// MockClientDialer is an autogenerated mock type for the ClientDialer type
341+type MockClientDialer struct {
342+ mock.Mock
343+}
344+
345+type MockClientDialer_Expecter struct {
346+ mock *mock.Mock
347+}
348+
349+func (_m *MockClientDialer) EXPECT() *MockClientDialer_Expecter {
350+ return &MockClientDialer_Expecter{mock: &_m.Mock}
351+}
352+
353+// Dial provides a mock function for the type MockClientDialer
354+func (_mock *MockClientDialer) Dial(context1 context.Context) (ports.ClientConnection, error) {
355+ ret := _mock.Called(context1)
356+
357+ if len(ret) == 0 {
358+ panic("no return value specified for Dial")
359+ }
360+
361+ var r0 ports.ClientConnection
362+ var r1 error
363+ if returnFunc, ok := ret.Get(0).(func(context.Context) (ports.ClientConnection, error)); ok {
364+ return returnFunc(context1)
365+ }
366+ if returnFunc, ok := ret.Get(0).(func(context.Context) ports.ClientConnection); ok {
367+ r0 = returnFunc(context1)
368+ } else {
369+ if ret.Get(0) != nil {
370+ r0 = ret.Get(0).(ports.ClientConnection)
371+ }
372+ }
373+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
374+ r1 = returnFunc(context1)
375+ } else {
376+ r1 = ret.Error(1)
377+ }
378+ return r0, r1
379+}
380+
381+// MockClientDialer_Dial_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dial'
382+type MockClientDialer_Dial_Call struct {
383+ *mock.Call
384+}
385+
386+// Dial is a helper method to define mock.On call
387+// - context1 context.Context
388+func (_e *MockClientDialer_Expecter) Dial(context1 any) *MockClientDialer_Dial_Call {
389+ return &MockClientDialer_Dial_Call{Call: _e.mock.On("Dial", context1)}
390+}
391+
392+func (_c *MockClientDialer_Dial_Call) Run(run func(context1 context.Context)) *MockClientDialer_Dial_Call {
393+ _c.Call.Run(func(args mock.Arguments) {
394+ var arg0 context.Context
395+ if args[0] != nil {
396+ arg0 = args[0].(context.Context)
397+ }
398+ run(
399+ arg0,
400+ )
401+ })
402+ return _c
403+}
404+
405+func (_c *MockClientDialer_Dial_Call) Return(clientConnection ports.ClientConnection, err error) *MockClientDialer_Dial_Call {
406+ _c.Call.Return(clientConnection, err)
407+ return _c
408+}
409+
410+func (_c *MockClientDialer_Dial_Call) RunAndReturn(run func(context1 context.Context) (ports.ClientConnection, error)) *MockClientDialer_Dial_Call {
411+ _c.Call.Return(run)
412+ return _c
413+}
414+
13415 // NewMockServerConnection creates a new instance of MockServerConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14416 // The first argument is typically a *testing.T value.
15417 func NewMockServerConnection(t interface {
@@ -5,11 +5,413 @@
5 package portsmocks5 package portsmocks
6 6
7 import (7 import (
8+ "context"
9+
8 "github.com/bnema/vev/internal/ports"10 "github.com/bnema/vev/internal/ports"
9 "github.com/bnema/vev/internal/protocol"11 "github.com/bnema/vev/internal/protocol"
10 mock "github.com/stretchr/testify/mock"12 mock "github.com/stretchr/testify/mock"
11 )13 )
12 14
15+// NewMockClientConnection creates a new instance of MockClientConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
16+// The first argument is typically a *testing.T value.
17+func NewMockClientConnection(t interface {
18+ mock.TestingT
19+ Cleanup(func())
20+}) *MockClientConnection {
21+ mock := &MockClientConnection{}
22+ mock.Mock.Test(t)
23+
24+ t.Cleanup(func() { mock.AssertExpectations(t) })
25+
26+ return mock
27+}
28+
29+// MockClientConnection is an autogenerated mock type for the ClientConnection type
30+type MockClientConnection struct {
31+ mock.Mock
32+}
33+
34+type MockClientConnection_Expecter struct {
35+ mock *mock.Mock
36+}
37+
38+func (_m *MockClientConnection) EXPECT() *MockClientConnection_Expecter {
39+ return &MockClientConnection_Expecter{mock: &_m.Mock}
40+}
41+
42+// Capabilities provides a mock function for the type MockClientConnection
43+func (_mock *MockClientConnection) Capabilities() protocol.ConnectionCapabilities {
44+ ret := _mock.Called()
45+
46+ if len(ret) == 0 {
47+ panic("no return value specified for Capabilities")
48+ }
49+
50+ var r0 protocol.ConnectionCapabilities
51+ if returnFunc, ok := ret.Get(0).(func() protocol.ConnectionCapabilities); ok {
52+ r0 = returnFunc()
53+ } else {
54+ r0 = ret.Get(0).(protocol.ConnectionCapabilities)
55+ }
56+ return r0
57+}
58+
59+// MockClientConnection_Capabilities_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Capabilities'
60+type MockClientConnection_Capabilities_Call struct {
61+ *mock.Call
62+}
63+
64+// Capabilities is a helper method to define mock.On call
65+func (_e *MockClientConnection_Expecter) Capabilities() *MockClientConnection_Capabilities_Call {
66+ return &MockClientConnection_Capabilities_Call{Call: _e.mock.On("Capabilities")}
67+}
68+
69+func (_c *MockClientConnection_Capabilities_Call) Run(run func()) *MockClientConnection_Capabilities_Call {
70+ _c.Call.Run(func(args mock.Arguments) {
71+ run()
72+ })
73+ return _c
74+}
75+
76+func (_c *MockClientConnection_Capabilities_Call) Return(connectionCapabilities protocol.ConnectionCapabilities) *MockClientConnection_Capabilities_Call {
77+ _c.Call.Return(connectionCapabilities)
78+ return _c
79+}
80+
81+func (_c *MockClientConnection_Capabilities_Call) RunAndReturn(run func() protocol.ConnectionCapabilities) *MockClientConnection_Capabilities_Call {
82+ _c.Call.Return(run)
83+ return _c
84+}
85+
86+// Close provides a mock function for the type MockClientConnection
87+func (_mock *MockClientConnection) Close() error {
88+ ret := _mock.Called()
89+
90+ if len(ret) == 0 {
91+ panic("no return value specified for Close")
92+ }
93+
94+ var r0 error
95+ if returnFunc, ok := ret.Get(0).(func() error); ok {
96+ r0 = returnFunc()
97+ } else {
98+ r0 = ret.Error(0)
99+ }
100+ return r0
101+}
102+
103+// MockClientConnection_Close_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Close'
104+type MockClientConnection_Close_Call struct {
105+ *mock.Call
106+}
107+
108+// Close is a helper method to define mock.On call
109+func (_e *MockClientConnection_Expecter) Close() *MockClientConnection_Close_Call {
110+ return &MockClientConnection_Close_Call{Call: _e.mock.On("Close")}
111+}
112+
113+func (_c *MockClientConnection_Close_Call) Run(run func()) *MockClientConnection_Close_Call {
114+ _c.Call.Run(func(args mock.Arguments) {
115+ run()
116+ })
117+ return _c
118+}
119+
120+func (_c *MockClientConnection_Close_Call) Return(err error) *MockClientConnection_Close_Call {
121+ _c.Call.Return(err)
122+ return _c
123+}
124+
125+func (_c *MockClientConnection_Close_Call) RunAndReturn(run func() error) *MockClientConnection_Close_Call {
126+ _c.Call.Return(run)
127+ return _c
128+}
129+
130+// LinkEvents provides a mock function for the type MockClientConnection
131+func (_mock *MockClientConnection) LinkEvents() <-chan ports.LinkEvent {
132+ ret := _mock.Called()
133+
134+ if len(ret) == 0 {
135+ panic("no return value specified for LinkEvents")
136+ }
137+
138+ var r0 <-chan ports.LinkEvent
139+ if returnFunc, ok := ret.Get(0).(func() <-chan ports.LinkEvent); ok {
140+ r0 = returnFunc()
141+ } else {
142+ if ret.Get(0) != nil {
143+ r0 = ret.Get(0).(<-chan ports.LinkEvent)
144+ }
145+ }
146+ return r0
147+}
148+
149+// MockClientConnection_LinkEvents_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkEvents'
150+type MockClientConnection_LinkEvents_Call struct {
151+ *mock.Call
152+}
153+
154+// LinkEvents is a helper method to define mock.On call
155+func (_e *MockClientConnection_Expecter) LinkEvents() *MockClientConnection_LinkEvents_Call {
156+ return &MockClientConnection_LinkEvents_Call{Call: _e.mock.On("LinkEvents")}
157+}
158+
159+func (_c *MockClientConnection_LinkEvents_Call) Run(run func()) *MockClientConnection_LinkEvents_Call {
160+ _c.Call.Run(func(args mock.Arguments) {
161+ run()
162+ })
163+ return _c
164+}
165+
166+func (_c *MockClientConnection_LinkEvents_Call) Return(linkEventCh <-chan ports.LinkEvent) *MockClientConnection_LinkEvents_Call {
167+ _c.Call.Return(linkEventCh)
168+ return _c
169+}
170+
171+func (_c *MockClientConnection_LinkEvents_Call) RunAndReturn(run func() <-chan ports.LinkEvent) *MockClientConnection_LinkEvents_Call {
172+ _c.Call.Return(run)
173+ return _c
174+}
175+
176+// LinkState provides a mock function for the type MockClientConnection
177+func (_mock *MockClientConnection) LinkState() ports.LinkState {
178+ ret := _mock.Called()
179+
180+ if len(ret) == 0 {
181+ panic("no return value specified for LinkState")
182+ }
183+
184+ var r0 ports.LinkState
185+ if returnFunc, ok := ret.Get(0).(func() ports.LinkState); ok {
186+ r0 = returnFunc()
187+ } else {
188+ r0 = ret.Get(0).(ports.LinkState)
189+ }
190+ return r0
191+}
192+
193+// MockClientConnection_LinkState_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'LinkState'
194+type MockClientConnection_LinkState_Call struct {
195+ *mock.Call
196+}
197+
198+// LinkState is a helper method to define mock.On call
199+func (_e *MockClientConnection_Expecter) LinkState() *MockClientConnection_LinkState_Call {
200+ return &MockClientConnection_LinkState_Call{Call: _e.mock.On("LinkState")}
201+}
202+
203+func (_c *MockClientConnection_LinkState_Call) Run(run func()) *MockClientConnection_LinkState_Call {
204+ _c.Call.Run(func(args mock.Arguments) {
205+ run()
206+ })
207+ return _c
208+}
209+
210+func (_c *MockClientConnection_LinkState_Call) Return(linkState ports.LinkState) *MockClientConnection_LinkState_Call {
211+ _c.Call.Return(linkState)
212+ return _c
213+}
214+
215+func (_c *MockClientConnection_LinkState_Call) RunAndReturn(run func() ports.LinkState) *MockClientConnection_LinkState_Call {
216+ _c.Call.Return(run)
217+ return _c
218+}
219+
220+// ReceiveServer provides a mock function for the type MockClientConnection
221+func (_mock *MockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
222+ ret := _mock.Called()
223+
224+ if len(ret) == 0 {
225+ panic("no return value specified for ReceiveServer")
226+ }
227+
228+ var r0 protocol.ServerMessage
229+ var r1 error
230+ if returnFunc, ok := ret.Get(0).(func() (protocol.ServerMessage, error)); ok {
231+ return returnFunc()
232+ }
233+ if returnFunc, ok := ret.Get(0).(func() protocol.ServerMessage); ok {
234+ r0 = returnFunc()
235+ } else {
236+ if ret.Get(0) != nil {
237+ r0 = ret.Get(0).(protocol.ServerMessage)
238+ }
239+ }
240+ if returnFunc, ok := ret.Get(1).(func() error); ok {
241+ r1 = returnFunc()
242+ } else {
243+ r1 = ret.Error(1)
244+ }
245+ return r0, r1
246+}
247+
248+// MockClientConnection_ReceiveServer_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ReceiveServer'
249+type MockClientConnection_ReceiveServer_Call struct {
250+ *mock.Call
251+}
252+
253+// ReceiveServer is a helper method to define mock.On call
254+func (_e *MockClientConnection_Expecter) ReceiveServer() *MockClientConnection_ReceiveServer_Call {
255+ return &MockClientConnection_ReceiveServer_Call{Call: _e.mock.On("ReceiveServer")}
256+}
257+
258+func (_c *MockClientConnection_ReceiveServer_Call) Run(run func()) *MockClientConnection_ReceiveServer_Call {
259+ _c.Call.Run(func(args mock.Arguments) {
260+ run()
261+ })
262+ return _c
263+}
264+
265+func (_c *MockClientConnection_ReceiveServer_Call) Return(serverMessage protocol.ServerMessage, err error) *MockClientConnection_ReceiveServer_Call {
266+ _c.Call.Return(serverMessage, err)
267+ return _c
268+}
269+
270+func (_c *MockClientConnection_ReceiveServer_Call) RunAndReturn(run func() (protocol.ServerMessage, error)) *MockClientConnection_ReceiveServer_Call {
271+ _c.Call.Return(run)
272+ return _c
273+}
274+
275+// SendClient provides a mock function for the type MockClientConnection
276+func (_mock *MockClientConnection) SendClient(clientMessage protocol.ClientMessage) error {
277+ ret := _mock.Called(clientMessage)
278+
279+ if len(ret) == 0 {
280+ panic("no return value specified for SendClient")
281+ }
282+
283+ var r0 error
284+ if returnFunc, ok := ret.Get(0).(func(protocol.ClientMessage) error); ok {
285+ r0 = returnFunc(clientMessage)
286+ } else {
287+ r0 = ret.Error(0)
288+ }
289+ return r0
290+}
291+
292+// MockClientConnection_SendClient_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'SendClient'
293+type MockClientConnection_SendClient_Call struct {
294+ *mock.Call
295+}
296+
297+// SendClient is a helper method to define mock.On call
298+// - clientMessage protocol.ClientMessage
299+func (_e *MockClientConnection_Expecter) SendClient(clientMessage any) *MockClientConnection_SendClient_Call {
300+ return &MockClientConnection_SendClient_Call{Call: _e.mock.On("SendClient", clientMessage)}
301+}
302+
303+func (_c *MockClientConnection_SendClient_Call) Run(run func(clientMessage protocol.ClientMessage)) *MockClientConnection_SendClient_Call {
304+ _c.Call.Run(func(args mock.Arguments) {
305+ var arg0 protocol.ClientMessage
306+ if args[0] != nil {
307+ arg0 = args[0].(protocol.ClientMessage)
308+ }
309+ run(
310+ arg0,
311+ )
312+ })
313+ return _c
314+}
315+
316+func (_c *MockClientConnection_SendClient_Call) Return(err error) *MockClientConnection_SendClient_Call {
317+ _c.Call.Return(err)
318+ return _c
319+}
320+
321+func (_c *MockClientConnection_SendClient_Call) RunAndReturn(run func(clientMessage protocol.ClientMessage) error) *MockClientConnection_SendClient_Call {
322+ _c.Call.Return(run)
323+ return _c
324+}
325+
326+// NewMockClientDialer creates a new instance of MockClientDialer. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
327+// The first argument is typically a *testing.T value.
328+func NewMockClientDialer(t interface {
329+ mock.TestingT
330+ Cleanup(func())
331+}) *MockClientDialer {
332+ mock := &MockClientDialer{}
333+ mock.Mock.Test(t)
334+
335+ t.Cleanup(func() { mock.AssertExpectations(t) })
336+
337+ return mock
338+}
339+
340+// MockClientDialer is an autogenerated mock type for the ClientDialer type
341+type MockClientDialer struct {
342+ mock.Mock
343+}
344+
345+type MockClientDialer_Expecter struct {
346+ mock *mock.Mock
347+}
348+
349+func (_m *MockClientDialer) EXPECT() *MockClientDialer_Expecter {
350+ return &MockClientDialer_Expecter{mock: &_m.Mock}
351+}
352+
353+// Dial provides a mock function for the type MockClientDialer
354+func (_mock *MockClientDialer) Dial(context1 context.Context) (ports.ClientConnection, error) {
355+ ret := _mock.Called(context1)
356+
357+ if len(ret) == 0 {
358+ panic("no return value specified for Dial")
359+ }
360+
361+ var r0 ports.ClientConnection
362+ var r1 error
363+ if returnFunc, ok := ret.Get(0).(func(context.Context) (ports.ClientConnection, error)); ok {
364+ return returnFunc(context1)
365+ }
366+ if returnFunc, ok := ret.Get(0).(func(context.Context) ports.ClientConnection); ok {
367+ r0 = returnFunc(context1)
368+ } else {
369+ if ret.Get(0) != nil {
370+ r0 = ret.Get(0).(ports.ClientConnection)
371+ }
372+ }
373+ if returnFunc, ok := ret.Get(1).(func(context.Context) error); ok {
374+ r1 = returnFunc(context1)
375+ } else {
376+ r1 = ret.Error(1)
377+ }
378+ return r0, r1
379+}
380+
381+// MockClientDialer_Dial_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Dial'
382+type MockClientDialer_Dial_Call struct {
383+ *mock.Call
384+}
385+
386+// Dial is a helper method to define mock.On call
387+// - context1 context.Context
388+func (_e *MockClientDialer_Expecter) Dial(context1 any) *MockClientDialer_Dial_Call {
389+ return &MockClientDialer_Dial_Call{Call: _e.mock.On("Dial", context1)}
390+}
391+
392+func (_c *MockClientDialer_Dial_Call) Run(run func(context1 context.Context)) *MockClientDialer_Dial_Call {
393+ _c.Call.Run(func(args mock.Arguments) {
394+ var arg0 context.Context
395+ if args[0] != nil {
396+ arg0 = args[0].(context.Context)
397+ }
398+ run(
399+ arg0,
400+ )
401+ })
402+ return _c
403+}
404+
405+func (_c *MockClientDialer_Dial_Call) Return(clientConnection ports.ClientConnection, err error) *MockClientDialer_Dial_Call {
406+ _c.Call.Return(clientConnection, err)
407+ return _c
408+}
409+
410+func (_c *MockClientDialer_Dial_Call) RunAndReturn(run func(context1 context.Context) (ports.ClientConnection, error)) *MockClientDialer_Dial_Call {
411+ _c.Call.Return(run)
412+ return _c
413+}
414+
13 // NewMockServerConnection creates a new instance of MockServerConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.415 // NewMockServerConnection creates a new instance of MockServerConnection. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
14 // The first argument is typically a *testing.T value.416 // The first argument is typically a *testing.T value.
15 func NewMockServerConnection(t interface {417 func NewMockServerConnection(t interface {
M internal/ports/session.go +20 -1
@@ -1,6 +1,25 @@
11 package ports
22
3-import "github.com/bnema/vev/internal/protocol"
3+import (
4+ "context"
5+
6+ "github.com/bnema/vev/internal/protocol"
7+)
8+
9+// ClientConnection is one typed client-side session channel.
10+type ClientConnection interface {
11+ SendClient(protocol.ClientMessage) error
12+ ReceiveServer() (protocol.ServerMessage, error)
13+ Capabilities() protocol.ConnectionCapabilities
14+ LinkState() LinkState
15+ LinkEvents() <-chan LinkEvent
16+ Close() error
17+}
18+
19+// ClientDialer establishes typed client-side session connections.
20+type ClientDialer interface {
21+ Dial(context.Context) (ClientConnection, error)
22+}
423
524 // ServerConnection is one typed client/daemon session channel. Close must be
625 // concurrent-safe and unblock ReceiveClient and every send mode.
@@ -1,6 +1,25 @@
1 package ports1 package ports
2 2
3-import "github.com/bnema/vev/internal/protocol"3+import (
4+ "context"
5+
6+ "github.com/bnema/vev/internal/protocol"
7+)
8+
9+// ClientConnection is one typed client-side session channel.
10+type ClientConnection interface {
11+ SendClient(protocol.ClientMessage) error
12+ ReceiveServer() (protocol.ServerMessage, error)
13+ Capabilities() protocol.ConnectionCapabilities
14+ LinkState() LinkState
15+ LinkEvents() <-chan LinkEvent
16+ Close() error
17+}
18+
19+// ClientDialer establishes typed client-side session connections.
20+type ClientDialer interface {
21+ Dial(context.Context) (ClientConnection, error)
22+}
4 23
5 // ServerConnection is one typed client/daemon session channel. Close must be24 // ServerConnection is one typed client/daemon session channel. Close must be
6 // concurrent-safe and unblock ReceiveClient and every send mode.25 // concurrent-safe and unblock ReceiveClient and every send mode.
M internal/usecase/client/ack_queue_test.go +11 -9
@@ -7,6 +7,8 @@ import (
77 "testing"
88 "time"
99
10+ "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/protocol"
1012 "github.com/bnema/vev/internal/protocol/wire"
1113 "github.com/stretchr/testify/require"
1214 )
@@ -18,9 +20,9 @@ func (t *ackRecordingTransport) Recv() (wire.Frame, error) { return wire.Frame{}
1820 func (t *ackRecordingTransport) Close() error { return nil }
1921
2022 func TestCumulativeAckBypassesFullNormalSendQueue(t *testing.T) {
21- normal := make(chan wire.Frame, sendQueueDepth)
23+ normal := make(chan protocol.ClientMessage, sendQueueDepth)
2224 for range cap(normal) {
23- normal <- wire.Frame{Type: wire.MsgInput}
25+ normal <- protocol.Input{}
2426 }
2527 const epoch = 7
2628 acks := newCumulativeAckQueue()
@@ -45,8 +47,8 @@ func TestCumulativeAckBypassesFullNormalSendQueue(t *testing.T) {
4547 func TestSamePeerSwitchControlBypassesHeldInput(t *testing.T) {
4648 ctx, cancel := context.WithCancel(context.Background())
4749 defer cancel()
48- normal := make(chan wire.Frame, 1)
49- control := make(chan wire.Frame, 1)
50+ normal := make(chan protocol.ClientMessage, 1)
51+ control := make(chan protocol.ClientMessage, 1)
5052 gate := newSamePeerInputGate()
5153 gate.setPaused(true)
5254 held := make(chan struct{})
@@ -56,9 +58,9 @@ func TestSamePeerSwitchControlBypassesHeldInput(t *testing.T) {
5658 errs := make(chan error, 1)
5759 go runSender(ctx, cancel, transport, control, nil, normal, gate, acks, errs, slog.Default())
5860
59- normal <- wire.Frame{Type: wire.MsgInput, Payload: []byte("held")}
61+ normal <- protocol.Input{Data: []byte("held")}
6062 awaitSenderSignal(t, held)
61- control <- wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: []byte("switch")}
63+ control <- protocol.SamePeerSwitchRequest{RequestID: 1, Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}}
6264 if got := awaitSenderFrame(t, transport.sent); got.Type != wire.MsgSamePeerSwitchRequest {
6365 t.Fatalf("first frame = %d, want same-peer switch request", got.Type)
6466 }
@@ -76,14 +78,14 @@ func TestSamePeerSwitchControlBypassesHeldInput(t *testing.T) {
7678 func TestSenderBarrierLeavesPausedInputQueued(t *testing.T) {
7779 ctx, cancel := context.WithCancel(context.Background())
7880 defer cancel()
79- normal := make(chan wire.Frame, 1)
81+ normal := make(chan protocol.ClientMessage, 1)
8082 barriers := make(chan chan struct{})
8183 gate := newSamePeerInputGate()
8284 gate.setPaused(true)
8385 transport := &ackRecordingTransport{sent: make(chan wire.Frame, 1)}
8486 go runSender(ctx, cancel, transport, nil, barriers, normal, gate, newCumulativeAckQueue(), make(chan error, 1), slog.Default())
8587
86- normal <- wire.Frame{Type: wire.MsgInput, Payload: []byte("held")}
88+ normal <- protocol.Input{Data: []byte("held")}
8789 barrierDone := make(chan struct{})
8890 barriers <- barrierDone
8991 awaitSenderSignal(t, barrierDone)
@@ -106,7 +108,7 @@ func TestSenderBarrierFlushesPendingAck(t *testing.T) {
106108 acks.offer(3, 9)
107109 transport := &ackRecordingTransport{sent: make(chan wire.Frame, 1)}
108110 barriers := make(chan chan struct{})
109- go runSender(ctx, cancel, transport, nil, barriers, make(chan wire.Frame, 1), newSamePeerInputGate(), acks, make(chan error, 1), slog.Default())
111+ go runSender(ctx, cancel, transport, nil, barriers, make(chan protocol.ClientMessage, 1), newSamePeerInputGate(), acks, make(chan error, 1), slog.Default())
110112
111113 done := make(chan struct{})
112114 barriers <- done
@@ -7,6 +7,8 @@ import (
7 "testing"7 "testing"
8 "time"8 "time"
9 9
10+ "github.com/bnema/vev/internal/domain"
11+ "github.com/bnema/vev/internal/protocol"
10 "github.com/bnema/vev/internal/protocol/wire"12 "github.com/bnema/vev/internal/protocol/wire"
11 "github.com/stretchr/testify/require"13 "github.com/stretchr/testify/require"
12 )14 )
@@ -18,9 +20,9 @@ func (t *ackRecordingTransport) Recv() (wire.Frame, error) { return wire.Frame{}
18 func (t *ackRecordingTransport) Close() error { return nil }20 func (t *ackRecordingTransport) Close() error { return nil }
19 21
20 func TestCumulativeAckBypassesFullNormalSendQueue(t *testing.T) {22 func TestCumulativeAckBypassesFullNormalSendQueue(t *testing.T) {
21- normal := make(chan wire.Frame, sendQueueDepth)23+ normal := make(chan protocol.ClientMessage, sendQueueDepth)
22 for range cap(normal) {24 for range cap(normal) {
23- normal <- wire.Frame{Type: wire.MsgInput}25+ normal <- protocol.Input{}
24 }26 }
25 const epoch = 727 const epoch = 7
26 acks := newCumulativeAckQueue()28 acks := newCumulativeAckQueue()
@@ -45,8 +47,8 @@ func TestCumulativeAckBypassesFullNormalSendQueue(t *testing.T) {
45 func TestSamePeerSwitchControlBypassesHeldInput(t *testing.T) {47 func TestSamePeerSwitchControlBypassesHeldInput(t *testing.T) {
46 ctx, cancel := context.WithCancel(context.Background())48 ctx, cancel := context.WithCancel(context.Background())
47 defer cancel()49 defer cancel()
48- normal := make(chan wire.Frame, 1)50+ normal := make(chan protocol.ClientMessage, 1)
49- control := make(chan wire.Frame, 1)51+ control := make(chan protocol.ClientMessage, 1)
50 gate := newSamePeerInputGate()52 gate := newSamePeerInputGate()
51 gate.setPaused(true)53 gate.setPaused(true)
52 held := make(chan struct{})54 held := make(chan struct{})
@@ -56,9 +58,9 @@ func TestSamePeerSwitchControlBypassesHeldInput(t *testing.T) {
56 errs := make(chan error, 1)58 errs := make(chan error, 1)
57 go runSender(ctx, cancel, transport, control, nil, normal, gate, acks, errs, slog.Default())59 go runSender(ctx, cancel, transport, control, nil, normal, gate, acks, errs, slog.Default())
58 60
59- normal <- wire.Frame{Type: wire.MsgInput, Payload: []byte("held")}61+ normal <- protocol.Input{Data: []byte("held")}
60 awaitSenderSignal(t, held)62 awaitSenderSignal(t, held)
61- control <- wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: []byte("switch")}63+ control <- protocol.SamePeerSwitchRequest{RequestID: 1, Target: protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}}
62 if got := awaitSenderFrame(t, transport.sent); got.Type != wire.MsgSamePeerSwitchRequest {64 if got := awaitSenderFrame(t, transport.sent); got.Type != wire.MsgSamePeerSwitchRequest {
63 t.Fatalf("first frame = %d, want same-peer switch request", got.Type)65 t.Fatalf("first frame = %d, want same-peer switch request", got.Type)
64 }66 }
@@ -76,14 +78,14 @@ func TestSamePeerSwitchControlBypassesHeldInput(t *testing.T) {
76 func TestSenderBarrierLeavesPausedInputQueued(t *testing.T) {78 func TestSenderBarrierLeavesPausedInputQueued(t *testing.T) {
77 ctx, cancel := context.WithCancel(context.Background())79 ctx, cancel := context.WithCancel(context.Background())
78 defer cancel()80 defer cancel()
79- normal := make(chan wire.Frame, 1)81+ normal := make(chan protocol.ClientMessage, 1)
80 barriers := make(chan chan struct{})82 barriers := make(chan chan struct{})
81 gate := newSamePeerInputGate()83 gate := newSamePeerInputGate()
82 gate.setPaused(true)84 gate.setPaused(true)
83 transport := &ackRecordingTransport{sent: make(chan wire.Frame, 1)}85 transport := &ackRecordingTransport{sent: make(chan wire.Frame, 1)}
84 go runSender(ctx, cancel, transport, nil, barriers, normal, gate, newCumulativeAckQueue(), make(chan error, 1), slog.Default())86 go runSender(ctx, cancel, transport, nil, barriers, normal, gate, newCumulativeAckQueue(), make(chan error, 1), slog.Default())
85 87
86- normal <- wire.Frame{Type: wire.MsgInput, Payload: []byte("held")}88+ normal <- protocol.Input{Data: []byte("held")}
87 barrierDone := make(chan struct{})89 barrierDone := make(chan struct{})
88 barriers <- barrierDone90 barriers <- barrierDone
89 awaitSenderSignal(t, barrierDone)91 awaitSenderSignal(t, barrierDone)
@@ -106,7 +108,7 @@ func TestSenderBarrierFlushesPendingAck(t *testing.T) {
106 acks.offer(3, 9)108 acks.offer(3, 9)
107 transport := &ackRecordingTransport{sent: make(chan wire.Frame, 1)}109 transport := &ackRecordingTransport{sent: make(chan wire.Frame, 1)}
108 barriers := make(chan chan struct{})110 barriers := make(chan chan struct{})
109- go runSender(ctx, cancel, transport, nil, barriers, make(chan wire.Frame, 1), newSamePeerInputGate(), acks, make(chan error, 1), slog.Default())111+ go runSender(ctx, cancel, transport, nil, barriers, make(chan protocol.ClientMessage, 1), newSamePeerInputGate(), acks, make(chan error, 1), slog.Default())
110 112
111 done := make(chan struct{})113 done := make(chan struct{})
112 barriers <- done114 barriers <- done
M internal/usecase/client/client.go +127 -198
@@ -24,7 +24,6 @@ import (
2424 "github.com/bnema/vev/internal/domain"
2525 "github.com/bnema/vev/internal/ports"
2626 "github.com/bnema/vev/internal/protocol"
27- "github.com/bnema/vev/internal/protocol/wire"
2827 "github.com/bnema/vev/internal/usecase/theme"
2928 )
3029
@@ -152,10 +151,10 @@ const (
152151 )
153152
154153 // Dependencies supplies the collaborators required by a Runner.
155-type AttachHandoffFunc func(protocol.AttachTarget) (ports.Dialer, AttachRequest, error)
154+type AttachHandoffFunc func(protocol.AttachTarget) (ports.ClientDialer, AttachRequest, error)
156155
157156 type Dependencies struct {
158- Dialer ports.Dialer
157+ Dialer ports.ClientDialer
159158 Terminal ports.Terminal
160159 Clock ports.Clock
161160 // DisableCapabilityProbe skips active outer-terminal discovery for headless
@@ -198,13 +197,13 @@ type AttachRequest struct {
198197 // attachRoute captures the dialer, request, and resume token needed to
199198 // restore a previous attachment route after a navigation handoff.
200199 type attachRoute struct {
201- dialer ports.Dialer
200+ dialer ports.ClientDialer
202201 request AttachRequest
203202 resumeToken uint64
204203 }
205204
206205 type Runner struct {
207- dialer ports.Dialer
206+ dialer ports.ClientDialer
208207 term ports.Terminal
209208 clock ports.Clock
210209 clipboard ports.ClipboardReader
@@ -649,8 +648,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
649648 }
650649
651650 var linkEvents <-chan ports.LinkEvent
652- if reporter, ok := connection.Transport().(ports.LinkStateReporter); ok {
653- linkEvents = reporter.LinkEvents()
651+ if connection.Connection().Capabilities().LinkState {
652+ linkEvents = connection.Connection().LinkEvents()
654653 }
655654 result := (&attachAttempt{
656655 runner: r,
@@ -803,7 +802,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
803802 }
804803 }
805804 var (
806- nextDialer ports.Dialer
805+ nextDialer ports.ClientDialer
807806 nextRequest AttachRequest
808807 handoffErr error
809808 )
@@ -1058,10 +1057,10 @@ func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time
10581057
10591058 type attachAttempt struct {
10601059 runner *Runner
1061- dialer ports.Dialer
1060+ dialer ports.ClientDialer
10621061 connection *SessionConnection
10631062 remote bool
1064- transport ports.Transport
1063+ transport ports.ClientConnection
10651064 handshakeCtx context.Context
10661065 handshakeTimedOut <-chan struct{}
10671066 finishHandshake func()
@@ -1125,17 +1124,18 @@ func DetectTrueColor(termEnv, colorTerm string) bool {
11251124 return ports.DetectTrueColor(termEnv, colorTerm)
11261125 }
11271126
1128-func requestedOutputWindow(transport ports.Transport) uint8 {
1129- if _, ok := transport.(ports.DatagramTransport); ok {
1130- return 1
1127+func requestedOutputWindow(connection ports.ClientConnection) uint8 {
1128+ window := connection.Capabilities().PreferredOutputWindow
1129+ if window == 0 {
1130+ return 8
11311131 }
1132- return 8
1132+ return window
11331133 }
11341134
11351135 func (a *attachAttempt) run(ctx context.Context) attachResult {
11361136 transport := a.transport
11371137 if a.connection != nil {
1138- transport = a.connection.Transport()
1138+ transport = a.connection.Connection()
11391139 }
11401140 request := a.request
11411141 term := a.runner.term
@@ -1244,18 +1244,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
12441244 Remote: remote,
12451245 }
12461246 if err := sendHandshake(func() error {
1247- return transport.Send(wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(hello)})
1247+ return transport.SendClient(hello)
12481248 }); err != nil {
12491249 return handshakeFailure("sending hello", err)
12501250 }
12511251 ms.helloSent = true
12521252
12531253 // 2. Await Welcome or a typed rejection.
1254- var reply wire.Frame
1254+ var reply protocol.ServerMessage
12551255 if err := boundedHandshakeOperationWithTransition(handshakeCtx, transport, func() error {
1256- var recvErr error
1257- reply, recvErr = transport.Recv()
1258- return recvErr
1256+ var receiveErr error
1257+ reply, receiveErr = transport.ReceiveServer()
1258+ return receiveErr
12591259 }, transition); err != nil {
12601260 return handshakeFailure("awaiting welcome", err)
12611261 }
@@ -1271,12 +1271,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
12711271 err: err,
12721272 }
12731273 }
1274- switch reply.Type {
1275- case wire.MsgWelcome:
1276- welcome, derr := wire.UnmarshalWelcome(reply.Payload)
1277- if derr != nil {
1278- return result(false, fmt.Errorf("vev: decoding welcome: %w", derr))
1279- }
1274+ switch message := reply.(type) {
1275+ case protocol.Welcome:
1276+ welcome := message
12801277 resumeToken = welcome.ResumeToken
12811278 name = welcome.SessionName
12821279 committedIdentity = cloneCommittedIdentity(welcome.CommittedIdentity)
@@ -1287,14 +1284,10 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
12871284 remember()
12881285 }
12891286 }
1290- case wire.MsgError:
1291- em, derr := wire.UnmarshalErrorMsg(reply.Payload)
1292- if derr != nil {
1293- return result(false, fmt.Errorf("vev: decoding error reply: %w", derr))
1294- }
1295- return result(false, &ProtocolError{Code: em.Code, Text: em.Text})
1287+ case protocol.ErrorMsg:
1288+ return result(false, &ProtocolError{Code: message.Code, Text: message.Text})
12961289 default:
1297- return result(false, fmt.Errorf("vev: unexpected reply type %d before welcome", reply.Type))
1290+ return result(false, fmt.Errorf("vev: unexpected %T before welcome", reply))
12981291 }
12991292 if err := handshakeCtx.Err(); err != nil {
13001293 return handshakeFailure("validating welcome", err)
@@ -1320,30 +1313,20 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
13201313 return welcomedResult(fmt.Errorf("vev: committing route identity: %w", commitErr))
13211314 }
13221315 }
1323- snapshotPayload, err := wire.MarshalRecentRouteSnapshot(a.runner.ledger.snapshot())
1324- if err != nil {
1325- return welcomedResult(fmt.Errorf("vev: encoding route snapshot: %w", err))
1326- }
1327- attentionPayload, err := wire.MarshalRouteAttentionSubscription(a.runner.ledger.attentionSubscription())
1328- if err != nil {
1329- return welcomedResult(fmt.Errorf("vev: encoding route attention subscription: %w", err))
1330- }
1316+ snapshot := a.runner.ledger.snapshot()
1317+ attention := a.runner.ledger.attentionSubscription()
13311318 if err := sendHandshake(func() error {
1332- if err := transport.Send(wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload}); err != nil {
1319+ if err := transport.SendClient(snapshot); err != nil {
13331320 return err
13341321 }
1335- return transport.Send(wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload})
1322+ return transport.SendClient(attention)
13361323 }); err != nil {
13371324 return welcomedResult(fmt.Errorf("vev: publishing route snapshot: %w", err))
13381325 }
13391326 }
13401327 if a.runner.routeFailure != nil {
1341- payload, err := wire.MarshalRouteNavigationFailure(*a.runner.routeFailure)
1342- if err != nil {
1343- return welcomedResult(fmt.Errorf("vev: encoding route navigation failure: %w", err))
1344- }
13451328 if err := sendHandshake(func() error {
1346- return transport.Send(wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload})
1329+ return transport.SendClient(*a.runner.routeFailure)
13471330 }); err != nil {
13481331 return welcomedResult(fmt.Errorf("vev: publishing route navigation failure: %w", err))
13491332 }
@@ -1370,8 +1353,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
13701353 input.start()
13711354 defer input.stop()
13721355 }
1373- sendCh := make(chan wire.Frame, sendQueueDepth)
1374- controlCh := make(chan wire.Frame, 1)
1356+ sendCh := make(chan protocol.ClientMessage, sendQueueDepth)
1357+ controlCh := make(chan protocol.ClientMessage, 1)
13751358 barrierCh := make(chan chan struct{})
13761359 inputGate := newSamePeerInputGate()
13771360 ackQueue := newCumulativeAckQueue()
@@ -1394,18 +1377,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
13941377 }
13951378 }
13961379 publish := func(snapshot protocol.Theme) error {
1397- frame := wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(snapshot)}
1380+ message := protocol.ClientMessage(snapshot)
13981381 // The initial cleared snapshot is sent before the sender exists. This
13991382 // keeps the generation publication ordered before the first query and
14001383 // avoids leaving a queued Theme behind an immediate detach.
14011384 if !senderStarted {
1402- if err := sendHandshake(func() error { return transport.Send(frame) }); err != nil {
1385+ if err := sendHandshake(func() error { return transport.SendClient(message) }); err != nil {
14031386 return fmt.Errorf("sending initial theme: %w", err)
14041387 }
14051388 return nil
14061389 }
14071390 select {
1408- case sendCh <- frame:
1391+ case sendCh <- message:
14091392 return nil
14101393 case <-loopCtx.Done():
14111394 return context.Canceled
@@ -1504,11 +1487,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
15041487 // Before the asynchronous sender starts, preserve transport ordering by
15051488 // requesting the reset synchronously after the initial Theme publication.
15061489 resetErr := sendHandshake(func() error {
1507- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1508- if err != nil {
1509- return err
1510- }
1511- return transport.Send(wire.Frame{Type: wire.MsgResize, Payload: payload})
1490+ return transport.SendClient(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
15121491 })
15131492 if resetErr != nil {
15141493 resetErr = handshakeContextError(ctx, handshakeTimedOut, resetErr)
@@ -1630,12 +1609,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16301609 return fmt.Errorf("reading terminal geometry for parked route: %w", err)
16311610 }
16321611 geometry = geometry.NormalizePixels()
1633- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1634- if err != nil {
1635- return fmt.Errorf("encoding terminal size for parked route: %w", err)
1612+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1613+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1614+ return fmt.Errorf("validating terminal size for parked route: %w", err)
16361615 }
16371616 select {
1638- case controlCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:
1617+ case controlCh <- message:
16391618 return nil
16401619 case <-loopCtx.Done():
16411620 return context.Canceled
@@ -1647,12 +1626,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16471626 }
16481627 nextParkedRequestID++
16491628 request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}
1650- payload := wire.MarshalParkedRouteRequest(request)
1651- if payload == nil {
1629+ if protocol.ValidateParkedRouteRequest(request) != nil {
16521630 return errors.New("vev: invalid parked-route request")
16531631 }
16541632 select {
1655- case controlCh <- wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: payload}:
1633+ case controlCh <- request:
16561634 parkedPending = &parkedRoutePending{
16571635 requestID: request.RequestID, action: action, leaseID: leaseID,
16581636 timer: clk.NewTimer(protocol.HandshakeTimeout),
@@ -1727,12 +1705,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17271705 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)
17281706 }
17291707 geometry = geometry.NormalizePixels()
1730- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1731- if err != nil {
1732- return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err)
1708+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1709+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1710+ return fmt.Errorf("validating terminal size for reconnect reconciliation: %w", err)
17331711 }
17341712 select {
1735- case sendCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:
1713+ case sendCh <- message:
17361714 awaitingReconnectReset = true
17371715 return nil
17381716 case <-loopCtx.Done():
@@ -1743,20 +1721,13 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17431721 if a.runner.ledger == nil {
17441722 return errors.New("vev: route ledger unavailable")
17451723 }
1746- snapshotPayload, err := wire.MarshalRecentRouteSnapshot(a.runner.ledger.snapshot())
1747- if err != nil {
1748- return err
1749- }
1750- attentionPayload, err := wire.MarshalRouteAttentionSubscription(a.runner.ledger.attentionSubscription())
1751- if err != nil {
1752- return err
1724+ messages := []protocol.ClientMessage{
1725+ a.runner.ledger.snapshot(),
1726+ a.runner.ledger.attentionSubscription(),
17531727 }
1754- for _, frame := range []wire.Frame{
1755- {Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload},
1756- {Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload},
1757- } {
1728+ for _, message := range messages {
17581729 select {
1759- case sendCh <- frame:
1730+ case sendCh <- message:
17601731 case <-loopCtx.Done():
17611732 return context.Canceled
17621733 }
@@ -1764,16 +1735,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17641735 return nil
17651736 }
17661737 sendRouteFailure := func(action protocol.RouteNavigationAction, code protocol.RouteFailureCode) error {
1767- payload, err := wire.MarshalRouteNavigationFailure(protocol.RouteNavigationFailure{
1768- Key: action.Key,
1769- Generation: action.Generation,
1770- Code: code,
1771- })
1772- if err != nil {
1773- return err
1738+ failure := protocol.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: code}
1739+ if failure.Code.Validate() != nil || failure.Key == 0 || failure.Generation == 0 {
1740+ return protocol.ErrInvalidRouteWire
17741741 }
17751742 select {
1776- case sendCh <- wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}:
1743+ case sendCh <- failure:
17771744 return nil
17781745 case <-loopCtx.Done():
17791746 return context.Canceled
@@ -1828,7 +1795,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18281795 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))
18291796 }
18301797 select {
1831- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}:
1798+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}:
18321799 case <-loopCtx.Done():
18331800 return welcomedResult(nil)
18341801 }
@@ -1837,7 +1804,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18371804 if ev.State == ports.LinkStateDegraded {
18381805 log.Warn("UDP link degraded")
18391806 select {
1840- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})}:
1807+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded}:
18411808 case <-loopCtx.Done():
18421809 return welcomedResult(nil)
18431810 }
@@ -1884,12 +1851,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18841851 }
18851852 return welcomedResult(fmt.Errorf("vev: receiving from daemon: %w", r.err))
18861853 }
1887- switch r.frame.Type {
1888- case wire.MsgOutput:
1889- o, derr := wire.UnmarshalOutput(r.frame.Payload)
1890- if derr != nil {
1891- return welcomedResult(fmt.Errorf("vev: decoding output: %w", derr))
1892- }
1854+ switch message := r.message.(type) {
1855+ case protocol.Output:
1856+ o := message
18931857 nextState, accepted := outputState.next(o)
18941858 if !accepted {
18951859 // A discarded frame was never written and must never be ACKed.
@@ -1897,7 +1861,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18971861 // repeated gaps while that reset is in flight.
18981862 if !outputResetRequested {
18991863 select {
1900- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
1864+ case sendCh <- protocol.OutputResetRequest{}:
19011865 outputResetRequested = true
19021866 case <-loopCtx.Done():
19031867 return loopCanceledResult()
@@ -1947,11 +1911,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19471911 log.Debug("received first output")
19481912 endHandshake()
19491913 }
1950- case wire.MsgAttachTarget:
1951- target, derr := wire.UnmarshalAttachTarget(r.frame.Payload)
1952- if derr != nil {
1953- return welcomedResult(fmt.Errorf("vev: decoding attach target: %w", derr))
1954- }
1914+ case protocol.AttachTarget:
1915+ target := message
19551916 if a.returnAttachTargets {
19561917 handoff := welcomedResult(nil)
19571918 handoff.target = &target
@@ -1967,18 +1928,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19671928 }
19681929 nextSamePeerRequestID++
19691930 request := a.runner.ledger.samePeerHandoff(request, target)
1970- payload, err := wire.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{
1931+ switchRequest := protocol.SamePeerSwitchRequest{
19711932 RequestID: nextSamePeerRequestID,
19721933 Target: *target.ExactTarget,
19731934 PreferredTabID: request.PreferredTabID,
1974- })
1975- if err != nil {
1976- return welcomedResult(fmt.Errorf("vev: encoding same-peer switch: %w", err))
1935+ }
1936+ if switchRequest.Validate() != nil {
1937+ return welcomedResult(errors.New("vev: invalid same-peer switch"))
19771938 }
19781939 samePeerSwitch = &samePeerSwitchPending{requestID: nextSamePeerRequestID, target: *target.ExactTarget}
19791940 inputGate.setPaused(true)
19801941 select {
1981- case controlCh <- wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: payload}:
1942+ case controlCh <- switchRequest:
19821943 continue
19831944 case <-loopCtx.Done():
19841945 return loopCanceledResult()
@@ -1987,11 +1948,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19871948 handoff := welcomedResult(nil)
19881949 handoff.target = &target
19891950 return handoff
1990- case wire.MsgParkedRouteResponse:
1991- response, derr := wire.UnmarshalParkedRouteResponse(r.frame.Payload)
1992- if derr != nil {
1993- return welcomedResult(fmt.Errorf("vev: decoding parked-route response: %w", derr))
1994- }
1951+ case protocol.ParkedRouteResponse:
1952+ response := message
19951953 if parkedPending == nil || response.RequestID != parkedPending.requestID {
19961954 return welcomedResult(errors.New("vev: unexpected parked-route response"))
19971955 }
@@ -2034,12 +1992,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20341992 return fallback
20351993 }
20361994 }
2037- case wire.MsgNavigationAction:
2038- directive, derr := wire.UnmarshalNavigationDirective(r.frame.Payload)
2039- if derr != nil {
2040- return welcomedResult(fmt.Errorf("vev: decoding navigation action: %w", derr))
2041- }
2042- _, datagramRoute := transport.(ports.DatagramTransport)
1995+ case protocol.NavigationDirective:
1996+ directive := message
1997+ datagramRoute := transport.Capabilities().PreferredOutputWindow == 1
20431998 if directive.Action == protocol.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {
20441999 stopForeground()
20452000 if err := flushSender(); err != nil {
@@ -2053,11 +2008,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20532008 navigation := welcomedResult(nil)
20542009 navigation.action = directive.Action
20552010 return navigation
2056- case wire.MsgNavigateRecentRoute:
2057- action, derr := wire.UnmarshalRouteNavigationAction(r.frame.Payload)
2058- if derr != nil {
2059- return welcomedResult(fmt.Errorf("vev: decoding route navigation action: %w", derr))
2060- }
2011+ case protocol.RouteNavigationAction:
2012+ action := message
20612013 if a.runner.ledger == nil {
20622014 if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {
20632015 return welcomedResult(ferr)
@@ -2077,11 +2029,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20772029 navigation := welcomedResult(nil)
20782030 navigation.routeAction = &action
20792031 return navigation
2080- case wire.MsgCommittedRouteIdentity:
2081- identity, derr := wire.UnmarshalCommittedRouteIdentity(r.frame.Payload)
2082- if derr != nil {
2083- return welcomedResult(fmt.Errorf("vev: decoding committed route identity: %w", derr))
2084- }
2032+ case protocol.CommittedRouteIdentity:
2033+ identity := message
20852034 if samePeerSwitch != nil && identity.Target != samePeerSwitch.target {
20862035 return welcomedResult(errors.New("vev: same-peer switch committed an unexpected target"))
20872036 }
@@ -2100,11 +2049,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21002049 samePeerSwitch = nil
21012050 inputGate.setPaused(false)
21022051 }
2103- case wire.MsgSamePeerSwitchFailure:
2104- failure, derr := wire.UnmarshalSamePeerSwitchFailure(r.frame.Payload)
2105- if derr != nil {
2106- return welcomedResult(fmt.Errorf("vev: decoding same-peer switch failure: %w", derr))
2107- }
2052+ case protocol.SamePeerSwitchFailure:
2053+ failure := message
21082054 if samePeerSwitch != nil && failure.RequestID == samePeerSwitch.requestID {
21092055 samePeerSwitch = nil
21102056 inputGate.setPaused(false)
@@ -2112,7 +2058,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21122058 transitionWaitingFull = true
21132059 if !outputResetRequested {
21142060 select {
2115- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
2061+ case sendCh <- protocol.OutputResetRequest{}:
21162062 outputResetRequested = true
21172063 case <-loopCtx.Done():
21182064 return loopCanceledResult()
@@ -2122,11 +2068,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21222068 transition.stop()
21232069 }
21242070 }
2125- case wire.MsgRoutePosition:
2126- position, derr := wire.UnmarshalRoutePosition(r.frame.Payload)
2127- if derr != nil {
2128- return welcomedResult(fmt.Errorf("vev: decoding route position: %w", derr))
2129- }
2071+ case protocol.RoutePosition:
2072+ position := message
21302073 if a.runner.ledger == nil {
21312074 return welcomedResult(errors.New("vev: route ledger unavailable"))
21322075 }
@@ -2134,17 +2077,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21342077 return welcomedResult(fmt.Errorf("vev: remembering route position: %w", derr))
21352078 }
21362079 routePosition = cloneRoutePosition(&position)
2137- case wire.MsgRouteNavigationFailure:
2138- failure, derr := wire.UnmarshalRouteNavigationFailure(r.frame.Payload)
2139- if derr != nil {
2140- return welcomedResult(fmt.Errorf("vev: decoding route navigation failure: %w", derr))
2141- }
2080+ case protocol.RouteNavigationFailure:
2081+ failure := message
21422082 log.Warn("route navigation rejected", "key", failure.Key, "generation", failure.Generation, "code", failure.Code)
2143- case wire.MsgDetached:
2144- d, derr := wire.UnmarshalDetached(r.frame.Payload)
2145- if derr != nil {
2146- return welcomedResult(fmt.Errorf("vev: decoding detached: %w", derr))
2147- }
2083+ case protocol.Detached:
2084+ d := message
21482085 ms.detached = true
21492086 if d.Reason == protocol.ReasonDetach {
21502087 log.Info("detached cleanly", "reason", d.Reason)
@@ -2152,34 +2089,36 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21522089 log.Warn("detached by daemon", "reason", d.Reason)
21532090 }
21542091 return welcomedResult(detachedResult(d.Reason))
2155- case wire.MsgPong:
2092+ case protocol.Pong:
21562093 // Liveness reply; nothing to do.
2157- default:
2158- // Unknown/out-of-band message types are ignored so a newer
2159- // daemon can add server->client messages without breaking us.
21602094 }
21612095 }
21622096 }
21632097 }
21642098
2165-// recvResult carries one framed message (or a read error) from the receive
2166-// pump to the main loop.
2099+// recvResult carries one typed message (or a read error) from the receive pump.
21672100 type recvResult struct {
2168- frame wire.Frame
2169- err error
2101+ message protocol.ServerMessage
2102+ err error
21702103 }
21712104
2172-// runRecv reads frames until an error, forwarding each to the main loop.
2105+// runRecv reads typed messages until an error, forwarding each to the main loop.
21732106 // It exits on the first error or when the loop context is cancelled.
2174-func runRecv(ctx context.Context, transport ports.Transport, out chan<- recvResult, failed chan<- struct{}, log *slog.Logger) {
2107+func runRecv(ctx context.Context, transport ports.ClientConnection, out chan<- recvResult, failed chan<- struct{}, log *slog.Logger) {
21752108 defer log.Debug("receive pump exited")
21762109 for {
2177- f, err := transport.Recv()
2110+ message, err := transport.ReceiveServer()
2111+ if err != nil {
2112+ var failure *protocol.DecodeFailure
2113+ if errors.As(err, &failure) && (failure.Category == protocol.DecodeUnknownType || failure.Category == protocol.DecodeWrongDirection) {
2114+ continue
2115+ }
2116+ }
21782117 if err != nil && failed != nil {
21792118 close(failed)
21802119 }
21812120 select {
2182- case out <- recvResult{frame: f, err: err}:
2121+ case out <- recvResult{message: message, err: err}:
21832122 case <-ctx.Done():
21842123 return
21852124 }
@@ -2189,9 +2128,9 @@ func runRecv(ctx context.Context, transport ports.Transport, out chan<- recvResu
21892128 }
21902129 }
21912130
2192-// runSender is the sole caller of transport.Send: serialising the input and
2193-// resize pumps through one goroutine keeps the transport's single-writer
2194-// contract intact. A send failure cancels the loop and is surfaced once.
2131+// runSender is the sole caller of SendClient: serialising the input and resize
2132+// pumps through one goroutine keeps the connection's single-writer contract.
2133+// A send failure cancels the loop and is surfaced once.
21952134 type cumulativeAckQueue struct {
21962135 mu sync.Mutex
21972136 latestEpoch uint64
@@ -2228,10 +2167,10 @@ func (q *cumulativeAckQueue) take() (uint64, uint64) {
22282167
22292168 // runSender preserves normal-frame order while allowing switch control and
22302169 // output ACKs to make progress when raw input is held for a same-peer switch.
2231-func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.Transport, control <-chan wire.Frame, barriers <-chan chan struct{}, in <-chan wire.Frame, inputGate *samePeerInputGate, acks *cumulativeAckQueue, errCh chan<- error, log *slog.Logger) {
2170+func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.ClientConnection, control <-chan protocol.ClientMessage, barriers <-chan chan struct{}, in <-chan protocol.ClientMessage, inputGate *samePeerInputGate, acks *cumulativeAckQueue, errCh chan<- error, log *slog.Logger) {
22322171 defer log.Debug("sender pump exited")
2233- send := func(f wire.Frame) bool {
2234- if err := transport.Send(f); err != nil {
2172+ send := func(message protocol.ClientMessage) bool {
2173+ if err := transport.SendClient(message); err != nil {
22352174 select {
22362175 case errCh <- err:
22372176 default:
@@ -2246,16 +2185,7 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
22462185 if state == 0 {
22472186 return true
22482187 }
2249- payload, err := wire.MarshalAck(protocol.Ack{Epoch: epoch, State: state})
2250- if err != nil {
2251- select {
2252- case errCh <- fmt.Errorf("encoding output ACK: %w", err):
2253- default:
2254- }
2255- cancel()
2256- return false
2257- }
2258- return send(wire.Frame{Type: wire.MsgAck, Payload: payload})
2188+ return send(protocol.Ack{Epoch: epoch, State: state})
22592189 }
22602190 flushPending := func() bool {
22612191 for {
@@ -2287,12 +2217,12 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
22872217 close(done)
22882218 return true
22892219 }
2290- var heldInput *wire.Frame
2220+ var heldInput protocol.ClientMessage
22912221 for {
22922222 if heldInput != nil {
22932223 paused, changed := inputGate.snapshot()
22942224 if !paused {
2295- if !send(*heldInput) {
2225+ if !send(heldInput) {
22962226 return
22972227 }
22982228 heldInput = nil
@@ -2348,23 +2278,23 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
23482278 if !completeBarrier(done) {
23492279 return
23502280 }
2351- case frame := <-in:
2281+ case message := <-in:
23522282 select {
23532283 case <-ctx.Done():
23542284 return
23552285 default:
23562286 }
2357- if frame.Type == wire.MsgInput || frame.Type == wire.MsgImagePush {
2287+ switch message.(type) {
2288+ case protocol.Input, protocol.ImagePush:
23582289 if paused, _ := inputGate.snapshot(); paused {
2359- held := frame
2360- heldInput = &held
2290+ heldInput = message
23612291 if inputGate != nil && inputGate.afterInputHeld != nil {
23622292 inputGate.afterInputHeld()
23632293 }
23642294 continue
23652295 }
23662296 }
2367- if !send(frame) {
2297+ if !send(message) {
23682298 return
23692299 }
23702300 case <-ctx.Done():
@@ -2726,7 +2656,7 @@ type stdinPump struct {
27262656 in io.Reader // compatibility for isolated scanner tests
27272657 input *terminalInputPump
27282658 consumer uint64
2729- out chan<- wire.Frame
2659+ out chan<- protocol.ClientMessage
27302660 clock ports.Clock
27312661 clipboard ports.ClipboardReader
27322662 logger *slog.Logger
@@ -2747,13 +2677,13 @@ func (p *stdinPump) run() {
27472677 var undeliveredMu sync.Mutex
27482678 var undelivered []byte
27492679 sendOK.Store(true)
2750- send := func(frame wire.Frame) bool {
2680+ send := func(message protocol.ClientMessage) bool {
27512681 if !sendOK.Load() {
27522682 return false
27532683 }
27542684 return p.sendLease.send(func() bool {
27552685 select {
2756- case p.out <- frame:
2686+ case p.out <- message:
27572687 if p.afterInputDelivered != nil {
27582688 p.afterInputDelivered()
27592689 }
@@ -2786,7 +2716,7 @@ func (p *stdinPump) run() {
27862716 return
27872717 }
27882718 inputSeq++
2789- if !send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {
2719+ if !send(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)}) {
27902720 undeliveredMu.Lock()
27912721 undelivered = append(undelivered, data...)
27922722 undeliveredMu.Unlock()
@@ -2800,11 +2730,11 @@ func (p *stdinPump) run() {
28002730 reader: p.clipboard,
28012731 log: p.logger,
28022732 sendNotice: func(action uint8) {
2803- send(wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: action})})
2733+ send(protocol.ClientNotice{Action: action})
28042734 },
28052735 sendImage: func(mime string, data []byte) {
28062736 inputSeq++
2807- send(wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})
2737+ send(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})
28082738 },
28092739 next: coalescer.Scan,
28102740 }
@@ -2946,7 +2876,7 @@ func (p *stdinPump) run() {
29462876 disarmMarkerDeadline()
29472877 markers.flush(sink)
29482878 select {
2949- case p.out <- wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(protocol.Detach{})}:
2879+ case p.out <- protocol.Detach{}:
29502880 case <-p.ctx.Done():
29512881 }
29522882 p.cancel()
@@ -2967,7 +2897,7 @@ func (p *stdinPump) run() {
29672897 // runResize forwards coalesced terminal resize events to the daemon. It
29682898 // tolerates an already-closed resize channel (which the terminal adapter
29692899 // hands back when restore ran before ResizeEvents was first called).
2970-func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- wire.Frame, sendLease *foregroundSendLease, log *slog.Logger) {
2900+func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- protocol.ClientMessage, sendLease *foregroundSendLease, log *slog.Logger) {
29712901 defer log.Debug("resize pump exited")
29722902 for {
29732903 select {
@@ -2976,15 +2906,14 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- wi
29762906 return
29772907 }
29782908 geometry = geometry.NormalizePixels()
2979- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
2980- if err != nil {
2981- log.Error("encoding terminal resize", "error", err)
2909+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
2910+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
2911+ log.Error("validating terminal resize", "error", err)
29822912 continue
29832913 }
2984- frame := wire.Frame{Type: wire.MsgResize, Payload: payload}
29852914 if !sendLease.send(func() bool {
29862915 select {
2987- case out <- frame:
2916+ case out <- message:
29882917 return true
29892918 case <-ctx.Done():
29902919 return false
@@ -24,7 +24,6 @@ import (
24 "github.com/bnema/vev/internal/domain"24 "github.com/bnema/vev/internal/domain"
25 "github.com/bnema/vev/internal/ports"25 "github.com/bnema/vev/internal/ports"
26 "github.com/bnema/vev/internal/protocol"26 "github.com/bnema/vev/internal/protocol"
27- "github.com/bnema/vev/internal/protocol/wire"
28 "github.com/bnema/vev/internal/usecase/theme"27 "github.com/bnema/vev/internal/usecase/theme"
29 )28 )
30 29
@@ -152,10 +151,10 @@ const (
152 )151 )
153 152
154 // Dependencies supplies the collaborators required by a Runner.153 // Dependencies supplies the collaborators required by a Runner.
155-type AttachHandoffFunc func(protocol.AttachTarget) (ports.Dialer, AttachRequest, error)154+type AttachHandoffFunc func(protocol.AttachTarget) (ports.ClientDialer, AttachRequest, error)
156 155
157 type Dependencies struct {156 type Dependencies struct {
158- Dialer ports.Dialer157+ Dialer ports.ClientDialer
159 Terminal ports.Terminal158 Terminal ports.Terminal
160 Clock ports.Clock159 Clock ports.Clock
161 // DisableCapabilityProbe skips active outer-terminal discovery for headless160 // DisableCapabilityProbe skips active outer-terminal discovery for headless
@@ -198,13 +197,13 @@ type AttachRequest struct {
198 // attachRoute captures the dialer, request, and resume token needed to197 // attachRoute captures the dialer, request, and resume token needed to
199 // restore a previous attachment route after a navigation handoff.198 // restore a previous attachment route after a navigation handoff.
200 type attachRoute struct {199 type attachRoute struct {
201- dialer ports.Dialer200+ dialer ports.ClientDialer
202 request AttachRequest201 request AttachRequest
203 resumeToken uint64202 resumeToken uint64
204 }203 }
205 204
206 type Runner struct {205 type Runner struct {
207- dialer ports.Dialer206+ dialer ports.ClientDialer
208 term ports.Terminal207 term ports.Terminal
209 clock ports.Clock208 clock ports.Clock
210 clipboard ports.ClipboardReader209 clipboard ports.ClipboardReader
@@ -649,8 +648,8 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
649 }648 }
650 649
651 var linkEvents <-chan ports.LinkEvent650 var linkEvents <-chan ports.LinkEvent
652- if reporter, ok := connection.Transport().(ports.LinkStateReporter); ok {651+ if connection.Connection().Capabilities().LinkState {
653- linkEvents = reporter.LinkEvents()652+ linkEvents = connection.Connection().LinkEvents()
654 }653 }
655 result := (&attachAttempt{654 result := (&attachAttempt{
656 runner: r,655 runner: r,
@@ -803,7 +802,7 @@ func (r *Runner) Run(ctx context.Context, request AttachRequest) (retErr error)
803 }802 }
804 }803 }
805 var (804 var (
806- nextDialer ports.Dialer805+ nextDialer ports.ClientDialer
807 nextRequest AttachRequest806 nextRequest AttachRequest
808 handoffErr error807 handoffErr error
809 )808 )
@@ -1058,10 +1057,10 @@ func sleepReconnectWithResizeEvents(ctx context.Context, clk ports.Clock, d time
1058 1057
1059 type attachAttempt struct {1058 type attachAttempt struct {
1060 runner *Runner1059 runner *Runner
1061- dialer ports.Dialer1060+ dialer ports.ClientDialer
1062 connection *SessionConnection1061 connection *SessionConnection
1063 remote bool1062 remote bool
1064- transport ports.Transport1063+ transport ports.ClientConnection
1065 handshakeCtx context.Context1064 handshakeCtx context.Context
1066 handshakeTimedOut <-chan struct{}1065 handshakeTimedOut <-chan struct{}
1067 finishHandshake func()1066 finishHandshake func()
@@ -1125,17 +1124,18 @@ func DetectTrueColor(termEnv, colorTerm string) bool {
1125 return ports.DetectTrueColor(termEnv, colorTerm)1124 return ports.DetectTrueColor(termEnv, colorTerm)
1126 }1125 }
1127 1126
1128-func requestedOutputWindow(transport ports.Transport) uint8 {1127+func requestedOutputWindow(connection ports.ClientConnection) uint8 {
1129- if _, ok := transport.(ports.DatagramTransport); ok {1128+ window := connection.Capabilities().PreferredOutputWindow
1130- return 11129+ if window == 0 {
1130+ return 8
1131 }1131 }
1132- return 81132+ return window
1133 }1133 }
1134 1134
1135 func (a *attachAttempt) run(ctx context.Context) attachResult {1135 func (a *attachAttempt) run(ctx context.Context) attachResult {
1136 transport := a.transport1136 transport := a.transport
1137 if a.connection != nil {1137 if a.connection != nil {
1138- transport = a.connection.Transport()1138+ transport = a.connection.Connection()
1139 }1139 }
1140 request := a.request1140 request := a.request
1141 term := a.runner.term1141 term := a.runner.term
@@ -1244,18 +1244,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1244 Remote: remote,1244 Remote: remote,
1245 }1245 }
1246 if err := sendHandshake(func() error {1246 if err := sendHandshake(func() error {
1247- return transport.Send(wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(hello)})1247+ return transport.SendClient(hello)
1248 }); err != nil {1248 }); err != nil {
1249 return handshakeFailure("sending hello", err)1249 return handshakeFailure("sending hello", err)
1250 }1250 }
1251 ms.helloSent = true1251 ms.helloSent = true
1252 1252
1253 // 2. Await Welcome or a typed rejection.1253 // 2. Await Welcome or a typed rejection.
1254- var reply wire.Frame1254+ var reply protocol.ServerMessage
1255 if err := boundedHandshakeOperationWithTransition(handshakeCtx, transport, func() error {1255 if err := boundedHandshakeOperationWithTransition(handshakeCtx, transport, func() error {
1256- var recvErr error1256+ var receiveErr error
1257- reply, recvErr = transport.Recv()1257+ reply, receiveErr = transport.ReceiveServer()
1258- return recvErr1258+ return receiveErr
1259 }, transition); err != nil {1259 }, transition); err != nil {
1260 return handshakeFailure("awaiting welcome", err)1260 return handshakeFailure("awaiting welcome", err)
1261 }1261 }
@@ -1271,12 +1271,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1271 err: err,1271 err: err,
1272 }1272 }
1273 }1273 }
1274- switch reply.Type {1274+ switch message := reply.(type) {
1275- case wire.MsgWelcome:1275+ case protocol.Welcome:
1276- welcome, derr := wire.UnmarshalWelcome(reply.Payload)1276+ welcome := message
1277- if derr != nil {
1278- return result(false, fmt.Errorf("vev: decoding welcome: %w", derr))
1279- }
1280 resumeToken = welcome.ResumeToken1277 resumeToken = welcome.ResumeToken
1281 name = welcome.SessionName1278 name = welcome.SessionName
1282 committedIdentity = cloneCommittedIdentity(welcome.CommittedIdentity)1279 committedIdentity = cloneCommittedIdentity(welcome.CommittedIdentity)
@@ -1287,14 +1284,10 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1287 remember()1284 remember()
1288 }1285 }
1289 }1286 }
1290- case wire.MsgError:1287+ case protocol.ErrorMsg:
1291- em, derr := wire.UnmarshalErrorMsg(reply.Payload)1288+ return result(false, &ProtocolError{Code: message.Code, Text: message.Text})
1292- if derr != nil {
1293- return result(false, fmt.Errorf("vev: decoding error reply: %w", derr))
1294- }
1295- return result(false, &ProtocolError{Code: em.Code, Text: em.Text})
1296 default:1289 default:
1297- return result(false, fmt.Errorf("vev: unexpected reply type %d before welcome", reply.Type))1290+ return result(false, fmt.Errorf("vev: unexpected %T before welcome", reply))
1298 }1291 }
1299 if err := handshakeCtx.Err(); err != nil {1292 if err := handshakeCtx.Err(); err != nil {
1300 return handshakeFailure("validating welcome", err)1293 return handshakeFailure("validating welcome", err)
@@ -1320,30 +1313,20 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1320 return welcomedResult(fmt.Errorf("vev: committing route identity: %w", commitErr))1313 return welcomedResult(fmt.Errorf("vev: committing route identity: %w", commitErr))
1321 }1314 }
1322 }1315 }
1323- snapshotPayload, err := wire.MarshalRecentRouteSnapshot(a.runner.ledger.snapshot())1316+ snapshot := a.runner.ledger.snapshot()
1324- if err != nil {1317+ attention := a.runner.ledger.attentionSubscription()
1325- return welcomedResult(fmt.Errorf("vev: encoding route snapshot: %w", err))
1326- }
1327- attentionPayload, err := wire.MarshalRouteAttentionSubscription(a.runner.ledger.attentionSubscription())
1328- if err != nil {
1329- return welcomedResult(fmt.Errorf("vev: encoding route attention subscription: %w", err))
1330- }
1331 if err := sendHandshake(func() error {1318 if err := sendHandshake(func() error {
1332- if err := transport.Send(wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload}); err != nil {1319+ if err := transport.SendClient(snapshot); err != nil {
1333 return err1320 return err
1334 }1321 }
1335- return transport.Send(wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload})1322+ return transport.SendClient(attention)
1336 }); err != nil {1323 }); err != nil {
1337 return welcomedResult(fmt.Errorf("vev: publishing route snapshot: %w", err))1324 return welcomedResult(fmt.Errorf("vev: publishing route snapshot: %w", err))
1338 }1325 }
1339 }1326 }
1340 if a.runner.routeFailure != nil {1327 if a.runner.routeFailure != nil {
1341- payload, err := wire.MarshalRouteNavigationFailure(*a.runner.routeFailure)
1342- if err != nil {
1343- return welcomedResult(fmt.Errorf("vev: encoding route navigation failure: %w", err))
1344- }
1345 if err := sendHandshake(func() error {1328 if err := sendHandshake(func() error {
1346- return transport.Send(wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload})1329+ return transport.SendClient(*a.runner.routeFailure)
1347 }); err != nil {1330 }); err != nil {
1348 return welcomedResult(fmt.Errorf("vev: publishing route navigation failure: %w", err))1331 return welcomedResult(fmt.Errorf("vev: publishing route navigation failure: %w", err))
1349 }1332 }
@@ -1370,8 +1353,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1370 input.start()1353 input.start()
1371 defer input.stop()1354 defer input.stop()
1372 }1355 }
1373- sendCh := make(chan wire.Frame, sendQueueDepth)1356+ sendCh := make(chan protocol.ClientMessage, sendQueueDepth)
1374- controlCh := make(chan wire.Frame, 1)1357+ controlCh := make(chan protocol.ClientMessage, 1)
1375 barrierCh := make(chan chan struct{})1358 barrierCh := make(chan chan struct{})
1376 inputGate := newSamePeerInputGate()1359 inputGate := newSamePeerInputGate()
1377 ackQueue := newCumulativeAckQueue()1360 ackQueue := newCumulativeAckQueue()
@@ -1394,18 +1377,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1394 }1377 }
1395 }1378 }
1396 publish := func(snapshot protocol.Theme) error {1379 publish := func(snapshot protocol.Theme) error {
1397- frame := wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(snapshot)}1380+ message := protocol.ClientMessage(snapshot)
1398 // The initial cleared snapshot is sent before the sender exists. This1381 // The initial cleared snapshot is sent before the sender exists. This
1399 // keeps the generation publication ordered before the first query and1382 // keeps the generation publication ordered before the first query and
1400 // avoids leaving a queued Theme behind an immediate detach.1383 // avoids leaving a queued Theme behind an immediate detach.
1401 if !senderStarted {1384 if !senderStarted {
1402- if err := sendHandshake(func() error { return transport.Send(frame) }); err != nil {1385+ if err := sendHandshake(func() error { return transport.SendClient(message) }); err != nil {
1403 return fmt.Errorf("sending initial theme: %w", err)1386 return fmt.Errorf("sending initial theme: %w", err)
1404 }1387 }
1405 return nil1388 return nil
1406 }1389 }
1407 select {1390 select {
1408- case sendCh <- frame:1391+ case sendCh <- message:
1409 return nil1392 return nil
1410 case <-loopCtx.Done():1393 case <-loopCtx.Done():
1411 return context.Canceled1394 return context.Canceled
@@ -1504,11 +1487,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1504 // Before the asynchronous sender starts, preserve transport ordering by1487 // Before the asynchronous sender starts, preserve transport ordering by
1505 // requesting the reset synchronously after the initial Theme publication.1488 // requesting the reset synchronously after the initial Theme publication.
1506 resetErr := sendHandshake(func() error {1489 resetErr := sendHandshake(func() error {
1507- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1490+ return transport.SendClient(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})
1508- if err != nil {
1509- return err
1510- }
1511- return transport.Send(wire.Frame{Type: wire.MsgResize, Payload: payload})
1512 })1491 })
1513 if resetErr != nil {1492 if resetErr != nil {
1514 resetErr = handshakeContextError(ctx, handshakeTimedOut, resetErr)1493 resetErr = handshakeContextError(ctx, handshakeTimedOut, resetErr)
@@ -1630,12 +1609,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1630 return fmt.Errorf("reading terminal geometry for parked route: %w", err)1609 return fmt.Errorf("reading terminal geometry for parked route: %w", err)
1631 }1610 }
1632 geometry = geometry.NormalizePixels()1611 geometry = geometry.NormalizePixels()
1633- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1612+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1634- if err != nil {1613+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1635- return fmt.Errorf("encoding terminal size for parked route: %w", err)1614+ return fmt.Errorf("validating terminal size for parked route: %w", err)
1636 }1615 }
1637 select {1616 select {
1638- case controlCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:1617+ case controlCh <- message:
1639 return nil1618 return nil
1640 case <-loopCtx.Done():1619 case <-loopCtx.Done():
1641 return context.Canceled1620 return context.Canceled
@@ -1647,12 +1626,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1647 }1626 }
1648 nextParkedRequestID++1627 nextParkedRequestID++
1649 request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}1628 request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}
1650- payload := wire.MarshalParkedRouteRequest(request)1629+ if protocol.ValidateParkedRouteRequest(request) != nil {
1651- if payload == nil {
1652 return errors.New("vev: invalid parked-route request")1630 return errors.New("vev: invalid parked-route request")
1653 }1631 }
1654 select {1632 select {
1655- case controlCh <- wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: payload}:1633+ case controlCh <- request:
1656 parkedPending = &parkedRoutePending{1634 parkedPending = &parkedRoutePending{
1657 requestID: request.RequestID, action: action, leaseID: leaseID,1635 requestID: request.RequestID, action: action, leaseID: leaseID,
1658 timer: clk.NewTimer(protocol.HandshakeTimeout),1636 timer: clk.NewTimer(protocol.HandshakeTimeout),
@@ -1727,12 +1705,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1727 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)1705 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)
1728 }1706 }
1729 geometry = geometry.NormalizePixels()1707 geometry = geometry.NormalizePixels()
1730- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1708+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1731- if err != nil {1709+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1732- return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err)1710+ return fmt.Errorf("validating terminal size for reconnect reconciliation: %w", err)
1733 }1711 }
1734 select {1712 select {
1735- case sendCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:1713+ case sendCh <- message:
1736 awaitingReconnectReset = true1714 awaitingReconnectReset = true
1737 return nil1715 return nil
1738 case <-loopCtx.Done():1716 case <-loopCtx.Done():
@@ -1743,20 +1721,13 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1743 if a.runner.ledger == nil {1721 if a.runner.ledger == nil {
1744 return errors.New("vev: route ledger unavailable")1722 return errors.New("vev: route ledger unavailable")
1745 }1723 }
1746- snapshotPayload, err := wire.MarshalRecentRouteSnapshot(a.runner.ledger.snapshot())1724+ messages := []protocol.ClientMessage{
1747- if err != nil {1725+ a.runner.ledger.snapshot(),
1748- return err1726+ a.runner.ledger.attentionSubscription(),
1749- }
1750- attentionPayload, err := wire.MarshalRouteAttentionSubscription(a.runner.ledger.attentionSubscription())
1751- if err != nil {
1752- return err
1753 }1727 }
1754- for _, frame := range []wire.Frame{1728+ for _, message := range messages {
1755- {Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload},
1756- {Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload},
1757- } {
1758 select {1729 select {
1759- case sendCh <- frame:1730+ case sendCh <- message:
1760 case <-loopCtx.Done():1731 case <-loopCtx.Done():
1761 return context.Canceled1732 return context.Canceled
1762 }1733 }
@@ -1764,16 +1735,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1764 return nil1735 return nil
1765 }1736 }
1766 sendRouteFailure := func(action protocol.RouteNavigationAction, code protocol.RouteFailureCode) error {1737 sendRouteFailure := func(action protocol.RouteNavigationAction, code protocol.RouteFailureCode) error {
1767- payload, err := wire.MarshalRouteNavigationFailure(protocol.RouteNavigationFailure{1738+ failure := protocol.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: code}
1768- Key: action.Key,1739+ if failure.Code.Validate() != nil || failure.Key == 0 || failure.Generation == 0 {
1769- Generation: action.Generation,1740+ return protocol.ErrInvalidRouteWire
1770- Code: code,
1771- })
1772- if err != nil {
1773- return err
1774 }1741 }
1775 select {1742 select {
1776- case sendCh <- wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}:1743+ case sendCh <- failure:
1777 return nil1744 return nil
1778 case <-loopCtx.Done():1745 case <-loopCtx.Done():
1779 return context.Canceled1746 return context.Canceled
@@ -1828,7 +1795,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1828 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))1795 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))
1829 }1796 }
1830 select {1797 select {
1831- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}:1798+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}:
1832 case <-loopCtx.Done():1799 case <-loopCtx.Done():
1833 return welcomedResult(nil)1800 return welcomedResult(nil)
1834 }1801 }
@@ -1837,7 +1804,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1837 if ev.State == ports.LinkStateDegraded {1804 if ev.State == ports.LinkStateDegraded {
1838 log.Warn("UDP link degraded")1805 log.Warn("UDP link degraded")
1839 select {1806 select {
1840- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})}:1807+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded}:
1841 case <-loopCtx.Done():1808 case <-loopCtx.Done():
1842 return welcomedResult(nil)1809 return welcomedResult(nil)
1843 }1810 }
@@ -1884,12 +1851,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1884 }1851 }
1885 return welcomedResult(fmt.Errorf("vev: receiving from daemon: %w", r.err))1852 return welcomedResult(fmt.Errorf("vev: receiving from daemon: %w", r.err))
1886 }1853 }
1887- switch r.frame.Type {1854+ switch message := r.message.(type) {
1888- case wire.MsgOutput:1855+ case protocol.Output:
1889- o, derr := wire.UnmarshalOutput(r.frame.Payload)1856+ o := message
1890- if derr != nil {
1891- return welcomedResult(fmt.Errorf("vev: decoding output: %w", derr))
1892- }
1893 nextState, accepted := outputState.next(o)1857 nextState, accepted := outputState.next(o)
1894 if !accepted {1858 if !accepted {
1895 // A discarded frame was never written and must never be ACKed.1859 // A discarded frame was never written and must never be ACKed.
@@ -1897,7 +1861,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1897 // repeated gaps while that reset is in flight.1861 // repeated gaps while that reset is in flight.
1898 if !outputResetRequested {1862 if !outputResetRequested {
1899 select {1863 select {
1900- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:1864+ case sendCh <- protocol.OutputResetRequest{}:
1901 outputResetRequested = true1865 outputResetRequested = true
1902 case <-loopCtx.Done():1866 case <-loopCtx.Done():
1903 return loopCanceledResult()1867 return loopCanceledResult()
@@ -1947,11 +1911,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1947 log.Debug("received first output")1911 log.Debug("received first output")
1948 endHandshake()1912 endHandshake()
1949 }1913 }
1950- case wire.MsgAttachTarget:1914+ case protocol.AttachTarget:
1951- target, derr := wire.UnmarshalAttachTarget(r.frame.Payload)1915+ target := message
1952- if derr != nil {
1953- return welcomedResult(fmt.Errorf("vev: decoding attach target: %w", derr))
1954- }
1955 if a.returnAttachTargets {1916 if a.returnAttachTargets {
1956 handoff := welcomedResult(nil)1917 handoff := welcomedResult(nil)
1957 handoff.target = &target1918 handoff.target = &target
@@ -1967,18 +1928,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1967 }1928 }
1968 nextSamePeerRequestID++1929 nextSamePeerRequestID++
1969 request := a.runner.ledger.samePeerHandoff(request, target)1930 request := a.runner.ledger.samePeerHandoff(request, target)
1970- payload, err := wire.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{1931+ switchRequest := protocol.SamePeerSwitchRequest{
1971 RequestID: nextSamePeerRequestID,1932 RequestID: nextSamePeerRequestID,
1972 Target: *target.ExactTarget,1933 Target: *target.ExactTarget,
1973 PreferredTabID: request.PreferredTabID,1934 PreferredTabID: request.PreferredTabID,
1974- })1935+ }
1975- if err != nil {1936+ if switchRequest.Validate() != nil {
1976- return welcomedResult(fmt.Errorf("vev: encoding same-peer switch: %w", err))1937+ return welcomedResult(errors.New("vev: invalid same-peer switch"))
1977 }1938 }
1978 samePeerSwitch = &samePeerSwitchPending{requestID: nextSamePeerRequestID, target: *target.ExactTarget}1939 samePeerSwitch = &samePeerSwitchPending{requestID: nextSamePeerRequestID, target: *target.ExactTarget}
1979 inputGate.setPaused(true)1940 inputGate.setPaused(true)
1980 select {1941 select {
1981- case controlCh <- wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: payload}:1942+ case controlCh <- switchRequest:
1982 continue1943 continue
1983 case <-loopCtx.Done():1944 case <-loopCtx.Done():
1984 return loopCanceledResult()1945 return loopCanceledResult()
@@ -1987,11 +1948,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1987 handoff := welcomedResult(nil)1948 handoff := welcomedResult(nil)
1988 handoff.target = &target1949 handoff.target = &target
1989 return handoff1950 return handoff
1990- case wire.MsgParkedRouteResponse:1951+ case protocol.ParkedRouteResponse:
1991- response, derr := wire.UnmarshalParkedRouteResponse(r.frame.Payload)1952+ response := message
1992- if derr != nil {
1993- return welcomedResult(fmt.Errorf("vev: decoding parked-route response: %w", derr))
1994- }
1995 if parkedPending == nil || response.RequestID != parkedPending.requestID {1953 if parkedPending == nil || response.RequestID != parkedPending.requestID {
1996 return welcomedResult(errors.New("vev: unexpected parked-route response"))1954 return welcomedResult(errors.New("vev: unexpected parked-route response"))
1997 }1955 }
@@ -2034,12 +1992,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2034 return fallback1992 return fallback
2035 }1993 }
2036 }1994 }
2037- case wire.MsgNavigationAction:1995+ case protocol.NavigationDirective:
2038- directive, derr := wire.UnmarshalNavigationDirective(r.frame.Payload)1996+ directive := message
2039- if derr != nil {1997+ datagramRoute := transport.Capabilities().PreferredOutputWindow == 1
2040- return welcomedResult(fmt.Errorf("vev: decoding navigation action: %w", derr))
2041- }
2042- _, datagramRoute := transport.(ports.DatagramTransport)
2043 if directive.Action == protocol.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {1998 if directive.Action == protocol.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {
2044 stopForeground()1999 stopForeground()
2045 if err := flushSender(); err != nil {2000 if err := flushSender(); err != nil {
@@ -2053,11 +2008,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2053 navigation := welcomedResult(nil)2008 navigation := welcomedResult(nil)
2054 navigation.action = directive.Action2009 navigation.action = directive.Action
2055 return navigation2010 return navigation
2056- case wire.MsgNavigateRecentRoute:2011+ case protocol.RouteNavigationAction:
2057- action, derr := wire.UnmarshalRouteNavigationAction(r.frame.Payload)2012+ action := message
2058- if derr != nil {
2059- return welcomedResult(fmt.Errorf("vev: decoding route navigation action: %w", derr))
2060- }
2061 if a.runner.ledger == nil {2013 if a.runner.ledger == nil {
2062 if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {2014 if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {
2063 return welcomedResult(ferr)2015 return welcomedResult(ferr)
@@ -2077,11 +2029,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2077 navigation := welcomedResult(nil)2029 navigation := welcomedResult(nil)
2078 navigation.routeAction = &action2030 navigation.routeAction = &action
2079 return navigation2031 return navigation
2080- case wire.MsgCommittedRouteIdentity:2032+ case protocol.CommittedRouteIdentity:
2081- identity, derr := wire.UnmarshalCommittedRouteIdentity(r.frame.Payload)2033+ identity := message
2082- if derr != nil {
2083- return welcomedResult(fmt.Errorf("vev: decoding committed route identity: %w", derr))
2084- }
2085 if samePeerSwitch != nil && identity.Target != samePeerSwitch.target {2034 if samePeerSwitch != nil && identity.Target != samePeerSwitch.target {
2086 return welcomedResult(errors.New("vev: same-peer switch committed an unexpected target"))2035 return welcomedResult(errors.New("vev: same-peer switch committed an unexpected target"))
2087 }2036 }
@@ -2100,11 +2049,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2100 samePeerSwitch = nil2049 samePeerSwitch = nil
2101 inputGate.setPaused(false)2050 inputGate.setPaused(false)
2102 }2051 }
2103- case wire.MsgSamePeerSwitchFailure:2052+ case protocol.SamePeerSwitchFailure:
2104- failure, derr := wire.UnmarshalSamePeerSwitchFailure(r.frame.Payload)2053+ failure := message
2105- if derr != nil {
2106- return welcomedResult(fmt.Errorf("vev: decoding same-peer switch failure: %w", derr))
2107- }
2108 if samePeerSwitch != nil && failure.RequestID == samePeerSwitch.requestID {2054 if samePeerSwitch != nil && failure.RequestID == samePeerSwitch.requestID {
2109 samePeerSwitch = nil2055 samePeerSwitch = nil
2110 inputGate.setPaused(false)2056 inputGate.setPaused(false)
@@ -2112,7 +2058,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2112 transitionWaitingFull = true2058 transitionWaitingFull = true
2113 if !outputResetRequested {2059 if !outputResetRequested {
2114 select {2060 select {
2115- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:2061+ case sendCh <- protocol.OutputResetRequest{}:
2116 outputResetRequested = true2062 outputResetRequested = true
2117 case <-loopCtx.Done():2063 case <-loopCtx.Done():
2118 return loopCanceledResult()2064 return loopCanceledResult()
@@ -2122,11 +2068,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2122 transition.stop()2068 transition.stop()
2123 }2069 }
2124 }2070 }
2125- case wire.MsgRoutePosition:2071+ case protocol.RoutePosition:
2126- position, derr := wire.UnmarshalRoutePosition(r.frame.Payload)2072+ position := message
2127- if derr != nil {
2128- return welcomedResult(fmt.Errorf("vev: decoding route position: %w", derr))
2129- }
2130 if a.runner.ledger == nil {2073 if a.runner.ledger == nil {
2131 return welcomedResult(errors.New("vev: route ledger unavailable"))2074 return welcomedResult(errors.New("vev: route ledger unavailable"))
2132 }2075 }
@@ -2134,17 +2077,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2134 return welcomedResult(fmt.Errorf("vev: remembering route position: %w", derr))2077 return welcomedResult(fmt.Errorf("vev: remembering route position: %w", derr))
2135 }2078 }
2136 routePosition = cloneRoutePosition(&position)2079 routePosition = cloneRoutePosition(&position)
2137- case wire.MsgRouteNavigationFailure:2080+ case protocol.RouteNavigationFailure:
2138- failure, derr := wire.UnmarshalRouteNavigationFailure(r.frame.Payload)2081+ failure := message
2139- if derr != nil {
2140- return welcomedResult(fmt.Errorf("vev: decoding route navigation failure: %w", derr))
2141- }
2142 log.Warn("route navigation rejected", "key", failure.Key, "generation", failure.Generation, "code", failure.Code)2082 log.Warn("route navigation rejected", "key", failure.Key, "generation", failure.Generation, "code", failure.Code)
2143- case wire.MsgDetached:2083+ case protocol.Detached:
2144- d, derr := wire.UnmarshalDetached(r.frame.Payload)2084+ d := message
2145- if derr != nil {
2146- return welcomedResult(fmt.Errorf("vev: decoding detached: %w", derr))
2147- }
2148 ms.detached = true2085 ms.detached = true
2149 if d.Reason == protocol.ReasonDetach {2086 if d.Reason == protocol.ReasonDetach {
2150 log.Info("detached cleanly", "reason", d.Reason)2087 log.Info("detached cleanly", "reason", d.Reason)
@@ -2152,34 +2089,36 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2152 log.Warn("detached by daemon", "reason", d.Reason)2089 log.Warn("detached by daemon", "reason", d.Reason)
2153 }2090 }
2154 return welcomedResult(detachedResult(d.Reason))2091 return welcomedResult(detachedResult(d.Reason))
2155- case wire.MsgPong:2092+ case protocol.Pong:
2156 // Liveness reply; nothing to do.2093 // Liveness reply; nothing to do.
2157- default:
2158- // Unknown/out-of-band message types are ignored so a newer
2159- // daemon can add server->client messages without breaking us.
2160 }2094 }
2161 }2095 }
2162 }2096 }
2163 }2097 }
2164 2098
2165-// recvResult carries one framed message (or a read error) from the receive2099+// recvResult carries one typed message (or a read error) from the receive pump.
2166-// pump to the main loop.
2167 type recvResult struct {2100 type recvResult struct {
2168- frame wire.Frame2101+ message protocol.ServerMessage
2169- err error2102+ err error
2170 }2103 }
2171 2104
2172-// runRecv reads frames until an error, forwarding each to the main loop.2105+// runRecv reads typed messages until an error, forwarding each to the main loop.
2173 // It exits on the first error or when the loop context is cancelled.2106 // It exits on the first error or when the loop context is cancelled.
2174-func runRecv(ctx context.Context, transport ports.Transport, out chan<- recvResult, failed chan<- struct{}, log *slog.Logger) {2107+func runRecv(ctx context.Context, transport ports.ClientConnection, out chan<- recvResult, failed chan<- struct{}, log *slog.Logger) {
2175 defer log.Debug("receive pump exited")2108 defer log.Debug("receive pump exited")
2176 for {2109 for {
2177- f, err := transport.Recv()2110+ message, err := transport.ReceiveServer()
2111+ if err != nil {
2112+ var failure *protocol.DecodeFailure
2113+ if errors.As(err, &failure) && (failure.Category == protocol.DecodeUnknownType || failure.Category == protocol.DecodeWrongDirection) {
2114+ continue
2115+ }
2116+ }
2178 if err != nil && failed != nil {2117 if err != nil && failed != nil {
2179 close(failed)2118 close(failed)
2180 }2119 }
2181 select {2120 select {
2182- case out <- recvResult{frame: f, err: err}:2121+ case out <- recvResult{message: message, err: err}:
2183 case <-ctx.Done():2122 case <-ctx.Done():
2184 return2123 return
2185 }2124 }
@@ -2189,9 +2128,9 @@ func runRecv(ctx context.Context, transport ports.Transport, out chan<- recvResu
2189 }2128 }
2190 }2129 }
2191 2130
2192-// runSender is the sole caller of transport.Send: serialising the input and2131+// runSender is the sole caller of SendClient: serialising the input and resize
2193-// resize pumps through one goroutine keeps the transport's single-writer2132+// pumps through one goroutine keeps the connection's single-writer contract.
2194-// contract intact. A send failure cancels the loop and is surfaced once.2133+// A send failure cancels the loop and is surfaced once.
2195 type cumulativeAckQueue struct {2134 type cumulativeAckQueue struct {
2196 mu sync.Mutex2135 mu sync.Mutex
2197 latestEpoch uint642136 latestEpoch uint64
@@ -2228,10 +2167,10 @@ func (q *cumulativeAckQueue) take() (uint64, uint64) {
2228 2167
2229 // runSender preserves normal-frame order while allowing switch control and2168 // runSender preserves normal-frame order while allowing switch control and
2230 // output ACKs to make progress when raw input is held for a same-peer switch.2169 // output ACKs to make progress when raw input is held for a same-peer switch.
2231-func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.Transport, control <-chan wire.Frame, barriers <-chan chan struct{}, in <-chan wire.Frame, inputGate *samePeerInputGate, acks *cumulativeAckQueue, errCh chan<- error, log *slog.Logger) {2170+func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.ClientConnection, control <-chan protocol.ClientMessage, barriers <-chan chan struct{}, in <-chan protocol.ClientMessage, inputGate *samePeerInputGate, acks *cumulativeAckQueue, errCh chan<- error, log *slog.Logger) {
2232 defer log.Debug("sender pump exited")2171 defer log.Debug("sender pump exited")
2233- send := func(f wire.Frame) bool {2172+ send := func(message protocol.ClientMessage) bool {
2234- if err := transport.Send(f); err != nil {2173+ if err := transport.SendClient(message); err != nil {
2235 select {2174 select {
2236 case errCh <- err:2175 case errCh <- err:
2237 default:2176 default:
@@ -2246,16 +2185,7 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
2246 if state == 0 {2185 if state == 0 {
2247 return true2186 return true
2248 }2187 }
2249- payload, err := wire.MarshalAck(protocol.Ack{Epoch: epoch, State: state})2188+ return send(protocol.Ack{Epoch: epoch, State: state})
2250- if err != nil {
2251- select {
2252- case errCh <- fmt.Errorf("encoding output ACK: %w", err):
2253- default:
2254- }
2255- cancel()
2256- return false
2257- }
2258- return send(wire.Frame{Type: wire.MsgAck, Payload: payload})
2259 }2189 }
2260 flushPending := func() bool {2190 flushPending := func() bool {
2261 for {2191 for {
@@ -2287,12 +2217,12 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
2287 close(done)2217 close(done)
2288 return true2218 return true
2289 }2219 }
2290- var heldInput *wire.Frame2220+ var heldInput protocol.ClientMessage
2291 for {2221 for {
2292 if heldInput != nil {2222 if heldInput != nil {
2293 paused, changed := inputGate.snapshot()2223 paused, changed := inputGate.snapshot()
2294 if !paused {2224 if !paused {
2295- if !send(*heldInput) {2225+ if !send(heldInput) {
2296 return2226 return
2297 }2227 }
2298 heldInput = nil2228 heldInput = nil
@@ -2348,23 +2278,23 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
2348 if !completeBarrier(done) {2278 if !completeBarrier(done) {
2349 return2279 return
2350 }2280 }
2351- case frame := <-in:2281+ case message := <-in:
2352 select {2282 select {
2353 case <-ctx.Done():2283 case <-ctx.Done():
2354 return2284 return
2355 default:2285 default:
2356 }2286 }
2357- if frame.Type == wire.MsgInput || frame.Type == wire.MsgImagePush {2287+ switch message.(type) {
2288+ case protocol.Input, protocol.ImagePush:
2358 if paused, _ := inputGate.snapshot(); paused {2289 if paused, _ := inputGate.snapshot(); paused {
2359- held := frame2290+ heldInput = message
2360- heldInput = &held
2361 if inputGate != nil && inputGate.afterInputHeld != nil {2291 if inputGate != nil && inputGate.afterInputHeld != nil {
2362 inputGate.afterInputHeld()2292 inputGate.afterInputHeld()
2363 }2293 }
2364 continue2294 continue
2365 }2295 }
2366 }2296 }
2367- if !send(frame) {2297+ if !send(message) {
2368 return2298 return
2369 }2299 }
2370 case <-ctx.Done():2300 case <-ctx.Done():
@@ -2726,7 +2656,7 @@ type stdinPump struct {
2726 in io.Reader // compatibility for isolated scanner tests2656 in io.Reader // compatibility for isolated scanner tests
2727 input *terminalInputPump2657 input *terminalInputPump
2728 consumer uint642658 consumer uint64
2729- out chan<- wire.Frame2659+ out chan<- protocol.ClientMessage
2730 clock ports.Clock2660 clock ports.Clock
2731 clipboard ports.ClipboardReader2661 clipboard ports.ClipboardReader
2732 logger *slog.Logger2662 logger *slog.Logger
@@ -2747,13 +2677,13 @@ func (p *stdinPump) run() {
2747 var undeliveredMu sync.Mutex2677 var undeliveredMu sync.Mutex
2748 var undelivered []byte2678 var undelivered []byte
2749 sendOK.Store(true)2679 sendOK.Store(true)
2750- send := func(frame wire.Frame) bool {2680+ send := func(message protocol.ClientMessage) bool {
2751 if !sendOK.Load() {2681 if !sendOK.Load() {
2752 return false2682 return false
2753 }2683 }
2754 return p.sendLease.send(func() bool {2684 return p.sendLease.send(func() bool {
2755 select {2685 select {
2756- case p.out <- frame:2686+ case p.out <- message:
2757 if p.afterInputDelivered != nil {2687 if p.afterInputDelivered != nil {
2758 p.afterInputDelivered()2688 p.afterInputDelivered()
2759 }2689 }
@@ -2786,7 +2716,7 @@ func (p *stdinPump) run() {
2786 return2716 return
2787 }2717 }
2788 inputSeq++2718 inputSeq++
2789- if !send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {2719+ if !send(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)}) {
2790 undeliveredMu.Lock()2720 undeliveredMu.Lock()
2791 undelivered = append(undelivered, data...)2721 undelivered = append(undelivered, data...)
2792 undeliveredMu.Unlock()2722 undeliveredMu.Unlock()
@@ -2800,11 +2730,11 @@ func (p *stdinPump) run() {
2800 reader: p.clipboard,2730 reader: p.clipboard,
2801 log: p.logger,2731 log: p.logger,
2802 sendNotice: func(action uint8) {2732 sendNotice: func(action uint8) {
2803- send(wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: action})})2733+ send(protocol.ClientNotice{Action: action})
2804 },2734 },
2805 sendImage: func(mime string, data []byte) {2735 sendImage: func(mime string, data []byte) {
2806 inputSeq++2736 inputSeq++
2807- send(wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})2737+ send(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})
2808 },2738 },
2809 next: coalescer.Scan,2739 next: coalescer.Scan,
2810 }2740 }
@@ -2946,7 +2876,7 @@ func (p *stdinPump) run() {
2946 disarmMarkerDeadline()2876 disarmMarkerDeadline()
2947 markers.flush(sink)2877 markers.flush(sink)
2948 select {2878 select {
2949- case p.out <- wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(protocol.Detach{})}:2879+ case p.out <- protocol.Detach{}:
2950 case <-p.ctx.Done():2880 case <-p.ctx.Done():
2951 }2881 }
2952 p.cancel()2882 p.cancel()
@@ -2967,7 +2897,7 @@ func (p *stdinPump) run() {
2967 // runResize forwards coalesced terminal resize events to the daemon. It2897 // runResize forwards coalesced terminal resize events to the daemon. It
2968 // tolerates an already-closed resize channel (which the terminal adapter2898 // tolerates an already-closed resize channel (which the terminal adapter
2969 // hands back when restore ran before ResizeEvents was first called).2899 // hands back when restore ran before ResizeEvents was first called).
2970-func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- wire.Frame, sendLease *foregroundSendLease, log *slog.Logger) {2900+func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- protocol.ClientMessage, sendLease *foregroundSendLease, log *slog.Logger) {
2971 defer log.Debug("resize pump exited")2901 defer log.Debug("resize pump exited")
2972 for {2902 for {
2973 select {2903 select {
@@ -2976,15 +2906,14 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- wi
2976 return2906 return
2977 }2907 }
2978 geometry = geometry.NormalizePixels()2908 geometry = geometry.NormalizePixels()
2979- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})2909+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
2980- if err != nil {2910+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
2981- log.Error("encoding terminal resize", "error", err)2911+ log.Error("validating terminal resize", "error", err)
2982 continue2912 continue
2983 }2913 }
2984- frame := wire.Frame{Type: wire.MsgResize, Payload: payload}
2985 if !sendLease.send(func() bool {2914 if !sendLease.send(func() bool {
2986 select {2915 select {
2987- case out <- frame:2916+ case out <- message:
2988 return true2917 return true
2989 case <-ctx.Done():2918 case <-ctx.Done():
2990 return false2919 return false
M internal/usecase/client/client_test.go +38 -38
@@ -130,7 +130,7 @@ func (markedDatagramTransport) DatagramTransport() {}
130130 // scriptRecv wires a MockTransport's Recv to yield the given items in order,
131131 // then block forever (simulating a live but idle connection). It returns a
132132 // cleanup that unblocks the parked reader.
133-func scriptRecv(tr *portsmocks.MockTransport, items ...recvItem) func() {
133+func scriptRecv(tr *mockClientConnection, items ...recvItem) func() {
134134 ch := make(chan recvItem, len(items))
135135 for _, it := range items {
136136 ch <- it
@@ -242,15 +242,15 @@ func isType(typ wire.MsgType) any {
242242 // transportDialer adapts one already-open test transport to the Runner API.
243243 type transportDialer struct{ transport ports.Transport }
244244
245-func (d transportDialer) Dial(context.Context) (ports.Transport, error) {
246- return d.transport, nil
245+func (d transportDialer) Dial(context.Context) (ports.ClientConnection, error) {
246+ return &rawClientConnection{raw: d.transport}, nil
247247 }
248248
249249 func runTestClient(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error {
250250 return client.NewRunner(deps).Run(ctx, request)
251251 }
252252
253-func testDependencies(dialer ports.Dialer, terminal ports.Terminal, clock ports.Clock, clipboard ports.ClipboardReader, observer ports.SerializedRuntimeObserver) client.Dependencies {
253+func testDependencies(dialer ports.ClientDialer, terminal ports.Terminal, clock ports.Clock, clipboard ports.ClipboardReader, observer ports.SerializedRuntimeObserver) client.Dependencies {
254254 return client.Dependencies{
255255 Dialer: dialer,
256256 Terminal: terminal,
@@ -286,7 +286,7 @@ func newHandshakeClock(t *testing.T, timerCount int) (*portsmocks.MockClock, <-c
286286 func TestRunBoundsBlockedDial(t *testing.T) {
287287 clk, createdTimers := newHandshakeClock(t, 1)
288288 started := make(chan struct{})
289- dialer := portsmocks.NewMockDialer(t)
289+ dialer := newMockClientDialer(t)
290290 dialer.EXPECT().Dial(mock.Anything).RunAndReturn(func(ctx context.Context) (ports.Transport, error) {
291291 close(started)
292292 <-ctx.Done()
@@ -330,7 +330,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
330330 closed := make(chan struct{})
331331 var closeOnce sync.Once
332332 operationExited := make(chan struct{})
333- tr := portsmocks.NewMockTransport(t)
333+ tr := newMockClientConnection(t)
334334 tr.EXPECT().Close().Run(func() { closeOnce.Do(func() { close(closed) }) }).Return(nil).Maybe()
335335
336336 block := func() error {
@@ -349,7 +349,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
349349 return wire.Frame{}, block()
350350 }).Once()
351351 }
352- dialer := portsmocks.NewMockDialer(t)
352+ dialer := newMockClientDialer(t)
353353 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
354354
355355 errCh := make(chan error, 1)
@@ -401,7 +401,7 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) {
401401 defer in.unblock()
402402
403403 gotHello := make(chan protocol.Hello, 1)
404- tr := portsmocks.NewMockTransport(t)
404+ tr := newMockClientConnection(t)
405405 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
406406 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
407407 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {
@@ -616,7 +616,7 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
616616 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2}}
617617 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}
618618 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
619- deps.AttachHandoff = func(protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
619+ deps.AttachHandoff = func(protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
620620 target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}
621621 return remoteDialer, client.AttachRequest{
622622 Intent: protocol.IntentAttach, SessionName: "work", Remote: true,
@@ -650,9 +650,9 @@ func hybridLocalBootstrap(lifecycle domain.SessionLifecycleID, target domain.Rem
650650 }}
651651 }
652652
653-func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clock ports.Clock, remoteByEndpoint map[string]ports.Dialer) client.Dependencies {
653+func hybridPickerDependencies(localDialer ports.ClientDialer, term ports.Terminal, clock ports.Clock, remoteByEndpoint map[string]ports.ClientDialer) client.Dependencies {
654654 deps := testDependencies(localDialer, term, clock, nil, nil)
655- deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
655+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
656656 if target.RemoteTarget == nil {
657657 return nil, client.AttachRequest{}, errors.New("hybrid picker target is not remote")
658658 }
@@ -743,7 +743,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
743743 }
744744 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
745745 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
746- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
746+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
747747
748748 err := runTestClient(context.Background(), deps, client.AttachRequest{
749749 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
@@ -826,7 +826,7 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
826826 remoteDialer := &sequenceDialer{trs: []ports.Transport{
827827 markedDatagramTransport{Transport: sourceRemote}, targetRemote,
828828 }}
829- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
829+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
830830
831831 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
832832 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
@@ -890,7 +890,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
890890
891891 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial}}
892892 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
893- deps := hybridPickerDependencies(localDialer, term, clock, map[string]ports.Dialer{"remote": remoteDialer})
893+ deps := hybridPickerDependencies(localDialer, term, clock, map[string]ports.ClientDialer{"remote": remoteDialer})
894894
895895 ctx, cancel := context.WithCancel(context.Background())
896896 defer cancel()
@@ -967,7 +967,7 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
967967 }}
968968 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
969969 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
970- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})
970+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
971971
972972 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
973973 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
@@ -1023,7 +1023,7 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
10231023 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
10241024 sourceDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: sourceRemote}}}
10251025 targetDialer := &sequenceDialer{trs: []ports.Transport{targetRemote}}
1026- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{
1026+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{
10271027 sourceTarget.Endpoint: sourceDialer,
10281028 targetTarget.Endpoint: targetDialer,
10291029 })
@@ -1109,7 +1109,7 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
11091109 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2, remote3}}
11101110
11111111 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
1112- deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {
1112+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
11131113 require.Equal(t, remoteHandoff, target)
11141114 return remoteDialer, client.AttachRequest{
11151115 Intent: protocol.IntentAttach, SessionName: target.Session, Remote: true,
@@ -1170,7 +1170,7 @@ func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.
11701170 defer in.unblock()
11711171
11721172 target := protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}
1173- tr := portsmocks.NewMockTransport(t)
1173+ tr := newMockClientConnection(t)
11741174 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
11751175 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
11761176 unblock := scriptRecv(tr,
@@ -1197,7 +1197,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) {
11971197 defer in.unblock()
11981198
11991199 gotHello := make(chan protocol.Hello, 1)
1200- tr := portsmocks.NewMockTransport(t)
1200+ tr := newMockClientConnection(t)
12011201 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
12021202 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
12031203 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {
@@ -1226,7 +1226,7 @@ func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) {
12261226 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
12271227 defer in.unblock()
12281228
1229- tr := portsmocks.NewMockTransport(t)
1229+ tr := newMockClientConnection(t)
12301230 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
12311231 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
12321232 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {
@@ -1252,7 +1252,7 @@ func TestAttachHappyPath(t *testing.T) {
12521252 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
12531253 defer in.unblock()
12541254
1255- tr := portsmocks.NewMockTransport(t)
1255+ tr := newMockClientConnection(t)
12561256 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
12571257 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
12581258 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1275,7 +1275,7 @@ func TestAttachVersionMismatch(t *testing.T) {
12751275 tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once()
12761276 // EnterRaw must NOT be called on the error-before-welcome path.
12771277
1278- tr := portsmocks.NewMockTransport(t)
1278+ tr := newMockClientConnection(t)
12791279 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
12801280 tr.EXPECT().Recv().Return(
12811281 frameOf(wire.MsgError, wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
@@ -1298,7 +1298,7 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
12981298 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
12991299 defer in.unblock()
13001300
1301- tr := portsmocks.NewMockTransport(t)
1301+ tr := newMockClientConnection(t)
13021302 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
13031303 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
13041304 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1322,7 +1322,7 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) {
13221322 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
13231323 defer in.unblock()
13241324
1325- tr := portsmocks.NewMockTransport(t)
1325+ tr := newMockClientConnection(t)
13261326 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
13271327 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
13281328 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1356,7 +1356,7 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) {
13561356
13571357 gotInput := make(chan []byte, 2)
13581358 allowDetach := make(chan struct{})
1359- tr := portsmocks.NewMockTransport(t)
1359+ tr := newMockClientConnection(t)
13601360 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
13611361 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
13621362 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1418,7 +1418,7 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) {
14181418
14191419 gotInput := make(chan []byte, 1)
14201420 allowDetach := make(chan struct{})
1421- tr := portsmocks.NewMockTransport(t)
1421+ tr := newMockClientConnection(t)
14221422 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
14231423 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
14241424 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1473,7 +1473,7 @@ func TestAttachForwardsResize(t *testing.T) {
14731473 firstRecv := make(chan struct{})
14741474 var firstRecvOnce sync.Once
14751475
1476- tr := portsmocks.NewMockTransport(t)
1476+ tr := newMockClientConnection(t)
14771477 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
14781478 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
14791479 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1534,7 +1534,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
15341534 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
15351535 defer in.unblock()
15361536
1537- tr := portsmocks.NewMockTransport(t)
1537+ tr := newMockClientConnection(t)
15381538 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
15391539 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
15401540 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1545,7 +1545,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
15451545 )
15461546 defer unblock()
15471547 tr.EXPECT().Close().Return(nil).Once()
1548- d := portsmocks.NewMockDialer(t)
1548+ d := newMockClientDialer(t)
15491549 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
15501550
15511551 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: false})
@@ -1558,14 +1558,14 @@ func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) {
15581558 tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once()
15591559 // EnterRaw must NOT be called when the daemon rejects Hello before Welcome.
15601560
1561- tr := portsmocks.NewMockTransport(t)
1561+ tr := newMockClientConnection(t)
15621562 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
15631563 tr.EXPECT().Recv().Return(
15641564 frameOf(wire.MsgError, wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
15651565 nil,
15661566 ).Once()
15671567 tr.EXPECT().Close().Return(nil).Once()
1568- d := portsmocks.NewMockDialer(t)
1568+ d := newMockClientDialer(t)
15691569 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
15701570
15711571 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
@@ -1577,7 +1577,7 @@ func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) {
15771577
15781578 func TestRunPhaseASingleAttempt(t *testing.T) {
15791579 dialErr := errors.New("dial failed")
1580- d := portsmocks.NewMockDialer(t)
1580+ d := newMockClientDialer(t)
15811581 d.EXPECT().Dial(mock.Anything).Return(nil, dialErr).Once()
15821582 tm := portsmocks.NewMockTerminal(t)
15831583
@@ -1591,7 +1591,7 @@ type sequenceDialer struct {
15911591 calls atomic.Int32
15921592 }
15931593
1594-func (d *sequenceDialer) Dial(context.Context) (ports.Transport, error) {
1594+func (d *sequenceDialer) Dial(context.Context) (ports.ClientConnection, error) {
15951595 i := int(d.calls.Add(1)) - 1
15961596 if i < len(d.errs) && d.errs[i] != nil {
15971597 return nil, d.errs[i]
@@ -1599,7 +1599,7 @@ func (d *sequenceDialer) Dial(context.Context) (ports.Transport, error) {
15991599 if i >= len(d.trs) {
16001600 return nil, io.EOF
16011601 }
1602- return d.trs[i], nil
1602+ return &rawClientConnection{raw: d.trs[i]}, nil
16031603 }
16041604
16051605 type recordingTransport struct {
@@ -1939,7 +1939,7 @@ func TestAttachRememberRemoteHost(t *testing.T) {
19391939 return nil
19401940 }
19411941
1942- tr := portsmocks.NewMockTransport(t)
1942+ tr := newMockClientConnection(t)
19431943 if !tt.preWelcomeReject {
19441944 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
19451945 }
@@ -2033,7 +2033,7 @@ func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) {
20332033 releaseLearner := make(chan struct{})
20342034 learnerDone := make(chan struct{})
20352035
2036- tr := portsmocks.NewMockTransport(t)
2036+ tr := newMockClientConnection(t)
20372037 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
20382038 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
20392039 unblock := scriptRecv(tr,
@@ -2114,7 +2114,7 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) {
21142114 releaseLearner := make(chan struct{})
21152115 learnerDone := make(chan struct{})
21162116
2117- tr := portsmocks.NewMockTransport(t)
2117+ tr := newMockClientConnection(t)
21182118 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
21192119 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
21202120 unblock := scriptRecv(tr,
@@ -2211,7 +2211,7 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
22112211 return realClock{}.NewTimer(d)
22122212 }).Maybe()
22132213
2214- tr := portsmocks.NewMockTransport(t)
2214+ tr := newMockClientConnection(t)
22152215 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
22162216 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
22172217 unblock := scriptRecv(tr,
@@ -130,7 +130,7 @@ func (markedDatagramTransport) DatagramTransport() {}
130 // scriptRecv wires a MockTransport's Recv to yield the given items in order,130 // scriptRecv wires a MockTransport's Recv to yield the given items in order,
131 // then block forever (simulating a live but idle connection). It returns a131 // then block forever (simulating a live but idle connection). It returns a
132 // cleanup that unblocks the parked reader.132 // cleanup that unblocks the parked reader.
133-func scriptRecv(tr *portsmocks.MockTransport, items ...recvItem) func() {133+func scriptRecv(tr *mockClientConnection, items ...recvItem) func() {
134 ch := make(chan recvItem, len(items))134 ch := make(chan recvItem, len(items))
135 for _, it := range items {135 for _, it := range items {
136 ch <- it136 ch <- it
@@ -242,15 +242,15 @@ func isType(typ wire.MsgType) any {
242 // transportDialer adapts one already-open test transport to the Runner API.242 // transportDialer adapts one already-open test transport to the Runner API.
243 type transportDialer struct{ transport ports.Transport }243 type transportDialer struct{ transport ports.Transport }
244 244
245-func (d transportDialer) Dial(context.Context) (ports.Transport, error) {245+func (d transportDialer) Dial(context.Context) (ports.ClientConnection, error) {
246- return d.transport, nil246+ return &rawClientConnection{raw: d.transport}, nil
247 }247 }
248 248
249 func runTestClient(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error {249 func runTestClient(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error {
250 return client.NewRunner(deps).Run(ctx, request)250 return client.NewRunner(deps).Run(ctx, request)
251 }251 }
252 252
253-func testDependencies(dialer ports.Dialer, terminal ports.Terminal, clock ports.Clock, clipboard ports.ClipboardReader, observer ports.SerializedRuntimeObserver) client.Dependencies {253+func testDependencies(dialer ports.ClientDialer, terminal ports.Terminal, clock ports.Clock, clipboard ports.ClipboardReader, observer ports.SerializedRuntimeObserver) client.Dependencies {
254 return client.Dependencies{254 return client.Dependencies{
255 Dialer: dialer,255 Dialer: dialer,
256 Terminal: terminal,256 Terminal: terminal,
@@ -286,7 +286,7 @@ func newHandshakeClock(t *testing.T, timerCount int) (*portsmocks.MockClock, <-c
286 func TestRunBoundsBlockedDial(t *testing.T) {286 func TestRunBoundsBlockedDial(t *testing.T) {
287 clk, createdTimers := newHandshakeClock(t, 1)287 clk, createdTimers := newHandshakeClock(t, 1)
288 started := make(chan struct{})288 started := make(chan struct{})
289- dialer := portsmocks.NewMockDialer(t)289+ dialer := newMockClientDialer(t)
290 dialer.EXPECT().Dial(mock.Anything).RunAndReturn(func(ctx context.Context) (ports.Transport, error) {290 dialer.EXPECT().Dial(mock.Anything).RunAndReturn(func(ctx context.Context) (ports.Transport, error) {
291 close(started)291 close(started)
292 <-ctx.Done()292 <-ctx.Done()
@@ -330,7 +330,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
330 closed := make(chan struct{})330 closed := make(chan struct{})
331 var closeOnce sync.Once331 var closeOnce sync.Once
332 operationExited := make(chan struct{})332 operationExited := make(chan struct{})
333- tr := portsmocks.NewMockTransport(t)333+ tr := newMockClientConnection(t)
334 tr.EXPECT().Close().Run(func() { closeOnce.Do(func() { close(closed) }) }).Return(nil).Maybe()334 tr.EXPECT().Close().Run(func() { closeOnce.Do(func() { close(closed) }) }).Return(nil).Maybe()
335 335
336 block := func() error {336 block := func() error {
@@ -349,7 +349,7 @@ func TestRunBoundsPreWelcomeOperations(t *testing.T) {
349 return wire.Frame{}, block()349 return wire.Frame{}, block()
350 }).Once()350 }).Once()
351 }351 }
352- dialer := portsmocks.NewMockDialer(t)352+ dialer := newMockClientDialer(t)
353 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()353 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
354 354
355 errCh := make(chan error, 1)355 errCh := make(chan error, 1)
@@ -401,7 +401,7 @@ func TestAttachHelloIncludesTrueColor(t *testing.T) {
401 defer in.unblock()401 defer in.unblock()
402 402
403 gotHello := make(chan protocol.Hello, 1)403 gotHello := make(chan protocol.Hello, 1)
404- tr := portsmocks.NewMockTransport(t)404+ tr := newMockClientConnection(t)
405 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()405 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
406 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()406 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
407 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {407 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {
@@ -616,7 +616,7 @@ func TestAcceptedHomeActionUsesCapturedRouteAfterRequestMetadataRebase(t *testin
616 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2}}616 localDialer := &sequenceDialer{trs: []ports.Transport{local1, local2}}
617 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}617 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote}}
618 deps := testDependencies(localDialer, term, realClock{}, nil, nil)618 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
619- deps.AttachHandoff = func(protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {619+ deps.AttachHandoff = func(protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
620 target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}620 target := protocol.ExactSessionTarget{LifecycleID: remoteLifecycle, SessionName: "work"}
621 return remoteDialer, client.AttachRequest{621 return remoteDialer, client.AttachRequest{
622 Intent: protocol.IntentAttach, SessionName: "work", Remote: true,622 Intent: protocol.IntentAttach, SessionName: "work", Remote: true,
@@ -650,9 +650,9 @@ func hybridLocalBootstrap(lifecycle domain.SessionLifecycleID, target domain.Rem
650 }}650 }}
651 }651 }
652 652
653-func hybridPickerDependencies(localDialer ports.Dialer, term ports.Terminal, clock ports.Clock, remoteByEndpoint map[string]ports.Dialer) client.Dependencies {653+func hybridPickerDependencies(localDialer ports.ClientDialer, term ports.Terminal, clock ports.Clock, remoteByEndpoint map[string]ports.ClientDialer) client.Dependencies {
654 deps := testDependencies(localDialer, term, clock, nil, nil)654 deps := testDependencies(localDialer, term, clock, nil, nil)
655- deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {655+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
656 if target.RemoteTarget == nil {656 if target.RemoteTarget == nil {
657 return nil, client.AttachRequest{}, errors.New("hybrid picker target is not remote")657 return nil, client.AttachRequest{}, errors.New("hybrid picker target is not remote")
658 }658 }
@@ -743,7 +743,7 @@ func TestHybridPickerSameHostSwitchReusesRemoteTransport(t *testing.T) {
743 }743 }
744 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}744 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
745 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}745 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
746- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})746+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
747 747
748 err := runTestClient(context.Background(), deps, client.AttachRequest{748 err := runTestClient(context.Background(), deps, client.AttachRequest{
749 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",749 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
@@ -826,7 +826,7 @@ func TestHybridPickerExpiredSwitchFallsBackToNewDial(t *testing.T) {
826 remoteDialer := &sequenceDialer{trs: []ports.Transport{826 remoteDialer := &sequenceDialer{trs: []ports.Transport{
827 markedDatagramTransport{Transport: sourceRemote}, targetRemote,827 markedDatagramTransport{Transport: sourceRemote}, targetRemote,
828 }}828 }}
829- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})829+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
830 830
831 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{831 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
832 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",832 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
@@ -890,7 +890,7 @@ func TestHybridPickerPrepareResponseTimeoutClosesRetainedTransport(t *testing.T)
890 890
891 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial}}891 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial}}
892 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}892 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
893- deps := hybridPickerDependencies(localDialer, term, clock, map[string]ports.Dialer{"remote": remoteDialer})893+ deps := hybridPickerDependencies(localDialer, term, clock, map[string]ports.ClientDialer{"remote": remoteDialer})
894 894
895 ctx, cancel := context.WithCancel(context.Background())895 ctx, cancel := context.WithCancel(context.Background())
896 defer cancel()896 defer cancel()
@@ -967,7 +967,7 @@ func TestHybridPickerBackResumesRetainedRemoteTransport(t *testing.T) {
967 }}967 }}
968 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}968 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
969 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}969 remoteDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: remote}}}
970- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{"remote": remoteDialer})970+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{"remote": remoteDialer})
971 971
972 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{972 require.NoError(t, runTestClient(context.Background(), deps, client.AttachRequest{
973 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",973 Intent: protocol.IntentAttach, SessionName: "local", Origin: protocol.RouteOriginLocal, OriginKey: "local",
@@ -1023,7 +1023,7 @@ func TestHybridPickerDifferentHostFallsBackToNewRemoteDial(t *testing.T) {
1023 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}1023 localDialer := &sequenceDialer{trs: []ports.Transport{localInitial, localPicker}}
1024 sourceDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: sourceRemote}}}1024 sourceDialer := &sequenceDialer{trs: []ports.Transport{markedDatagramTransport{Transport: sourceRemote}}}
1025 targetDialer := &sequenceDialer{trs: []ports.Transport{targetRemote}}1025 targetDialer := &sequenceDialer{trs: []ports.Transport{targetRemote}}
1026- deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.Dialer{1026+ deps := hybridPickerDependencies(localDialer, term, realClock{}, map[string]ports.ClientDialer{
1027 sourceTarget.Endpoint: sourceDialer,1027 sourceTarget.Endpoint: sourceDialer,
1028 targetTarget.Endpoint: targetDialer,1028 targetTarget.Endpoint: targetDialer,
1029 })1029 })
@@ -1109,7 +1109,7 @@ func TestRouteNavigationPreservesRemoteHomePickerAcrossLocalReturn(t *testing.T)
1109 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2, remote3}}1109 remoteDialer := &sequenceDialer{trs: []ports.Transport{remote1, remote2, remote3}}
1110 1110
1111 deps := testDependencies(localDialer, term, realClock{}, nil, nil)1111 deps := testDependencies(localDialer, term, realClock{}, nil, nil)
1112- deps.AttachHandoff = func(target protocol.AttachTarget) (ports.Dialer, client.AttachRequest, error) {1112+ deps.AttachHandoff = func(target protocol.AttachTarget) (ports.ClientDialer, client.AttachRequest, error) {
1113 require.Equal(t, remoteHandoff, target)1113 require.Equal(t, remoteHandoff, target)
1114 return remoteDialer, client.AttachRequest{1114 return remoteDialer, client.AttachRequest{
1115 Intent: protocol.IntentAttach, SessionName: target.Session, Remote: true,1115 Intent: protocol.IntentAttach, SessionName: target.Session, Remote: true,
@@ -1170,7 +1170,7 @@ func TestAttachTargetHandoffReturnsValidatedTargetAndClosesTransport(t *testing.
1170 defer in.unblock()1170 defer in.unblock()
1171 1171
1172 target := protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}1172 target := protocol.AttachTarget{Endpoint: "remote.example", Session: "work", Intent: protocol.IntentAttach}
1173- tr := portsmocks.NewMockTransport(t)1173+ tr := newMockClientConnection(t)
1174 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1174 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1175 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1175 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
1176 unblock := scriptRecv(tr,1176 unblock := scriptRecv(tr,
@@ -1197,7 +1197,7 @@ func TestAttachHelloIncludesCompleteLocalEnvironment(t *testing.T) {
1197 defer in.unblock()1197 defer in.unblock()
1198 1198
1199 gotHello := make(chan protocol.Hello, 1)1199 gotHello := make(chan protocol.Hello, 1)
1200- tr := portsmocks.NewMockTransport(t)1200+ tr := newMockClientConnection(t)
1201 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1201 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1202 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1202 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1203 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {1203 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {
@@ -1226,7 +1226,7 @@ func TestAttachHelloRequestsSingleOutputForDatagramTransport(t *testing.T) {
1226 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)1226 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
1227 defer in.unblock()1227 defer in.unblock()
1228 1228
1229- tr := portsmocks.NewMockTransport(t)1229+ tr := newMockClientConnection(t)
1230 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1230 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1231 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1231 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1232 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {1232 tr.EXPECT().Send(isType(wire.MsgHello)).RunAndReturn(func(f wire.Frame) error {
@@ -1252,7 +1252,7 @@ func TestAttachHappyPath(t *testing.T) {
1252 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)1252 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
1253 defer in.unblock()1253 defer in.unblock()
1254 1254
1255- tr := portsmocks.NewMockTransport(t)1255+ tr := newMockClientConnection(t)
1256 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1256 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1257 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1257 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1258 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1258 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1275,7 +1275,7 @@ func TestAttachVersionMismatch(t *testing.T) {
1275 tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once()1275 tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once()
1276 // EnterRaw must NOT be called on the error-before-welcome path.1276 // EnterRaw must NOT be called on the error-before-welcome path.
1277 1277
1278- tr := portsmocks.NewMockTransport(t)1278+ tr := newMockClientConnection(t)
1279 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1279 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
1280 tr.EXPECT().Recv().Return(1280 tr.EXPECT().Recv().Return(
1281 frameOf(wire.MsgError, wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),1281 frameOf(wire.MsgError, wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
@@ -1298,7 +1298,7 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
1298 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)1298 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
1299 defer in.unblock()1299 defer in.unblock()
1300 1300
1301- tr := portsmocks.NewMockTransport(t)1301+ tr := newMockClientConnection(t)
1302 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1302 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1303 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1303 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1304 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1304 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1322,7 +1322,7 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) {
1322 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)1322 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
1323 defer in.unblock()1323 defer in.unblock()
1324 1324
1325- tr := portsmocks.NewMockTransport(t)1325+ tr := newMockClientConnection(t)
1326 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1326 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1327 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1327 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1328 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1328 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1356,7 +1356,7 @@ func TestAttachStdinForwardsSGRMouseReportAsSingleFrame(t *testing.T) {
1356 1356
1357 gotInput := make(chan []byte, 2)1357 gotInput := make(chan []byte, 2)
1358 allowDetach := make(chan struct{})1358 allowDetach := make(chan struct{})
1359- tr := portsmocks.NewMockTransport(t)1359+ tr := newMockClientConnection(t)
1360 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1360 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1361 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1361 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1362 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1362 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1418,7 +1418,7 @@ func TestAttachStdinCoalescesSplitBracketedPaste(t *testing.T) {
1418 1418
1419 gotInput := make(chan []byte, 1)1419 gotInput := make(chan []byte, 1)
1420 allowDetach := make(chan struct{})1420 allowDetach := make(chan struct{})
1421- tr := portsmocks.NewMockTransport(t)1421+ tr := newMockClientConnection(t)
1422 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1422 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1423 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1423 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1424 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1424 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1473,7 +1473,7 @@ func TestAttachForwardsResize(t *testing.T) {
1473 firstRecv := make(chan struct{})1473 firstRecv := make(chan struct{})
1474 var firstRecvOnce sync.Once1474 var firstRecvOnce sync.Once
1475 1475
1476- tr := portsmocks.NewMockTransport(t)1476+ tr := newMockClientConnection(t)
1477 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1477 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1478 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1478 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1479 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1479 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1534,7 +1534,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
1534 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)1534 tm, in := newHappyTerminal(t, &out, &restoreCount, resizeCh)
1535 defer in.unblock()1535 defer in.unblock()
1536 1536
1537- tr := portsmocks.NewMockTransport(t)1537+ tr := newMockClientConnection(t)
1538 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1538 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1539 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1539 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1540 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1540 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
@@ -1545,7 +1545,7 @@ func TestRunRestoresRawModeAfterAttachError(t *testing.T) {
1545 )1545 )
1546 defer unblock()1546 defer unblock()
1547 tr.EXPECT().Close().Return(nil).Once()1547 tr.EXPECT().Close().Return(nil).Once()
1548- d := portsmocks.NewMockDialer(t)1548+ d := newMockClientDialer(t)
1549 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1549 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1550 1550
1551 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: false})1551 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, SessionName: "", Remote: false})
@@ -1558,14 +1558,14 @@ func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) {
1558 tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once()1558 tm.EXPECT().Geometry().Return(domain.Geometry{Size: domain.Size{Cols: 80, Rows: 24}}, nil).Once()
1559 // EnterRaw must NOT be called when the daemon rejects Hello before Welcome.1559 // EnterRaw must NOT be called when the daemon rejects Hello before Welcome.
1560 1560
1561- tr := portsmocks.NewMockTransport(t)1561+ tr := newMockClientConnection(t)
1562 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()1562 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
1563 tr.EXPECT().Recv().Return(1563 tr.EXPECT().Recv().Return(
1564 frameOf(wire.MsgError, wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),1564 frameOf(wire.MsgError, wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrVersionMismatch, Text: "version mismatch"})),
1565 nil,1565 nil,
1566 ).Once()1566 ).Once()
1567 tr.EXPECT().Close().Return(nil).Once()1567 tr.EXPECT().Close().Return(nil).Once()
1568- d := portsmocks.NewMockDialer(t)1568+ d := newMockClientDialer(t)
1569 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1569 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1570 1570
1571 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})1571 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, nil, nil), client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "main", Remote: false})
@@ -1577,7 +1577,7 @@ func TestRunDoesNotEnterRawBeforePreWelcomeError(t *testing.T) {
1577 1577
1578 func TestRunPhaseASingleAttempt(t *testing.T) {1578 func TestRunPhaseASingleAttempt(t *testing.T) {
1579 dialErr := errors.New("dial failed")1579 dialErr := errors.New("dial failed")
1580- d := portsmocks.NewMockDialer(t)1580+ d := newMockClientDialer(t)
1581 d.EXPECT().Dial(mock.Anything).Return(nil, dialErr).Once()1581 d.EXPECT().Dial(mock.Anything).Return(nil, dialErr).Once()
1582 tm := portsmocks.NewMockTerminal(t)1582 tm := portsmocks.NewMockTerminal(t)
1583 1583
@@ -1591,7 +1591,7 @@ type sequenceDialer struct {
1591 calls atomic.Int321591 calls atomic.Int32
1592 }1592 }
1593 1593
1594-func (d *sequenceDialer) Dial(context.Context) (ports.Transport, error) {1594+func (d *sequenceDialer) Dial(context.Context) (ports.ClientConnection, error) {
1595 i := int(d.calls.Add(1)) - 11595 i := int(d.calls.Add(1)) - 1
1596 if i < len(d.errs) && d.errs[i] != nil {1596 if i < len(d.errs) && d.errs[i] != nil {
1597 return nil, d.errs[i]1597 return nil, d.errs[i]
@@ -1599,7 +1599,7 @@ func (d *sequenceDialer) Dial(context.Context) (ports.Transport, error) {
1599 if i >= len(d.trs) {1599 if i >= len(d.trs) {
1600 return nil, io.EOF1600 return nil, io.EOF
1601 }1601 }
1602- return d.trs[i], nil1602+ return &rawClientConnection{raw: d.trs[i]}, nil
1603 }1603 }
1604 1604
1605 type recordingTransport struct {1605 type recordingTransport struct {
@@ -1939,7 +1939,7 @@ func TestAttachRememberRemoteHost(t *testing.T) {
1939 return nil1939 return nil
1940 }1940 }
1941 1941
1942- tr := portsmocks.NewMockTransport(t)1942+ tr := newMockClientConnection(t)
1943 if !tt.preWelcomeReject {1943 if !tt.preWelcomeReject {
1944 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()1944 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
1945 }1945 }
@@ -2033,7 +2033,7 @@ func TestAttachRememberRemoteHostDoesNotBlockAfterWelcome(t *testing.T) {
2033 releaseLearner := make(chan struct{})2033 releaseLearner := make(chan struct{})
2034 learnerDone := make(chan struct{})2034 learnerDone := make(chan struct{})
2035 2035
2036- tr := portsmocks.NewMockTransport(t)2036+ tr := newMockClientConnection(t)
2037 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()2037 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
2038 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()2038 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
2039 unblock := scriptRecv(tr,2039 unblock := scriptRecv(tr,
@@ -2114,7 +2114,7 @@ func TestRunRestoresTerminalBeforeWaitingForLearner(t *testing.T) {
2114 releaseLearner := make(chan struct{})2114 releaseLearner := make(chan struct{})
2115 learnerDone := make(chan struct{})2115 learnerDone := make(chan struct{})
2116 2116
2117- tr := portsmocks.NewMockTransport(t)2117+ tr := newMockClientConnection(t)
2118 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()2118 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
2119 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()2119 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
2120 unblock := scriptRecv(tr,2120 unblock := scriptRecv(tr,
@@ -2211,7 +2211,7 @@ func TestRunReturnsWhenRemoteHostLearnerStalls(t *testing.T) {
2211 return realClock{}.NewTimer(d)2211 return realClock{}.NewTimer(d)
2212 }).Maybe()2212 }).Maybe()
2213 2213
2214- tr := portsmocks.NewMockTransport(t)2214+ tr := newMockClientConnection(t)
2215 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()2215 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
2216 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()2216 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
2217 unblock := scriptRecv(tr,2217 unblock := scriptRecv(tr,
M internal/usecase/client/clipboard_intercept_test.go +4 -4
@@ -44,7 +44,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
4444 gotImage = make(chan protocol.ImagePush, 1)
4545 allowDetach := make(chan struct{})
4646
47- tr := portsmocks.NewMockTransport(t)
47+ tr := newMockClientConnection(t)
4848 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
4949 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
5050 tr.EXPECT().Send(isType(wire.MsgResize)).Return(nil).Maybe()
@@ -88,7 +88,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
8888 }).Maybe()
8989 tr.EXPECT().Close().Return(nil).Once()
9090
91- d := portsmocks.NewMockDialer(t)
91+ d := newMockClientDialer(t)
9292 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
9393
9494 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, clip, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, Remote: remote})
@@ -157,8 +157,8 @@ func (*clipboardToastLifecycleTransport) Close() error { return n
157157
158158 type clipboardToastLifecycleDialer struct{ transport ports.Transport }
159159
160-func (d clipboardToastLifecycleDialer) Dial(context.Context) (ports.Transport, error) {
161- return d.transport, nil
160+func (d clipboardToastLifecycleDialer) Dial(context.Context) (ports.ClientConnection, error) {
161+ return &rawClientConnection{raw: d.transport}, nil
162162 }
163163
164164 func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testing.T) {
@@ -44,7 +44,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
44 gotImage = make(chan protocol.ImagePush, 1)44 gotImage = make(chan protocol.ImagePush, 1)
45 allowDetach := make(chan struct{})45 allowDetach := make(chan struct{})
46 46
47- tr := portsmocks.NewMockTransport(t)47+ tr := newMockClientConnection(t)
48 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()48 tr.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
49 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()49 tr.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
50 tr.EXPECT().Send(isType(wire.MsgResize)).Return(nil).Maybe()50 tr.EXPECT().Send(isType(wire.MsgResize)).Return(nil).Maybe()
@@ -88,7 +88,7 @@ func runRemoteClipboardTest(t *testing.T, remote bool, clip ports.ClipboardReade
88 }).Maybe()88 }).Maybe()
89 tr.EXPECT().Close().Return(nil).Once()89 tr.EXPECT().Close().Return(nil).Once()
90 90
91- d := portsmocks.NewMockDialer(t)91+ d := newMockClientDialer(t)
92 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()92 d.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
93 93
94 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, clip, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, Remote: remote})94 err := runTestClient(context.Background(), testDependencies(d, tm, realClock{}, clip, nil), client.AttachRequest{Intent: protocol.IntentEphemeral, Remote: remote})
@@ -157,8 +157,8 @@ func (*clipboardToastLifecycleTransport) Close() error { return n
157 157
158 type clipboardToastLifecycleDialer struct{ transport ports.Transport }158 type clipboardToastLifecycleDialer struct{ transport ports.Transport }
159 159
160-func (d clipboardToastLifecycleDialer) Dial(context.Context) (ports.Transport, error) {160+func (d clipboardToastLifecycleDialer) Dial(context.Context) (ports.ClientConnection, error) {
161- return d.transport, nil161+ return &rawClientConnection{raw: d.transport}, nil
162 }162 }
163 163
164 func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testing.T) {164 func TestRunRemoteClipboardFailureNotifiesDaemonAndWritesOutputVerbatim(t *testing.T) {
M internal/usecase/client/handshake.go +6 -6
@@ -63,7 +63,7 @@ func newHandshakeContext(parent context.Context, clock ports.Clock) (context.Con
6363
6464 // watchHandshakeTransport closes a transport when the handshake context ends.
6565 // Transport.Close is required to interrupt blocked Send and Recv operations.
66-func watchHandshakeTransport(ctx context.Context, transport ports.Transport) func() {
66+func watchHandshakeTransport(ctx context.Context, transport ports.ClientConnection) func() {
6767 stop := make(chan struct{})
6868 done := make(chan struct{})
6969 go func() {
@@ -83,11 +83,11 @@ func watchHandshakeTransport(ctx context.Context, transport ports.Transport) fun
8383 }
8484 }
8585
86-func boundedHandshakeOperation(ctx context.Context, transport ports.Transport, operation func() error) error {
86+func boundedHandshakeOperation(ctx context.Context, transport ports.ClientConnection, operation func() error) error {
8787 return boundedHandshakeOperationWithTransition(ctx, transport, operation, nil)
8888 }
8989
90-func boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.Transport, operation func() error, transition *transitionUI) error {
90+func boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.ClientConnection, operation func() error, transition *transitionUI) error {
9191 if err := ctx.Err(); err != nil {
9292 _ = transport.Close()
9393 return err
@@ -116,16 +116,16 @@ func boundedHandshakeOperationWithTransition(ctx context.Context, transport port
116116 }
117117 }
118118
119-func boundedDial(ctx context.Context, dialer ports.Dialer) (ports.Transport, error) {
119+func boundedDial(ctx context.Context, dialer ports.ClientDialer) (ports.ClientConnection, error) {
120120 return boundedDialWithTransition(ctx, dialer, nil)
121121 }
122122
123-func boundedDialWithTransition(ctx context.Context, dialer ports.Dialer, transition *transitionUI) (ports.Transport, error) {
123+func boundedDialWithTransition(ctx context.Context, dialer ports.ClientDialer, transition *transitionUI) (ports.ClientConnection, error) {
124124 // The dial context only bounds startup. A successful carriage outlives the
125125 // handshake context; canceling that context after Welcome must not kill SSH.
126126 dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
127127 type dialResult struct {
128- transport ports.Transport
128+ transport ports.ClientConnection
129129 err error
130130 }
131131 result := make(chan dialResult, 1)
@@ -63,7 +63,7 @@ func newHandshakeContext(parent context.Context, clock ports.Clock) (context.Con
63 63
64 // watchHandshakeTransport closes a transport when the handshake context ends.64 // watchHandshakeTransport closes a transport when the handshake context ends.
65 // Transport.Close is required to interrupt blocked Send and Recv operations.65 // Transport.Close is required to interrupt blocked Send and Recv operations.
66-func watchHandshakeTransport(ctx context.Context, transport ports.Transport) func() {66+func watchHandshakeTransport(ctx context.Context, transport ports.ClientConnection) func() {
67 stop := make(chan struct{})67 stop := make(chan struct{})
68 done := make(chan struct{})68 done := make(chan struct{})
69 go func() {69 go func() {
@@ -83,11 +83,11 @@ func watchHandshakeTransport(ctx context.Context, transport ports.Transport) fun
83 }83 }
84 }84 }
85 85
86-func boundedHandshakeOperation(ctx context.Context, transport ports.Transport, operation func() error) error {86+func boundedHandshakeOperation(ctx context.Context, transport ports.ClientConnection, operation func() error) error {
87 return boundedHandshakeOperationWithTransition(ctx, transport, operation, nil)87 return boundedHandshakeOperationWithTransition(ctx, transport, operation, nil)
88 }88 }
89 89
90-func boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.Transport, operation func() error, transition *transitionUI) error {90+func boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.ClientConnection, operation func() error, transition *transitionUI) error {
91 if err := ctx.Err(); err != nil {91 if err := ctx.Err(); err != nil {
92 _ = transport.Close()92 _ = transport.Close()
93 return err93 return err
@@ -116,16 +116,16 @@ func boundedHandshakeOperationWithTransition(ctx context.Context, transport port
116 }116 }
117 }117 }
118 118
119-func boundedDial(ctx context.Context, dialer ports.Dialer) (ports.Transport, error) {119+func boundedDial(ctx context.Context, dialer ports.ClientDialer) (ports.ClientConnection, error) {
120 return boundedDialWithTransition(ctx, dialer, nil)120 return boundedDialWithTransition(ctx, dialer, nil)
121 }121 }
122 122
123-func boundedDialWithTransition(ctx context.Context, dialer ports.Dialer, transition *transitionUI) (ports.Transport, error) {123+func boundedDialWithTransition(ctx context.Context, dialer ports.ClientDialer, transition *transitionUI) (ports.ClientConnection, error) {
124 // The dial context only bounds startup. A successful carriage outlives the124 // The dial context only bounds startup. A successful carriage outlives the
125 // handshake context; canceling that context after Welcome must not kill SSH.125 // handshake context; canceling that context after Welcome must not kill SSH.
126 dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))126 dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
127 type dialResult struct {127 type dialResult struct {
128- transport ports.Transport128+ transport ports.ClientConnection
129 err error129 err error
130 }130 }
131 result := make(chan dialResult, 1)131 result := make(chan dialResult, 1)
M internal/usecase/client/handshake_test.go +1 -1
@@ -14,7 +14,7 @@ type failedContextDialer struct {
1414 ctx context.Context
1515 }
1616
17-func (d *failedContextDialer) Dial(ctx context.Context) (ports.Transport, error) {
17+func (d *failedContextDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
1818 d.ctx = ctx
1919 return nil, errors.New("dial failed")
2020 }
@@ -14,7 +14,7 @@ type failedContextDialer struct {
14 ctx context.Context14 ctx context.Context
15 }15 }
16 16
17-func (d *failedContextDialer) Dial(ctx context.Context) (ports.Transport, error) {17+func (d *failedContextDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
18 d.ctx = ctx18 d.ctx = ctx
19 return nil, errors.New("dial failed")19 return nil, errors.New("dial failed")
20 }20 }
M internal/usecase/client/palette_attach_generation_test.go +1 -1
@@ -438,7 +438,7 @@ func TestStdinPumpSameReadSchemeAndOldMarkerCannotAdvanceReplacement(t *testing.
438438 // notification and publish its replacement generation before the scanner
439439 // can deliver the old marker from the same terminal Read.
440440 events := make(chan paletteGenerationEvent)
441- out := make(chan wire.Frame, 1)
441+ out := make(chan protocol.ClientMessage, 1)
442442 schemeHandled := make(chan struct{})
443443 pump := &stdinPump{
444444 ctx: ctx,
@@ -438,7 +438,7 @@ func TestStdinPumpSameReadSchemeAndOldMarkerCannotAdvanceReplacement(t *testing.
438 // notification and publish its replacement generation before the scanner438 // notification and publish its replacement generation before the scanner
439 // can deliver the old marker from the same terminal Read.439 // can deliver the old marker from the same terminal Read.
440 events := make(chan paletteGenerationEvent)440 events := make(chan paletteGenerationEvent)
441- out := make(chan wire.Frame, 1)441+ out := make(chan protocol.ClientMessage, 1)
442 schemeHandled := make(chan struct{})442 schemeHandled := make(chan struct{})
443 pump := &stdinPump{443 pump := &stdinPump{
444 ctx: ctx,444 ctx: ctx,
M internal/usecase/client/reconnect_toast_test.go +13 -13
@@ -152,13 +152,13 @@ type reconnectToastSequenceDialer struct {
152152 calls int
153153 }
154154
155-func (d *reconnectToastSequenceDialer) Dial(context.Context) (ports.Transport, error) {
155+func (d *reconnectToastSequenceDialer) Dial(context.Context) (ports.ClientConnection, error) {
156156 if d.calls >= len(d.transports) {
157157 return nil, io.EOF
158158 }
159159 tr := d.transports[d.calls]
160160 d.calls++
161- return tr, nil
161+ return &rawClientConnection{raw: tr}, nil
162162 }
163163
164164 type reconnectToastRecordingTransport struct {
@@ -323,7 +323,7 @@ func newReconnectHandshakeClock(t *testing.T) *portsmocks.MockClock {
323323 return clk
324324 }
325325
326-func newReconnectAttachAttempt(term ports.Terminal, transport ports.Transport, clock ports.Clock, request AttachRequest, resumeToken uint64, state *terminalThemeState, linkEvents <-chan ports.LinkEvent, ms *milestones) *attachAttempt {
326+func newReconnectAttachAttempt(term ports.Terminal, transport ports.ClientConnection, clock ports.Clock, request AttachRequest, resumeToken uint64, state *terminalThemeState, linkEvents <-chan ports.LinkEvent, ms *milestones) *attachAttempt {
327327 rawEntered := false
328328 enterRaw := func() error {
329329 if rawEntered {
@@ -1430,7 +1430,7 @@ func TestReconnectRetrySleepFailuresPreserveDialError(t *testing.T) {
14301430 reconnectToastRecv{frame: reconnectToastWelcome(44)},
14311431 reconnectToastRecv{err: io.EOF},
14321432 )
1433- dialer := portsmocks.NewMockDialer(t)
1433+ dialer := newMockClientDialer(t)
14341434 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
14351435 dialer.EXPECT().Dial(mock.Anything).Run(func(context.Context) { armed.Store(true) }).Return(nil, dialErr).Once()
14361436
@@ -1484,12 +1484,12 @@ func (w *reconnectStatusClearFailWriter) Write(p []byte) (int, error) {
14841484 func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T) {
14851485 tests := []struct {
14861486 name string
1487- configure func(*portsmocks.MockDialer, ports.Transport)
1487+ configure func(*mockClientDialer, ports.Transport)
14881488 sleep func(context.CancelFunc) bool
14891489 }{
14901490 {
14911491 name: "after attach failure",
1492- configure: func(dialer *portsmocks.MockDialer, tr ports.Transport) {
1492+ configure: func(dialer *mockClientDialer, tr ports.Transport) {
14931493 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
14941494 },
14951495 sleep: func(cancel context.CancelFunc) bool {
@@ -1499,7 +1499,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T)
14991499 },
15001500 {
15011501 name: "after reconnect dial failure",
1502- configure: func(dialer *portsmocks.MockDialer, tr ports.Transport) {
1502+ configure: func(dialer *mockClientDialer, tr ports.Transport) {
15031503 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
15041504 dialer.EXPECT().Dial(mock.Anything).Return(nil, errors.New("redial failed")).Once()
15051505 },
@@ -1538,7 +1538,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T)
15381538 reconnectToastRecv{frame: reconnectToastWelcome(11)},
15391539 reconnectToastRecv{err: io.EOF},
15401540 )
1541- dialer := portsmocks.NewMockDialer(t)
1541+ dialer := newMockClientDialer(t)
15421542 tt.configure(dialer, tr)
15431543
15441544 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"})
@@ -1640,13 +1640,13 @@ func TestRemoteEphemeralReconnectUsesAssignedSessionName(t *testing.T) {
16401640 func TestRemoteReconnectToastLifecycle(t *testing.T) {
16411641 tests := []struct {
16421642 name string
1643- configure func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport
1643+ configure func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport
16441644 sleep func(cancel context.CancelFunc) bool
16451645 wantErr func(t *testing.T, err error)
16461646 }{
16471647 {
16481648 name: "clears on successful reconnect",
1649- configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {
1649+ configure: func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport {
16501650 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
16511651 tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)})
16521652 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
@@ -1658,7 +1658,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
16581658 },
16591659 {
16601660 name: "clears on cancellation",
1661- configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {
1661+ configure: func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport {
16621662 tr := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
16631663 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
16641664 return []*reconnectToastRecordingTransport{tr}
@@ -1671,7 +1671,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
16711671 },
16721672 {
16731673 name: "clears on final exit",
1674- configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {
1674+ configure: func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport {
16751675 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
16761676 tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonSessionKilled)})
16771677 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
@@ -1702,7 +1702,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
17021702
17031703 term := newReconnectToastTerminalHarness(t)
17041704 defer term.closeInput()
1705- dialer := portsmocks.NewMockDialer(t)
1705+ dialer := newMockClientDialer(t)
17061706 transports := tt.configure(t, dialer)
17071707
17081708 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})
@@ -152,13 +152,13 @@ type reconnectToastSequenceDialer struct {
152 calls int152 calls int
153 }153 }
154 154
155-func (d *reconnectToastSequenceDialer) Dial(context.Context) (ports.Transport, error) {155+func (d *reconnectToastSequenceDialer) Dial(context.Context) (ports.ClientConnection, error) {
156 if d.calls >= len(d.transports) {156 if d.calls >= len(d.transports) {
157 return nil, io.EOF157 return nil, io.EOF
158 }158 }
159 tr := d.transports[d.calls]159 tr := d.transports[d.calls]
160 d.calls++160 d.calls++
161- return tr, nil161+ return &rawClientConnection{raw: tr}, nil
162 }162 }
163 163
164 type reconnectToastRecordingTransport struct {164 type reconnectToastRecordingTransport struct {
@@ -323,7 +323,7 @@ func newReconnectHandshakeClock(t *testing.T) *portsmocks.MockClock {
323 return clk323 return clk
324 }324 }
325 325
326-func newReconnectAttachAttempt(term ports.Terminal, transport ports.Transport, clock ports.Clock, request AttachRequest, resumeToken uint64, state *terminalThemeState, linkEvents <-chan ports.LinkEvent, ms *milestones) *attachAttempt {326+func newReconnectAttachAttempt(term ports.Terminal, transport ports.ClientConnection, clock ports.Clock, request AttachRequest, resumeToken uint64, state *terminalThemeState, linkEvents <-chan ports.LinkEvent, ms *milestones) *attachAttempt {
327 rawEntered := false327 rawEntered := false
328 enterRaw := func() error {328 enterRaw := func() error {
329 if rawEntered {329 if rawEntered {
@@ -1430,7 +1430,7 @@ func TestReconnectRetrySleepFailuresPreserveDialError(t *testing.T) {
1430 reconnectToastRecv{frame: reconnectToastWelcome(44)},1430 reconnectToastRecv{frame: reconnectToastWelcome(44)},
1431 reconnectToastRecv{err: io.EOF},1431 reconnectToastRecv{err: io.EOF},
1432 )1432 )
1433- dialer := portsmocks.NewMockDialer(t)1433+ dialer := newMockClientDialer(t)
1434 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1434 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1435 dialer.EXPECT().Dial(mock.Anything).Run(func(context.Context) { armed.Store(true) }).Return(nil, dialErr).Once()1435 dialer.EXPECT().Dial(mock.Anything).Run(func(context.Context) { armed.Store(true) }).Return(nil, dialErr).Once()
1436 1436
@@ -1484,12 +1484,12 @@ func (w *reconnectStatusClearFailWriter) Write(p []byte) (int, error) {
1484 func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T) {1484 func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T) {
1485 tests := []struct {1485 tests := []struct {
1486 name string1486 name string
1487- configure func(*portsmocks.MockDialer, ports.Transport)1487+ configure func(*mockClientDialer, ports.Transport)
1488 sleep func(context.CancelFunc) bool1488 sleep func(context.CancelFunc) bool
1489 }{1489 }{
1490 {1490 {
1491 name: "after attach failure",1491 name: "after attach failure",
1492- configure: func(dialer *portsmocks.MockDialer, tr ports.Transport) {1492+ configure: func(dialer *mockClientDialer, tr ports.Transport) {
1493 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1493 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1494 },1494 },
1495 sleep: func(cancel context.CancelFunc) bool {1495 sleep: func(cancel context.CancelFunc) bool {
@@ -1499,7 +1499,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T)
1499 },1499 },
1500 {1500 {
1501 name: "after reconnect dial failure",1501 name: "after reconnect dial failure",
1502- configure: func(dialer *portsmocks.MockDialer, tr ports.Transport) {1502+ configure: func(dialer *mockClientDialer, tr ports.Transport) {
1503 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1503 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1504 dialer.EXPECT().Dial(mock.Anything).Return(nil, errors.New("redial failed")).Once()1504 dialer.EXPECT().Dial(mock.Anything).Return(nil, errors.New("redial failed")).Once()
1505 },1505 },
@@ -1538,7 +1538,7 @@ func TestReconnectCancellationPreservesContextWhenStatusClearFails(t *testing.T)
1538 reconnectToastRecv{frame: reconnectToastWelcome(11)},1538 reconnectToastRecv{frame: reconnectToastWelcome(11)},
1539 reconnectToastRecv{err: io.EOF},1539 reconnectToastRecv{err: io.EOF},
1540 )1540 )
1541- dialer := portsmocks.NewMockDialer(t)1541+ dialer := newMockClientDialer(t)
1542 tt.configure(dialer, tr)1542 tt.configure(dialer, tr)
1543 1543
1544 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 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"})
@@ -1640,13 +1640,13 @@ func TestRemoteEphemeralReconnectUsesAssignedSessionName(t *testing.T) {
1640 func TestRemoteReconnectToastLifecycle(t *testing.T) {1640 func TestRemoteReconnectToastLifecycle(t *testing.T) {
1641 tests := []struct {1641 tests := []struct {
1642 name string1642 name string
1643- configure func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport1643+ configure func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport
1644 sleep func(cancel context.CancelFunc) bool1644 sleep func(cancel context.CancelFunc) bool
1645 wantErr func(t *testing.T, err error)1645 wantErr func(t *testing.T, err error)
1646 }{1646 }{
1647 {1647 {
1648 name: "clears on successful reconnect",1648 name: "clears on successful reconnect",
1649- configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {1649+ configure: func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport {
1650 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})1650 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
1651 tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)})1651 tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonDetach)})
1652 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()1652 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
@@ -1658,7 +1658,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
1658 },1658 },
1659 {1659 {
1660 name: "clears on cancellation",1660 name: "clears on cancellation",
1661- configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {1661+ configure: func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport {
1662 tr := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})1662 tr := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
1663 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()1663 dialer.EXPECT().Dial(mock.Anything).Return(tr, nil).Once()
1664 return []*reconnectToastRecordingTransport{tr}1664 return []*reconnectToastRecordingTransport{tr}
@@ -1671,7 +1671,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
1671 },1671 },
1672 {1672 {
1673 name: "clears on final exit",1673 name: "clears on final exit",
1674- configure: func(t *testing.T, dialer *portsmocks.MockDialer) []*reconnectToastRecordingTransport {1674+ configure: func(t *testing.T, dialer *mockClientDialer) []*reconnectToastRecordingTransport {
1675 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})1675 tr1 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(11)}, reconnectToastRecv{err: io.EOF})
1676 tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonSessionKilled)})1676 tr2 := newReconnectToastRecordingTransport(reconnectToastRecv{frame: reconnectToastWelcome(22)}, reconnectToastRecv{frame: reconnectToastDetach(protocol.ReasonSessionKilled)})
1677 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()1677 dialer.EXPECT().Dial(mock.Anything).Return(tr1, nil).Once()
@@ -1702,7 +1702,7 @@ func TestRemoteReconnectToastLifecycle(t *testing.T) {
1702 1702
1703 term := newReconnectToastTerminalHarness(t)1703 term := newReconnectToastTerminalHarness(t)
1704 defer term.closeInput()1704 defer term.closeInput()
1705- dialer := portsmocks.NewMockDialer(t)1705+ dialer := newMockClientDialer(t)
1706 transports := tt.configure(t, dialer)1706 transports := tt.configure(t, dialer)
1707 1707
1708 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 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})
M internal/usecase/client/remote_attach_validation_test.go +1 -2
@@ -7,7 +7,6 @@ import (
77 "github.com/stretchr/testify/require"
88
99 "github.com/bnema/vev/internal/domain"
10- portsmocks "github.com/bnema/vev/internal/ports/mocks"
1110 "github.com/bnema/vev/internal/protocol"
1211 )
1312
@@ -24,7 +23,7 @@ func TestMalformedAttachRequestsNeverDial(t *testing.T) {
2423 for _, tt := range tests {
2524 t.Run(tt.name, func(t *testing.T) {
2625 // No EXPECT: any Dial call fails the test.
27- dialer := portsmocks.NewMockDialer(t)
26+ dialer := newMockClientDialer(t)
2827 err := NewRunner(Dependencies{Dialer: dialer}).Run(context.Background(), tt.request)
2928 require.Error(t, err)
3029 })
@@ -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- portsmocks "github.com/bnema/vev/internal/ports/mocks"
11 "github.com/bnema/vev/internal/protocol"10 "github.com/bnema/vev/internal/protocol"
12 )11 )
13 12
@@ -24,7 +23,7 @@ func TestMalformedAttachRequestsNeverDial(t *testing.T) {
24 for _, tt := range tests {23 for _, tt := range tests {
25 t.Run(tt.name, func(t *testing.T) {24 t.Run(tt.name, func(t *testing.T) {
26 // No EXPECT: any Dial call fails the test.25 // No EXPECT: any Dial call fails the test.
27- dialer := portsmocks.NewMockDialer(t)26+ dialer := newMockClientDialer(t)
28 err := NewRunner(Dependencies{Dialer: dialer}).Run(context.Background(), tt.request)27 err := NewRunner(Dependencies{Dialer: dialer}).Run(context.Background(), tt.request)
29 require.Error(t, err)28 require.Error(t, err)
30 })29 })
M internal/usecase/client/routes.go +4 -4
@@ -62,7 +62,7 @@ type routeCandidate struct {
6262 originKey string
6363 target protocol.ExactSessionTarget
6464 presentation routePresentation
65- dialer ports.Dialer
65+ dialer ports.ClientDialer
6666 request AttachRequest
6767 resumeToken uint64
6868 home bool
@@ -74,7 +74,7 @@ type routeRecord struct {
7474 originKey string
7575 target protocol.ExactSessionTarget
7676 presentation routePresentation
77- dialer ports.Dialer
77+ dialer ports.ClientDialer
7878 request AttachRequest
7979 resumeToken uint64
8080 home bool
@@ -115,7 +115,7 @@ func cloneRoutePosition(position *protocol.RoutePosition) *protocol.RoutePositio
115115 return &clone
116116 }
117117
118-func routeCandidateForAttach(request AttachRequest, identity protocol.CommittedRouteIdentity, dialer ports.Dialer, resumeToken uint64) routeCandidate {
118+func routeCandidateForAttach(request AttachRequest, identity protocol.CommittedRouteIdentity, dialer ports.ClientDialer, resumeToken uint64) routeCandidate {
119119 origin := normalizeRouteOrigin(request.Origin, request.Remote)
120120 originKey := normalizeRouteOriginKey(request.OriginKey, origin)
121121 kind := protocol.RouteKindLocal
@@ -628,7 +628,7 @@ type routeTransitionPlan struct {
628628 type routeTransitionCommit struct {
629629 identity protocol.CommittedRouteIdentity
630630 presentation routePresentation
631- dialer ports.Dialer
631+ dialer ports.ClientDialer
632632 resumeToken uint64
633633 }
634634
@@ -62,7 +62,7 @@ type routeCandidate struct {
62 originKey string62 originKey string
63 target protocol.ExactSessionTarget63 target protocol.ExactSessionTarget
64 presentation routePresentation64 presentation routePresentation
65- dialer ports.Dialer65+ dialer ports.ClientDialer
66 request AttachRequest66 request AttachRequest
67 resumeToken uint6467 resumeToken uint64
68 home bool68 home bool
@@ -74,7 +74,7 @@ type routeRecord struct {
74 originKey string74 originKey string
75 target protocol.ExactSessionTarget75 target protocol.ExactSessionTarget
76 presentation routePresentation76 presentation routePresentation
77- dialer ports.Dialer77+ dialer ports.ClientDialer
78 request AttachRequest78 request AttachRequest
79 resumeToken uint6479 resumeToken uint64
80 home bool80 home bool
@@ -115,7 +115,7 @@ func cloneRoutePosition(position *protocol.RoutePosition) *protocol.RoutePositio
115 return &clone115 return &clone
116 }116 }
117 117
118-func routeCandidateForAttach(request AttachRequest, identity protocol.CommittedRouteIdentity, dialer ports.Dialer, resumeToken uint64) routeCandidate {118+func routeCandidateForAttach(request AttachRequest, identity protocol.CommittedRouteIdentity, dialer ports.ClientDialer, resumeToken uint64) routeCandidate {
119 origin := normalizeRouteOrigin(request.Origin, request.Remote)119 origin := normalizeRouteOrigin(request.Origin, request.Remote)
120 originKey := normalizeRouteOriginKey(request.OriginKey, origin)120 originKey := normalizeRouteOriginKey(request.OriginKey, origin)
121 kind := protocol.RouteKindLocal121 kind := protocol.RouteKindLocal
@@ -628,7 +628,7 @@ type routeTransitionPlan struct {
628 type routeTransitionCommit struct {628 type routeTransitionCommit struct {
629 identity protocol.CommittedRouteIdentity629 identity protocol.CommittedRouteIdentity
630 presentation routePresentation630 presentation routePresentation
631- dialer ports.Dialer631+ dialer ports.ClientDialer
632 resumeToken uint64632 resumeToken uint64
633 }633 }
634 634
M internal/usecase/client/session_connection.go +16 -17
@@ -10,7 +10,7 @@ import (
1010 )
1111
1212 var (
13- errNilSessionTransport = errors.New("client: nil session transport")
13+ errNilSessionConnection = errors.New("client: nil session connection")
1414 ErrEphemeralSessionName = errors.New("client: ephemeral sessions cannot have a name")
1515 )
1616
@@ -38,31 +38,30 @@ func (t SessionTarget) validate() error {
3838 return domain.ValidateSessionName(t.SessionName)
3939 }
4040
41-// SessionConnection owns one already-open transport and its daemon-facing
42-// session target. Local sockets and remote carriages use this same owner.
41+// SessionConnection owns one already-open typed connection and its
42+// daemon-facing session target. Local and remote carriages use this owner.
4343 type SessionConnection struct {
44- transport ports.Transport
45- target SessionTarget
44+ connection ports.ClientConnection
45+ target SessionTarget
4646 }
4747
48-// NewSessionConnection creates a connection for an already-selected transport.
49-// Target validation happens before the connection is used for a Hello.
50-func NewSessionConnection(transport ports.Transport, target SessionTarget) (*SessionConnection, error) {
51- if transport == nil {
52- return nil, errNilSessionTransport
48+// NewSessionConnection binds an already-selected typed connection to a target.
49+func NewSessionConnection(connection ports.ClientConnection, target SessionTarget) (*SessionConnection, error) {
50+ if connection == nil {
51+ return nil, errNilSessionConnection
5352 }
5453 if err := target.validate(); err != nil {
5554 return nil, fmt.Errorf("invalid session target: %w", err)
5655 }
57- return &SessionConnection{transport: transport, target: target}, nil
56+ return &SessionConnection{connection: connection, target: target}, nil
5857 }
5958
60-// Transport returns the owned carriage for this connection.
61-func (c *SessionConnection) Transport() ports.Transport {
59+// Connection returns the owned typed session connection.
60+func (c *SessionConnection) Connection() ports.ClientConnection {
6261 if c == nil {
6362 return nil
6463 }
65- return c.transport
64+ return c.connection
6665 }
6766
6867 // AttachRequest returns the common request shape used after transport
@@ -88,10 +87,10 @@ func cloneAttachRequest(request AttachRequest) AttachRequest {
8887 return request
8988 }
9089
91-// Close releases the owned transport.
90+// Close releases the owned connection.
9291 func (c *SessionConnection) Close() error {
93- if c == nil || c.transport == nil {
92+ if c == nil || c.connection == nil {
9493 return nil
9594 }
96- return c.transport.Close()
95+ return c.connection.Close()
9796 }
@@ -10,7 +10,7 @@ import (
10 )10 )
11 11
12 var (12 var (
13- errNilSessionTransport = errors.New("client: nil session transport")13+ errNilSessionConnection = errors.New("client: nil session connection")
14 ErrEphemeralSessionName = errors.New("client: ephemeral sessions cannot have a name")14 ErrEphemeralSessionName = errors.New("client: ephemeral sessions cannot have a name")
15 )15 )
16 16
@@ -38,31 +38,30 @@ func (t SessionTarget) validate() error {
38 return domain.ValidateSessionName(t.SessionName)38 return domain.ValidateSessionName(t.SessionName)
39 }39 }
40 40
41-// SessionConnection owns one already-open transport and its daemon-facing41+// SessionConnection owns one already-open typed connection and its
42-// session target. Local sockets and remote carriages use this same owner.42+// daemon-facing session target. Local and remote carriages use this owner.
43 type SessionConnection struct {43 type SessionConnection struct {
44- transport ports.Transport44+ connection ports.ClientConnection
45- target SessionTarget45+ target SessionTarget
46 }46 }
47 47
48-// NewSessionConnection creates a connection for an already-selected transport.48+// NewSessionConnection binds an already-selected typed connection to a target.
49-// Target validation happens before the connection is used for a Hello.49+func NewSessionConnection(connection ports.ClientConnection, target SessionTarget) (*SessionConnection, error) {
50-func NewSessionConnection(transport ports.Transport, target SessionTarget) (*SessionConnection, error) {50+ if connection == nil {
51- if transport == nil {51+ return nil, errNilSessionConnection
52- return nil, errNilSessionTransport
53 }52 }
54 if err := target.validate(); err != nil {53 if err := target.validate(); err != nil {
55 return nil, fmt.Errorf("invalid session target: %w", err)54 return nil, fmt.Errorf("invalid session target: %w", err)
56 }55 }
57- return &SessionConnection{transport: transport, target: target}, nil56+ return &SessionConnection{connection: connection, target: target}, nil
58 }57 }
59 58
60-// Transport returns the owned carriage for this connection.59+// Connection returns the owned typed session connection.
61-func (c *SessionConnection) Transport() ports.Transport {60+func (c *SessionConnection) Connection() ports.ClientConnection {
62 if c == nil {61 if c == nil {
63 return nil62 return nil
64 }63 }
65- return c.transport64+ return c.connection
66 }65 }
67 66
68 // AttachRequest returns the common request shape used after transport67 // AttachRequest returns the common request shape used after transport
@@ -88,10 +87,10 @@ func cloneAttachRequest(request AttachRequest) AttachRequest {
88 return request87 return request
89 }88 }
90 89
91-// Close releases the owned transport.90+// Close releases the owned connection.
92 func (c *SessionConnection) Close() error {91 func (c *SessionConnection) Close() error {
93- if c == nil || c.transport == nil {92+ if c == nil || c.connection == nil {
94 return nil93 return nil
95 }94 }
96- return c.transport.Close()95+ return c.connection.Close()
97 }96 }
M internal/usecase/client/session_connection_test.go +5 -6
@@ -3,7 +3,6 @@ package client_test
33 import (
44 "testing"
55
6- portsmocks "github.com/bnema/vev/internal/ports/mocks"
76 "github.com/bnema/vev/internal/protocol"
87 "github.com/bnema/vev/internal/usecase/client"
98 "github.com/stretchr/testify/require"
@@ -11,8 +10,8 @@ import (
1110
1211 func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testing.T) {
1312 target := client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "work"}
14- local := portsmocks.NewMockTransport(t)
15- remote := portsmocks.NewMockTransport(t)
13+ local := newMockClientConnection(t)
14+ remote := newMockClientConnection(t)
1615
1716 localConnection, err := client.NewSessionConnection(local, target)
1817 require.NoError(t, err)
@@ -21,8 +20,8 @@ func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testi
2120
2221 require.Equal(t, localConnection.AttachRequest(), remoteConnection.AttachRequest())
2322 require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}, localConnection.AttachRequest())
24- require.Same(t, local, localConnection.Transport())
25- require.Same(t, remote, remoteConnection.Transport())
23+ require.Same(t, local, localConnection.Connection())
24+ require.Same(t, remote, remoteConnection.Connection())
2625 }
2726
2827 func TestSessionTargetValidation(t *testing.T) {
@@ -43,7 +42,7 @@ func TestSessionTargetValidation(t *testing.T) {
4342 }
4443 for _, tt := range tests {
4544 t.Run(tt.name, func(t *testing.T) {
46- transport := portsmocks.NewMockTransport(t)
45+ transport := newMockClientConnection(t)
4746 _, err := client.NewSessionConnection(transport, tt.target)
4847 if tt.wantErr {
4948 require.Error(t, err)
@@ -3,7 +3,6 @@ package client_test
3 import (3 import (
4 "testing"4 "testing"
5 5
6- portsmocks "github.com/bnema/vev/internal/ports/mocks"
7 "github.com/bnema/vev/internal/protocol"6 "github.com/bnema/vev/internal/protocol"
8 "github.com/bnema/vev/internal/usecase/client"7 "github.com/bnema/vev/internal/usecase/client"
9 "github.com/stretchr/testify/require"8 "github.com/stretchr/testify/require"
@@ -11,8 +10,8 @@ import (
11 10
12 func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testing.T) {11 func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testing.T) {
13 target := client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "work"}12 target := client.SessionTarget{Intent: protocol.IntentAttach, SessionName: "work"}
14- local := portsmocks.NewMockTransport(t)13+ local := newMockClientConnection(t)
15- remote := portsmocks.NewMockTransport(t)14+ remote := newMockClientConnection(t)
16 15
17 localConnection, err := client.NewSessionConnection(local, target)16 localConnection, err := client.NewSessionConnection(local, target)
18 require.NoError(t, err)17 require.NoError(t, err)
@@ -21,8 +20,8 @@ func TestSessionConnectionUsesOneAttachShapeForLocalAndRemoteTransports(t *testi
21 20
22 require.Equal(t, localConnection.AttachRequest(), remoteConnection.AttachRequest())21 require.Equal(t, localConnection.AttachRequest(), remoteConnection.AttachRequest())
23 require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}, localConnection.AttachRequest())22 require.Equal(t, client.AttachRequest{Intent: protocol.IntentAttach, SessionName: "work"}, localConnection.AttachRequest())
24- require.Same(t, local, localConnection.Transport())23+ require.Same(t, local, localConnection.Connection())
25- require.Same(t, remote, remoteConnection.Transport())24+ require.Same(t, remote, remoteConnection.Connection())
26 }25 }
27 26
28 func TestSessionTargetValidation(t *testing.T) {27 func TestSessionTargetValidation(t *testing.T) {
@@ -43,7 +42,7 @@ func TestSessionTargetValidation(t *testing.T) {
43 }42 }
44 for _, tt := range tests {43 for _, tt := range tests {
45 t.Run(tt.name, func(t *testing.T) {44 t.Run(tt.name, func(t *testing.T) {
46- transport := portsmocks.NewMockTransport(t)45+ transport := newMockClientConnection(t)
47 _, err := client.NewSessionConnection(transport, tt.target)46 _, err := client.NewSessionConnection(transport, tt.target)
48 if tt.wantErr {47 if tt.wantErr {
49 require.Error(t, err)48 require.Error(t, err)
M internal/usecase/client/terminal_input_pump_test.go +30 -27
@@ -13,6 +13,7 @@ import (
1313
1414 "github.com/bnema/vev/internal/domain"
1515 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
1617 "github.com/bnema/vev/internal/protocol/wire"
1718 )
1819
@@ -82,10 +83,12 @@ func requireSignal(t *testing.T, ch <-chan struct{}, message string) {
8283 }
8384 }
8485
85-func requireFrame(t *testing.T, ch <-chan wire.Frame, message string) wire.Frame {
86+func requireFrame(t *testing.T, ch <-chan protocol.ClientMessage, message string) wire.Frame {
8687 t.Helper()
8788 select {
88- case frame := <-ch:
89+ case message := <-ch:
90+ frame, err := testClientFrame(message)
91+ require.NoError(t, err)
8992 return frame
9093 case <-time.After(time.Second):
9194 t.Fatal(message)
@@ -182,7 +185,7 @@ func TestTerminalInputPumpCancellationHandoffPreservesQueuedBytes(t *testing.T)
182185 go func() {
183186 (&stdinPump{
184187 ctx: cancelledCtx, cancel: cancelCancelled, input: input,
185- out: make(chan wire.Frame, 1), clock: newAttachPaletteClock(),
188+ out: make(chan protocol.ClientMessage, 1), clock: newAttachPaletteClock(),
186189 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
187190 activeGeneration: &activeGeneration,
188191 }).run()
@@ -192,7 +195,7 @@ func TestTerminalInputPumpCancellationHandoffPreservesQueuedBytes(t *testing.T)
192195
193196 replacementCtx, cancelReplacement := context.WithCancel(context.Background())
194197 defer cancelReplacement()
195- out := make(chan wire.Frame, len(want))
198+ out := make(chan protocol.ClientMessage, len(want))
196199 replacementDone := make(chan struct{})
197200 go func() {
198201 (&stdinPump{
@@ -206,9 +209,9 @@ func TestTerminalInputPumpCancellationHandoffPreservesQueuedBytes(t *testing.T)
206209
207210 var got []byte
208211 for len(got) < len(want) {
209- frame := <-out
210- input, err := wire.UnmarshalInput(frame.Payload)
211- require.NoError(t, err)
212+ message := <-out
213+ input, ok := message.(protocol.Input)
214+ require.True(t, ok)
212215 got = append(got, input.Data...)
213216 }
214217 require.Equal(t, want, got, "handoff must preserve every queued byte in order")
@@ -230,7 +233,7 @@ func TestTerminalInputPumpCancellationAfterTakeRequeuesRawRead(t *testing.T) {
230233 go func() {
231234 (&stdinPump{
232235 ctx: ctx, cancel: cancel, input: input, consumer: consumer,
233- out: make(chan wire.Frame, 1), clock: newAttachPaletteClock(),
236+ out: make(chan protocol.ClientMessage, 1), clock: newAttachPaletteClock(),
234237 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
235238 afterInputTake: func() {
236239 close(taken)
@@ -264,7 +267,7 @@ func TestTerminalInputPumpCancellationPreservesStandaloneEscapeForReplacement(t
264267 oldDone := make(chan struct{})
265268 go func() {
266269 (&stdinPump{
267- ctx: oldCtx, cancel: cancelOld, input: input, out: make(chan wire.Frame, 1),
270+ ctx: oldCtx, cancel: cancelOld, input: input, out: make(chan protocol.ClientMessage, 1),
268271 clock: oldClock, logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
269272 activeGeneration: &activeGeneration,
270273 }).run()
@@ -279,7 +282,7 @@ func TestTerminalInputPumpCancellationPreservesStandaloneEscapeForReplacement(t
279282 newCtx, cancelNew := context.WithCancel(context.Background())
280283 defer cancelNew()
281284 newClock := newAttachPaletteClock()
282- out := make(chan wire.Frame, 1)
285+ out := make(chan protocol.ClientMessage, 1)
283286 newDone := make(chan struct{})
284287 go func() {
285288 (&stdinPump{
@@ -291,9 +294,9 @@ func TestTerminalInputPumpCancellationPreservesStandaloneEscapeForReplacement(t
291294 }()
292295 (<-newClock.timers).fire() // marker ambiguity deadline
293296 (<-newClock.timers).fire() // paste coalescer's lone-ESC deadline
294- frame := <-out
295- got, err := wire.UnmarshalInput(frame.Payload)
296- require.NoError(t, err)
297+ message := <-out
298+ got, ok := message.(protocol.Input)
299+ require.True(t, ok)
297300 require.Equal(t, []byte("\x1b"), got.Data)
298301
299302 cancelNew()
@@ -307,7 +310,7 @@ func TestTerminalInputPumpCancellationAfterInputBeforeOSCPreservesOnlyUndelivere
307310 ctx, cancel := context.WithCancel(context.Background())
308311 delivered := make(chan struct{})
309312 done := make(chan struct{})
310- out := make(chan wire.Frame, 1)
313+ out := make(chan protocol.ClientMessage, 1)
311314 go func() {
312315 (&stdinPump{
313316 ctx: ctx, cancel: cancel, input: input, out: out, clock: newAttachPaletteClock(),
@@ -328,7 +331,7 @@ func TestTerminalInputPumpCancellationAfterInputBeforeOSCPreservesOnlyUndelivere
328331 requireSignal(t, done, "timed out waiting for cancelled stdin pump")
329332
330333 replacementCtx, cancelReplacement := context.WithCancel(context.Background())
331- replacementOut := make(chan wire.Frame, 1)
334+ replacementOut := make(chan protocol.ClientMessage, 1)
332335 replacementDone := make(chan struct{})
333336 go func() {
334337 (&stdinPump{
@@ -355,7 +358,7 @@ func TestTerminalInputPumpCancellationPreservesCoalescerHeldSuffix(t *testing.T)
355358 input.enqueue(terminalReadResult{data: raw}, 1)
356359 var activeGeneration atomic.Uint64
357360 ctx, cancel := context.WithCancel(context.Background())
358- out := make(chan wire.Frame, 1)
361+ out := make(chan protocol.ClientMessage, 1)
359362 done := make(chan struct{})
360363 go func() {
361364 (&stdinPump{
@@ -374,7 +377,7 @@ func TestTerminalInputPumpCancellationPreservesCoalescerHeldSuffix(t *testing.T)
374377
375378 replacementCtx, cancelReplacement := context.WithCancel(context.Background())
376379 replacementClock := newAttachPaletteClock()
377- replacementOut := make(chan wire.Frame, 1)
380+ replacementOut := make(chan protocol.ClientMessage, 1)
378381 replacementDone := make(chan struct{})
379382 go func() {
380383 (&stdinPump{
@@ -425,7 +428,7 @@ func TestTerminalInputPumpCancellationAfterDeliveredCallbackDoesNotReplay(t *tes
425428 ctx, cancel := context.WithCancel(context.Background())
426429 delivered := make(chan struct{})
427430 done := make(chan struct{})
428- out := make(chan wire.Frame, 1)
431+ out := make(chan protocol.ClientMessage, 1)
429432 go func() {
430433 (&stdinPump{
431434 ctx: ctx, cancel: cancel, input: input, out: out, clock: newAttachPaletteClock(),
@@ -438,15 +441,15 @@ func TestTerminalInputPumpCancellationAfterDeliveredCallbackDoesNotReplay(t *tes
438441 }).run()
439442 close(done)
440443 }()
441- frame := <-out
442- got, err := wire.UnmarshalInput(frame.Payload)
443- require.NoError(t, err)
444+ message := <-out
445+ got, ok := message.(protocol.Input)
446+ require.True(t, ok)
444447 require.Equal(t, []byte("x"), got.Data)
445448 <-delivered
446449 <-done
447450
448451 consumer := input.claim()
449- _, ok := input.take(context.Background(), consumer)
452+ _, ok = input.take(context.Background(), consumer)
450453 require.False(t, ok, "a callback already delivered to the sender must not replay after reconnect")
451454 input.revoke(consumer)
452455 }
@@ -464,7 +467,7 @@ func TestTerminalInputPumpReusesOneReaderAcrossCancelledAttempts(t *testing.T) {
464467 go func() {
465468 (&stdinPump{
466469 ctx: firstCtx, cancel: cancelFirst, input: input,
467- out: make(chan wire.Frame), clock: newAttachPaletteClock(),
470+ out: make(chan protocol.ClientMessage), clock: newAttachPaletteClock(),
468471 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
469472 activeGeneration: &activeGeneration,
470473 }).run()
@@ -475,7 +478,7 @@ func TestTerminalInputPumpReusesOneReaderAcrossCancelledAttempts(t *testing.T) {
475478
476479 secondCtx, cancelSecond := context.WithCancel(context.Background())
477480 defer cancelSecond()
478- out := make(chan wire.Frame, 1)
481+ out := make(chan protocol.ClientMessage, 1)
479482 secondDone := make(chan struct{})
480483 go func() {
481484 (&stdinPump{
@@ -488,9 +491,9 @@ func TestTerminalInputPumpReusesOneReaderAcrossCancelledAttempts(t *testing.T) {
488491 }()
489492
490493 reader.chunks <- []byte("x")
491- frame := <-out
492- got, err := wire.UnmarshalInput(frame.Payload)
493- require.NoError(t, err)
494+ message := <-out
495+ got, ok := message.(protocol.Input)
496+ require.True(t, ok)
494497 require.Equal(t, []byte("x"), got.Data)
495498 require.Equal(t, 1, reader.maxActive(), "cancelled attempts must not leave competing reads")
496499
@@ -13,6 +13,7 @@ import (
13 13
14 "github.com/bnema/vev/internal/domain"14 "github.com/bnema/vev/internal/domain"
15 "github.com/bnema/vev/internal/ports"15 "github.com/bnema/vev/internal/ports"
16+ "github.com/bnema/vev/internal/protocol"
16 "github.com/bnema/vev/internal/protocol/wire"17 "github.com/bnema/vev/internal/protocol/wire"
17 )18 )
18 19
@@ -82,10 +83,12 @@ func requireSignal(t *testing.T, ch <-chan struct{}, message string) {
82 }83 }
83 }84 }
84 85
85-func requireFrame(t *testing.T, ch <-chan wire.Frame, message string) wire.Frame {86+func requireFrame(t *testing.T, ch <-chan protocol.ClientMessage, message string) wire.Frame {
86 t.Helper()87 t.Helper()
87 select {88 select {
88- case frame := <-ch:89+ case message := <-ch:
90+ frame, err := testClientFrame(message)
91+ require.NoError(t, err)
89 return frame92 return frame
90 case <-time.After(time.Second):93 case <-time.After(time.Second):
91 t.Fatal(message)94 t.Fatal(message)
@@ -182,7 +185,7 @@ func TestTerminalInputPumpCancellationHandoffPreservesQueuedBytes(t *testing.T)
182 go func() {185 go func() {
183 (&stdinPump{186 (&stdinPump{
184 ctx: cancelledCtx, cancel: cancelCancelled, input: input,187 ctx: cancelledCtx, cancel: cancelCancelled, input: input,
185- out: make(chan wire.Frame, 1), clock: newAttachPaletteClock(),188+ out: make(chan protocol.ClientMessage, 1), clock: newAttachPaletteClock(),
186 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),189 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
187 activeGeneration: &activeGeneration,190 activeGeneration: &activeGeneration,
188 }).run()191 }).run()
@@ -192,7 +195,7 @@ func TestTerminalInputPumpCancellationHandoffPreservesQueuedBytes(t *testing.T)
192 195
193 replacementCtx, cancelReplacement := context.WithCancel(context.Background())196 replacementCtx, cancelReplacement := context.WithCancel(context.Background())
194 defer cancelReplacement()197 defer cancelReplacement()
195- out := make(chan wire.Frame, len(want))198+ out := make(chan protocol.ClientMessage, len(want))
196 replacementDone := make(chan struct{})199 replacementDone := make(chan struct{})
197 go func() {200 go func() {
198 (&stdinPump{201 (&stdinPump{
@@ -206,9 +209,9 @@ func TestTerminalInputPumpCancellationHandoffPreservesQueuedBytes(t *testing.T)
206 209
207 var got []byte210 var got []byte
208 for len(got) < len(want) {211 for len(got) < len(want) {
209- frame := <-out212+ message := <-out
210- input, err := wire.UnmarshalInput(frame.Payload)213+ input, ok := message.(protocol.Input)
211- require.NoError(t, err)214+ require.True(t, ok)
212 got = append(got, input.Data...)215 got = append(got, input.Data...)
213 }216 }
214 require.Equal(t, want, got, "handoff must preserve every queued byte in order")217 require.Equal(t, want, got, "handoff must preserve every queued byte in order")
@@ -230,7 +233,7 @@ func TestTerminalInputPumpCancellationAfterTakeRequeuesRawRead(t *testing.T) {
230 go func() {233 go func() {
231 (&stdinPump{234 (&stdinPump{
232 ctx: ctx, cancel: cancel, input: input, consumer: consumer,235 ctx: ctx, cancel: cancel, input: input, consumer: consumer,
233- out: make(chan wire.Frame, 1), clock: newAttachPaletteClock(),236+ out: make(chan protocol.ClientMessage, 1), clock: newAttachPaletteClock(),
234 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),237 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
235 afterInputTake: func() {238 afterInputTake: func() {
236 close(taken)239 close(taken)
@@ -264,7 +267,7 @@ func TestTerminalInputPumpCancellationPreservesStandaloneEscapeForReplacement(t
264 oldDone := make(chan struct{})267 oldDone := make(chan struct{})
265 go func() {268 go func() {
266 (&stdinPump{269 (&stdinPump{
267- ctx: oldCtx, cancel: cancelOld, input: input, out: make(chan wire.Frame, 1),270+ ctx: oldCtx, cancel: cancelOld, input: input, out: make(chan protocol.ClientMessage, 1),
268 clock: oldClock, logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),271 clock: oldClock, logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
269 activeGeneration: &activeGeneration,272 activeGeneration: &activeGeneration,
270 }).run()273 }).run()
@@ -279,7 +282,7 @@ func TestTerminalInputPumpCancellationPreservesStandaloneEscapeForReplacement(t
279 newCtx, cancelNew := context.WithCancel(context.Background())282 newCtx, cancelNew := context.WithCancel(context.Background())
280 defer cancelNew()283 defer cancelNew()
281 newClock := newAttachPaletteClock()284 newClock := newAttachPaletteClock()
282- out := make(chan wire.Frame, 1)285+ out := make(chan protocol.ClientMessage, 1)
283 newDone := make(chan struct{})286 newDone := make(chan struct{})
284 go func() {287 go func() {
285 (&stdinPump{288 (&stdinPump{
@@ -291,9 +294,9 @@ func TestTerminalInputPumpCancellationPreservesStandaloneEscapeForReplacement(t
291 }()294 }()
292 (<-newClock.timers).fire() // marker ambiguity deadline295 (<-newClock.timers).fire() // marker ambiguity deadline
293 (<-newClock.timers).fire() // paste coalescer's lone-ESC deadline296 (<-newClock.timers).fire() // paste coalescer's lone-ESC deadline
294- frame := <-out297+ message := <-out
295- got, err := wire.UnmarshalInput(frame.Payload)298+ got, ok := message.(protocol.Input)
296- require.NoError(t, err)299+ require.True(t, ok)
297 require.Equal(t, []byte("\x1b"), got.Data)300 require.Equal(t, []byte("\x1b"), got.Data)
298 301
299 cancelNew()302 cancelNew()
@@ -307,7 +310,7 @@ func TestTerminalInputPumpCancellationAfterInputBeforeOSCPreservesOnlyUndelivere
307 ctx, cancel := context.WithCancel(context.Background())310 ctx, cancel := context.WithCancel(context.Background())
308 delivered := make(chan struct{})311 delivered := make(chan struct{})
309 done := make(chan struct{})312 done := make(chan struct{})
310- out := make(chan wire.Frame, 1)313+ out := make(chan protocol.ClientMessage, 1)
311 go func() {314 go func() {
312 (&stdinPump{315 (&stdinPump{
313 ctx: ctx, cancel: cancel, input: input, out: out, clock: newAttachPaletteClock(),316 ctx: ctx, cancel: cancel, input: input, out: out, clock: newAttachPaletteClock(),
@@ -328,7 +331,7 @@ func TestTerminalInputPumpCancellationAfterInputBeforeOSCPreservesOnlyUndelivere
328 requireSignal(t, done, "timed out waiting for cancelled stdin pump")331 requireSignal(t, done, "timed out waiting for cancelled stdin pump")
329 332
330 replacementCtx, cancelReplacement := context.WithCancel(context.Background())333 replacementCtx, cancelReplacement := context.WithCancel(context.Background())
331- replacementOut := make(chan wire.Frame, 1)334+ replacementOut := make(chan protocol.ClientMessage, 1)
332 replacementDone := make(chan struct{})335 replacementDone := make(chan struct{})
333 go func() {336 go func() {
334 (&stdinPump{337 (&stdinPump{
@@ -355,7 +358,7 @@ func TestTerminalInputPumpCancellationPreservesCoalescerHeldSuffix(t *testing.T)
355 input.enqueue(terminalReadResult{data: raw}, 1)358 input.enqueue(terminalReadResult{data: raw}, 1)
356 var activeGeneration atomic.Uint64359 var activeGeneration atomic.Uint64
357 ctx, cancel := context.WithCancel(context.Background())360 ctx, cancel := context.WithCancel(context.Background())
358- out := make(chan wire.Frame, 1)361+ out := make(chan protocol.ClientMessage, 1)
359 done := make(chan struct{})362 done := make(chan struct{})
360 go func() {363 go func() {
361 (&stdinPump{364 (&stdinPump{
@@ -374,7 +377,7 @@ func TestTerminalInputPumpCancellationPreservesCoalescerHeldSuffix(t *testing.T)
374 377
375 replacementCtx, cancelReplacement := context.WithCancel(context.Background())378 replacementCtx, cancelReplacement := context.WithCancel(context.Background())
376 replacementClock := newAttachPaletteClock()379 replacementClock := newAttachPaletteClock()
377- replacementOut := make(chan wire.Frame, 1)380+ replacementOut := make(chan protocol.ClientMessage, 1)
378 replacementDone := make(chan struct{})381 replacementDone := make(chan struct{})
379 go func() {382 go func() {
380 (&stdinPump{383 (&stdinPump{
@@ -425,7 +428,7 @@ func TestTerminalInputPumpCancellationAfterDeliveredCallbackDoesNotReplay(t *tes
425 ctx, cancel := context.WithCancel(context.Background())428 ctx, cancel := context.WithCancel(context.Background())
426 delivered := make(chan struct{})429 delivered := make(chan struct{})
427 done := make(chan struct{})430 done := make(chan struct{})
428- out := make(chan wire.Frame, 1)431+ out := make(chan protocol.ClientMessage, 1)
429 go func() {432 go func() {
430 (&stdinPump{433 (&stdinPump{
431 ctx: ctx, cancel: cancel, input: input, out: out, clock: newAttachPaletteClock(),434 ctx: ctx, cancel: cancel, input: input, out: out, clock: newAttachPaletteClock(),
@@ -438,15 +441,15 @@ func TestTerminalInputPumpCancellationAfterDeliveredCallbackDoesNotReplay(t *tes
438 }).run()441 }).run()
439 close(done)442 close(done)
440 }()443 }()
441- frame := <-out444+ message := <-out
442- got, err := wire.UnmarshalInput(frame.Payload)445+ got, ok := message.(protocol.Input)
443- require.NoError(t, err)446+ require.True(t, ok)
444 require.Equal(t, []byte("x"), got.Data)447 require.Equal(t, []byte("x"), got.Data)
445 <-delivered448 <-delivered
446 <-done449 <-done
447 450
448 consumer := input.claim()451 consumer := input.claim()
449- _, ok := input.take(context.Background(), consumer)452+ _, ok = input.take(context.Background(), consumer)
450 require.False(t, ok, "a callback already delivered to the sender must not replay after reconnect")453 require.False(t, ok, "a callback already delivered to the sender must not replay after reconnect")
451 input.revoke(consumer)454 input.revoke(consumer)
452 }455 }
@@ -464,7 +467,7 @@ func TestTerminalInputPumpReusesOneReaderAcrossCancelledAttempts(t *testing.T) {
464 go func() {467 go func() {
465 (&stdinPump{468 (&stdinPump{
466 ctx: firstCtx, cancel: cancelFirst, input: input,469 ctx: firstCtx, cancel: cancelFirst, input: input,
467- out: make(chan wire.Frame), clock: newAttachPaletteClock(),470+ out: make(chan protocol.ClientMessage), clock: newAttachPaletteClock(),
468 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),471 logger: slog.New(slog.DiscardHandler), paletteEvents: make(chan paletteGenerationEvent),
469 activeGeneration: &activeGeneration,472 activeGeneration: &activeGeneration,
470 }).run()473 }).run()
@@ -475,7 +478,7 @@ func TestTerminalInputPumpReusesOneReaderAcrossCancelledAttempts(t *testing.T) {
475 478
476 secondCtx, cancelSecond := context.WithCancel(context.Background())479 secondCtx, cancelSecond := context.WithCancel(context.Background())
477 defer cancelSecond()480 defer cancelSecond()
478- out := make(chan wire.Frame, 1)481+ out := make(chan protocol.ClientMessage, 1)
479 secondDone := make(chan struct{})482 secondDone := make(chan struct{})
480 go func() {483 go func() {
481 (&stdinPump{484 (&stdinPump{
@@ -488,9 +491,9 @@ func TestTerminalInputPumpReusesOneReaderAcrossCancelledAttempts(t *testing.T) {
488 }()491 }()
489 492
490 reader.chunks <- []byte("x")493 reader.chunks <- []byte("x")
491- frame := <-out494+ message := <-out
492- got, err := wire.UnmarshalInput(frame.Payload)495+ got, ok := message.(protocol.Input)
493- require.NoError(t, err)496+ require.True(t, ok)
494 require.Equal(t, []byte("x"), got.Data)497 require.Equal(t, []byte("x"), got.Data)
495 require.Equal(t, 1, reader.maxActive(), "cancelled attempts must not leave competing reads")498 require.Equal(t, 1, reader.maxActive(), "cancelled attempts must not leave competing reads")
496 499
M internal/usecase/client/transport_observability_test.go +2 -2
@@ -46,7 +46,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
4646 reporter := ports.NewSerializedRuntimeObserver(observer, 1)
4747 defer reporter.Close()
4848 acked := make(chan struct{})
49- transport := portsmocks.NewMockTransport(t)
49+ transport := newMockClientConnection(t)
5050 transport.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
5151 transport.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
5252 transport.EXPECT().Send(isType(wire.MsgAck)).Run(func(wire.Frame) { close(acked) }).Return(nil).Once()
@@ -87,7 +87,7 @@ func TestTerminalFlushBoundaryTransportObservability(t *testing.T) {
8787 observer := &clientRuntimeObserver{}
8888 reporter := ports.NewSerializedRuntimeObserver(observer, 64)
8989 defer reporter.Close()
90- transport := portsmocks.NewMockTransport(t)
90+ transport := newMockClientConnection(t)
9191 transport.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
9292 transport.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
9393 unblock := scriptRecv(transport,
@@ -46,7 +46,7 @@ func TestBlockingRuntimeObserverDoesNotDelayTerminalFlushOrACK(t *testing.T) {
46 reporter := ports.NewSerializedRuntimeObserver(observer, 1)46 reporter := ports.NewSerializedRuntimeObserver(observer, 1)
47 defer reporter.Close()47 defer reporter.Close()
48 acked := make(chan struct{})48 acked := make(chan struct{})
49- transport := portsmocks.NewMockTransport(t)49+ transport := newMockClientConnection(t)
50 transport.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()50 transport.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
51 transport.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()51 transport.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
52 transport.EXPECT().Send(isType(wire.MsgAck)).Run(func(wire.Frame) { close(acked) }).Return(nil).Once()52 transport.EXPECT().Send(isType(wire.MsgAck)).Run(func(wire.Frame) { close(acked) }).Return(nil).Once()
@@ -87,7 +87,7 @@ func TestTerminalFlushBoundaryTransportObservability(t *testing.T) {
87 observer := &clientRuntimeObserver{}87 observer := &clientRuntimeObserver{}
88 reporter := ports.NewSerializedRuntimeObserver(observer, 64)88 reporter := ports.NewSerializedRuntimeObserver(observer, 64)
89 defer reporter.Close()89 defer reporter.Close()
90- transport := portsmocks.NewMockTransport(t)90+ transport := newMockClientConnection(t)
91 transport.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()91 transport.EXPECT().Send(isType(wire.MsgHello)).Return(nil).Once()
92 transport.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()92 transport.EXPECT().Send(isType(wire.MsgTheme)).Return(nil).Maybe()
93 unblock := scriptRecv(transport,93 unblock := scriptRecv(transport,
A internal/usecase/client/typed_connection_external_test.go +232 -0
@@ -0,0 +1,232 @@
1+package client_test
2+
3+import (
4+ "context"
5+ "errors"
6+
7+ "github.com/stretchr/testify/mock"
8+
9+ "github.com/bnema/vev/internal/ports"
10+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
11+ "github.com/bnema/vev/internal/protocol"
12+ "github.com/bnema/vev/internal/protocol/wire"
13+)
14+
15+type mockTestingT interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}
19+
20+type mockClientConnection struct{ *portsmocks.MockTransport }
21+
22+func newMockClientConnection(t mockTestingT) *mockClientConnection {
23+ return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)}
24+}
25+func (c *mockClientConnection) SendClient(m protocol.ClientMessage) error {
26+ f, e := externalClientFrame(m)
27+ if e != nil {
28+ return e
29+ }
30+ return c.Send(f)
31+}
32+func (c *mockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
33+ f, e := c.Recv()
34+ if e != nil {
35+ return nil, e
36+ }
37+ return externalServerMessage(f)
38+}
39+func (c *mockClientConnection) Capabilities() protocol.ConnectionCapabilities {
40+ return externalCapabilities(c.MockTransport)
41+}
42+func (c *mockClientConnection) LinkState() ports.LinkState { return ports.LinkStateConnected }
43+func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent { return nil }
44+
45+type rawClientConnection struct{ raw ports.Transport }
46+
47+func (c *rawClientConnection) SendClient(m protocol.ClientMessage) error {
48+ f, e := externalClientFrame(m)
49+ if e != nil {
50+ return e
51+ }
52+ return c.raw.Send(f)
53+}
54+func (c *rawClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
55+ f, e := c.raw.Recv()
56+ if e != nil {
57+ return nil, e
58+ }
59+ return externalServerMessage(f)
60+}
61+func (c *rawClientConnection) Capabilities() protocol.ConnectionCapabilities {
62+ return externalCapabilities(c.raw)
63+}
64+func (c *rawClientConnection) LinkState() ports.LinkState {
65+ if r, ok := c.raw.(ports.LinkStateReporter); ok {
66+ return r.LinkState()
67+ }
68+ return ports.LinkStateConnected
69+}
70+func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent {
71+ if r, ok := c.raw.(ports.LinkStateReporter); ok {
72+ return r.LinkEvents()
73+ }
74+ return nil
75+}
76+func (c *rawClientConnection) Close() error { return c.raw.Close() }
77+
78+type mockClientDialer struct{ *portsmocks.MockDialer }
79+
80+func newMockClientDialer(t mockTestingT) *mockClientDialer {
81+ return &mockClientDialer{MockDialer: portsmocks.NewMockDialer(t)}
82+}
83+func (d *mockClientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
84+ raw, e := d.MockDialer.Dial(ctx)
85+ if e != nil {
86+ return nil, e
87+ }
88+ return &rawClientConnection{raw: raw}, nil
89+}
90+
91+func externalCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
92+ _, d := raw.(ports.DatagramTransport)
93+ window := uint8(8)
94+ if d {
95+ window = 1
96+ }
97+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window}
98+}
99+func externalClientFrame(m protocol.ClientMessage) (wire.Frame, error) {
100+ switch x := m.(type) {
101+ case protocol.Hello:
102+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(x)}, nil
103+ case protocol.Input:
104+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(x)}, nil
105+ case protocol.Resize:
106+ p, e := wire.MarshalResize(x)
107+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
108+ case protocol.Detach:
109+ return wire.Frame{Type: wire.MsgDetach}, nil
110+ case protocol.Theme:
111+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(x)}, nil
112+ case protocol.Ack:
113+ p, e := wire.MarshalAck(x)
114+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
115+ case protocol.ImagePush:
116+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(x)}, nil
117+ case protocol.ClientNotice:
118+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(x)}, nil
119+ case protocol.OutputResetRequest:
120+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
121+ case protocol.RecentRouteSnapshot:
122+ p, e := wire.MarshalRecentRouteSnapshot(x)
123+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
124+ case protocol.RouteAttentionSubscription:
125+ p, e := wire.MarshalRouteAttentionSubscription(x)
126+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
127+ case protocol.SamePeerSwitchRequest:
128+ p, e := wire.MarshalSamePeerSwitchRequest(x)
129+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
130+ case protocol.ParkedRouteRequest:
131+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(x)}, nil
132+ case protocol.RouteNavigationFailure:
133+ p, e := wire.MarshalRouteNavigationFailure(x)
134+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
135+ default:
136+ return wire.Frame{}, errors.New("test client: unsupported message")
137+ }
138+}
139+func externalServerMessage(f wire.Frame) (protocol.ServerMessage, error) {
140+ switch f.Type {
141+ case wire.MsgWelcome:
142+ return wire.UnmarshalWelcome(f.Payload)
143+ case wire.MsgError:
144+ return wire.UnmarshalErrorMsg(f.Payload)
145+ case wire.MsgOutput:
146+ return wire.UnmarshalOutput(f.Payload)
147+ case wire.MsgDetached:
148+ return wire.UnmarshalDetached(f.Payload)
149+ case wire.MsgPong:
150+ return wire.UnmarshalPong(f.Payload)
151+ case wire.MsgAttachTarget:
152+ return wire.UnmarshalAttachTarget(f.Payload)
153+ case wire.MsgNavigationAction:
154+ return wire.UnmarshalNavigationDirective(f.Payload)
155+ case wire.MsgParkedRouteResponse:
156+ return wire.UnmarshalParkedRouteResponse(f.Payload)
157+ case wire.MsgNavigateRecentRoute:
158+ return wire.UnmarshalRouteNavigationAction(f.Payload)
159+ case wire.MsgCommittedRouteIdentity:
160+ return wire.UnmarshalCommittedRouteIdentity(f.Payload)
161+ case wire.MsgSamePeerSwitchFailure:
162+ return wire.UnmarshalSamePeerSwitchFailure(f.Payload)
163+ case wire.MsgRoutePosition:
164+ return wire.UnmarshalRoutePosition(f.Payload)
165+ case wire.MsgRouteNavigationFailure:
166+ return wire.UnmarshalRouteNavigationFailure(f.Payload)
167+ default:
168+ return nil, errors.New("test client: unsupported server frame")
169+ }
170+}
171+
172+func (t *markedDatagramTransport) SendClient(m protocol.ClientMessage) error {
173+ f, e := externalClientFrame(m)
174+ if e != nil {
175+ return e
176+ }
177+ return t.Send(f)
178+}
179+func (t *markedDatagramTransport) ReceiveServer() (protocol.ServerMessage, error) {
180+ f, e := t.Recv()
181+ if e != nil {
182+ return nil, e
183+ }
184+ return externalServerMessage(f)
185+}
186+func (t *markedDatagramTransport) Capabilities() protocol.ConnectionCapabilities {
187+ return externalCapabilities(t)
188+}
189+func (t *markedDatagramTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
190+func (t *markedDatagramTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
191+
192+func (t *recordingTransport) SendClient(m protocol.ClientMessage) error {
193+ f, e := externalClientFrame(m)
194+ if e != nil {
195+ return e
196+ }
197+ return t.Send(f)
198+}
199+func (t *recordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
200+ f, e := t.Recv()
201+ if e != nil {
202+ return nil, e
203+ }
204+ return externalServerMessage(f)
205+}
206+func (t *recordingTransport) Capabilities() protocol.ConnectionCapabilities {
207+ return externalCapabilities(t)
208+}
209+func (t *recordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
210+func (t *recordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
211+
212+func (t *clipboardToastLifecycleTransport) SendClient(m protocol.ClientMessage) error {
213+ f, e := externalClientFrame(m)
214+ if e != nil {
215+ return e
216+ }
217+ return t.Send(f)
218+}
219+func (t *clipboardToastLifecycleTransport) ReceiveServer() (protocol.ServerMessage, error) {
220+ f, e := t.Recv()
221+ if e != nil {
222+ return nil, e
223+ }
224+ return externalServerMessage(f)
225+}
226+func (t *clipboardToastLifecycleTransport) Capabilities() protocol.ConnectionCapabilities {
227+ return externalCapabilities(t)
228+}
229+func (t *clipboardToastLifecycleTransport) LinkState() ports.LinkState {
230+ return ports.LinkStateConnected
231+}
232+func (t *clipboardToastLifecycleTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
@@ -0,0 +1,232 @@
1+package client_test
2+
3+import (
4+ "context"
5+ "errors"
6+
7+ "github.com/stretchr/testify/mock"
8+
9+ "github.com/bnema/vev/internal/ports"
10+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
11+ "github.com/bnema/vev/internal/protocol"
12+ "github.com/bnema/vev/internal/protocol/wire"
13+)
14+
15+type mockTestingT interface {
16+ mock.TestingT
17+ Cleanup(func())
18+}
19+
20+type mockClientConnection struct{ *portsmocks.MockTransport }
21+
22+func newMockClientConnection(t mockTestingT) *mockClientConnection {
23+ return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)}
24+}
25+func (c *mockClientConnection) SendClient(m protocol.ClientMessage) error {
26+ f, e := externalClientFrame(m)
27+ if e != nil {
28+ return e
29+ }
30+ return c.Send(f)
31+}
32+func (c *mockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
33+ f, e := c.Recv()
34+ if e != nil {
35+ return nil, e
36+ }
37+ return externalServerMessage(f)
38+}
39+func (c *mockClientConnection) Capabilities() protocol.ConnectionCapabilities {
40+ return externalCapabilities(c.MockTransport)
41+}
42+func (c *mockClientConnection) LinkState() ports.LinkState { return ports.LinkStateConnected }
43+func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent { return nil }
44+
45+type rawClientConnection struct{ raw ports.Transport }
46+
47+func (c *rawClientConnection) SendClient(m protocol.ClientMessage) error {
48+ f, e := externalClientFrame(m)
49+ if e != nil {
50+ return e
51+ }
52+ return c.raw.Send(f)
53+}
54+func (c *rawClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
55+ f, e := c.raw.Recv()
56+ if e != nil {
57+ return nil, e
58+ }
59+ return externalServerMessage(f)
60+}
61+func (c *rawClientConnection) Capabilities() protocol.ConnectionCapabilities {
62+ return externalCapabilities(c.raw)
63+}
64+func (c *rawClientConnection) LinkState() ports.LinkState {
65+ if r, ok := c.raw.(ports.LinkStateReporter); ok {
66+ return r.LinkState()
67+ }
68+ return ports.LinkStateConnected
69+}
70+func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent {
71+ if r, ok := c.raw.(ports.LinkStateReporter); ok {
72+ return r.LinkEvents()
73+ }
74+ return nil
75+}
76+func (c *rawClientConnection) Close() error { return c.raw.Close() }
77+
78+type mockClientDialer struct{ *portsmocks.MockDialer }
79+
80+func newMockClientDialer(t mockTestingT) *mockClientDialer {
81+ return &mockClientDialer{MockDialer: portsmocks.NewMockDialer(t)}
82+}
83+func (d *mockClientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
84+ raw, e := d.MockDialer.Dial(ctx)
85+ if e != nil {
86+ return nil, e
87+ }
88+ return &rawClientConnection{raw: raw}, nil
89+}
90+
91+func externalCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
92+ _, d := raw.(ports.DatagramTransport)
93+ window := uint8(8)
94+ if d {
95+ window = 1
96+ }
97+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window}
98+}
99+func externalClientFrame(m protocol.ClientMessage) (wire.Frame, error) {
100+ switch x := m.(type) {
101+ case protocol.Hello:
102+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(x)}, nil
103+ case protocol.Input:
104+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(x)}, nil
105+ case protocol.Resize:
106+ p, e := wire.MarshalResize(x)
107+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
108+ case protocol.Detach:
109+ return wire.Frame{Type: wire.MsgDetach}, nil
110+ case protocol.Theme:
111+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(x)}, nil
112+ case protocol.Ack:
113+ p, e := wire.MarshalAck(x)
114+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
115+ case protocol.ImagePush:
116+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(x)}, nil
117+ case protocol.ClientNotice:
118+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(x)}, nil
119+ case protocol.OutputResetRequest:
120+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
121+ case protocol.RecentRouteSnapshot:
122+ p, e := wire.MarshalRecentRouteSnapshot(x)
123+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
124+ case protocol.RouteAttentionSubscription:
125+ p, e := wire.MarshalRouteAttentionSubscription(x)
126+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
127+ case protocol.SamePeerSwitchRequest:
128+ p, e := wire.MarshalSamePeerSwitchRequest(x)
129+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
130+ case protocol.ParkedRouteRequest:
131+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(x)}, nil
132+ case protocol.RouteNavigationFailure:
133+ p, e := wire.MarshalRouteNavigationFailure(x)
134+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
135+ default:
136+ return wire.Frame{}, errors.New("test client: unsupported message")
137+ }
138+}
139+func externalServerMessage(f wire.Frame) (protocol.ServerMessage, error) {
140+ switch f.Type {
141+ case wire.MsgWelcome:
142+ return wire.UnmarshalWelcome(f.Payload)
143+ case wire.MsgError:
144+ return wire.UnmarshalErrorMsg(f.Payload)
145+ case wire.MsgOutput:
146+ return wire.UnmarshalOutput(f.Payload)
147+ case wire.MsgDetached:
148+ return wire.UnmarshalDetached(f.Payload)
149+ case wire.MsgPong:
150+ return wire.UnmarshalPong(f.Payload)
151+ case wire.MsgAttachTarget:
152+ return wire.UnmarshalAttachTarget(f.Payload)
153+ case wire.MsgNavigationAction:
154+ return wire.UnmarshalNavigationDirective(f.Payload)
155+ case wire.MsgParkedRouteResponse:
156+ return wire.UnmarshalParkedRouteResponse(f.Payload)
157+ case wire.MsgNavigateRecentRoute:
158+ return wire.UnmarshalRouteNavigationAction(f.Payload)
159+ case wire.MsgCommittedRouteIdentity:
160+ return wire.UnmarshalCommittedRouteIdentity(f.Payload)
161+ case wire.MsgSamePeerSwitchFailure:
162+ return wire.UnmarshalSamePeerSwitchFailure(f.Payload)
163+ case wire.MsgRoutePosition:
164+ return wire.UnmarshalRoutePosition(f.Payload)
165+ case wire.MsgRouteNavigationFailure:
166+ return wire.UnmarshalRouteNavigationFailure(f.Payload)
167+ default:
168+ return nil, errors.New("test client: unsupported server frame")
169+ }
170+}
171+
172+func (t *markedDatagramTransport) SendClient(m protocol.ClientMessage) error {
173+ f, e := externalClientFrame(m)
174+ if e != nil {
175+ return e
176+ }
177+ return t.Send(f)
178+}
179+func (t *markedDatagramTransport) ReceiveServer() (protocol.ServerMessage, error) {
180+ f, e := t.Recv()
181+ if e != nil {
182+ return nil, e
183+ }
184+ return externalServerMessage(f)
185+}
186+func (t *markedDatagramTransport) Capabilities() protocol.ConnectionCapabilities {
187+ return externalCapabilities(t)
188+}
189+func (t *markedDatagramTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
190+func (t *markedDatagramTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
191+
192+func (t *recordingTransport) SendClient(m protocol.ClientMessage) error {
193+ f, e := externalClientFrame(m)
194+ if e != nil {
195+ return e
196+ }
197+ return t.Send(f)
198+}
199+func (t *recordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
200+ f, e := t.Recv()
201+ if e != nil {
202+ return nil, e
203+ }
204+ return externalServerMessage(f)
205+}
206+func (t *recordingTransport) Capabilities() protocol.ConnectionCapabilities {
207+ return externalCapabilities(t)
208+}
209+func (t *recordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
210+func (t *recordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
211+
212+func (t *clipboardToastLifecycleTransport) SendClient(m protocol.ClientMessage) error {
213+ f, e := externalClientFrame(m)
214+ if e != nil {
215+ return e
216+ }
217+ return t.Send(f)
218+}
219+func (t *clipboardToastLifecycleTransport) ReceiveServer() (protocol.ServerMessage, error) {
220+ f, e := t.Recv()
221+ if e != nil {
222+ return nil, e
223+ }
224+ return externalServerMessage(f)
225+}
226+func (t *clipboardToastLifecycleTransport) Capabilities() protocol.ConnectionCapabilities {
227+ return externalCapabilities(t)
228+}
229+func (t *clipboardToastLifecycleTransport) LinkState() ports.LinkState {
230+ return ports.LinkStateConnected
231+}
232+func (t *clipboardToastLifecycleTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
A internal/usecase/client/typed_connection_test.go +365 -0
@@ -0,0 +1,365 @@
1+package client
2+
3+import (
4+ "context"
5+ "errors"
6+ "io"
7+ "log/slog"
8+ "testing"
9+
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+
13+ "github.com/bnema/vev/internal/ports"
14+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
15+ "github.com/bnema/vev/internal/protocol"
16+ "github.com/bnema/vev/internal/protocol/wire"
17+)
18+
19+type mockTestingT interface {
20+ mock.TestingT
21+ Cleanup(func())
22+}
23+
24+type mockClientConnection struct{ *portsmocks.MockTransport }
25+
26+func newMockClientConnection(t mockTestingT) *mockClientConnection {
27+ return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)}
28+}
29+
30+func (c *mockClientConnection) SendClient(message protocol.ClientMessage) error {
31+ frame, err := testClientFrame(message)
32+ if err != nil {
33+ return err
34+ }
35+ return c.MockTransport.Send(frame)
36+}
37+func (c *mockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
38+ frame, err := c.MockTransport.Recv()
39+ if err != nil {
40+ return nil, err
41+ }
42+ return testServerMessage(frame)
43+}
44+func (c *mockClientConnection) Capabilities() protocol.ConnectionCapabilities {
45+ return testClientCapabilities(c.MockTransport)
46+}
47+func (c *mockClientConnection) LinkState() ports.LinkState {
48+ return testClientLinkState(c.MockTransport)
49+}
50+func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent {
51+ return testClientLinkEvents(c.MockTransport)
52+}
53+
54+type rawClientConnection struct{ raw ports.Transport }
55+
56+func (c *rawClientConnection) SendClient(message protocol.ClientMessage) error {
57+ frame, err := testClientFrame(message)
58+ if err != nil {
59+ return err
60+ }
61+ return c.raw.Send(frame)
62+}
63+func (c *rawClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
64+ frame, err := c.raw.Recv()
65+ if err != nil {
66+ return nil, err
67+ }
68+ return testServerMessage(frame)
69+}
70+func (c *rawClientConnection) Capabilities() protocol.ConnectionCapabilities {
71+ return testClientCapabilities(c.raw)
72+}
73+func (c *rawClientConnection) LinkState() ports.LinkState { return testClientLinkState(c.raw) }
74+func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent { return testClientLinkEvents(c.raw) }
75+func (c *rawClientConnection) Close() error { return c.raw.Close() }
76+
77+type mockClientDialer struct{ *portsmocks.MockDialer }
78+
79+func newMockClientDialer(t mockTestingT) *mockClientDialer {
80+ return &mockClientDialer{MockDialer: portsmocks.NewMockDialer(t)}
81+}
82+func (d *mockClientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
83+ raw, err := d.MockDialer.Dial(ctx)
84+ if err != nil {
85+ return nil, err
86+ }
87+ return &rawClientConnection{raw: raw}, nil
88+}
89+
90+func testClientFrame(message protocol.ClientMessage) (wire.Frame, error) {
91+ switch m := message.(type) {
92+ case protocol.Hello:
93+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(m)}, nil
94+ case protocol.Input:
95+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(m)}, nil
96+ case protocol.Resize:
97+ p, e := wire.MarshalResize(m)
98+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
99+ case protocol.Detach:
100+ return wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(m)}, nil
101+ case protocol.Ping:
102+ return wire.Frame{Type: wire.MsgPing, Payload: wire.MarshalPing(m)}, nil
103+ case protocol.List:
104+ return wire.Frame{Type: wire.MsgList, Payload: wire.MarshalList(m)}, nil
105+ case protocol.Kill:
106+ return wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(m)}, nil
107+ case protocol.Theme:
108+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(m)}, nil
109+ case protocol.Ack:
110+ p, e := wire.MarshalAck(m)
111+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
112+ case protocol.ImagePush:
113+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(m)}, nil
114+ case protocol.ClientNotice:
115+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(m)}, nil
116+ case protocol.CommandRequest:
117+ p, e := wire.MarshalCommandRequest(m)
118+ return wire.Frame{Type: wire.MsgCommand, Payload: p}, e
119+ case protocol.OutputResetRequest:
120+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
121+ case protocol.RemotePreviewRequest:
122+ return wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: wire.MarshalRemotePreviewRequest(m)}, nil
123+ case protocol.RouteAttentionSubscription:
124+ p, e := wire.MarshalRouteAttentionSubscription(m)
125+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
126+ case protocol.SamePeerSwitchRequest:
127+ p, e := wire.MarshalSamePeerSwitchRequest(m)
128+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
129+ case protocol.ParkedRouteRequest:
130+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(m)}, nil
131+ case protocol.RecentRouteSnapshot:
132+ p, e := wire.MarshalRecentRouteSnapshot(m)
133+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
134+ case protocol.RouteNavigationFailure:
135+ p, e := wire.MarshalRouteNavigationFailure(m)
136+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
137+ default:
138+ return wire.Frame{}, errors.New("test client connection: unsupported client message")
139+ }
140+}
141+
142+func testServerMessage(frame wire.Frame) (protocol.ServerMessage, error) {
143+ switch frame.Type {
144+ case wire.MsgWelcome:
145+ return wire.UnmarshalWelcome(frame.Payload)
146+ case wire.MsgError:
147+ return wire.UnmarshalErrorMsg(frame.Payload)
148+ case wire.MsgOutput:
149+ return wire.UnmarshalOutput(frame.Payload)
150+ case wire.MsgDetached:
151+ return wire.UnmarshalDetached(frame.Payload)
152+ case wire.MsgPong:
153+ return wire.UnmarshalPong(frame.Payload)
154+ case wire.MsgSessions:
155+ return wire.UnmarshalSessions(frame.Payload)
156+ case wire.MsgCommandResult:
157+ return wire.UnmarshalCommandResult(frame.Payload)
158+ case wire.MsgNavigationAction:
159+ return wire.UnmarshalNavigationDirective(frame.Payload)
160+ case wire.MsgAttachTarget:
161+ return wire.UnmarshalAttachTarget(frame.Payload)
162+ case wire.MsgRemotePreviewResponse:
163+ return wire.UnmarshalRemotePreview(frame.Payload)
164+ case wire.MsgCommittedRouteIdentity:
165+ return wire.UnmarshalCommittedRouteIdentity(frame.Payload)
166+ case wire.MsgNavigateRecentRoute:
167+ return wire.UnmarshalRouteNavigationAction(frame.Payload)
168+ case wire.MsgRouteNavigationFailure:
169+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
170+ case wire.MsgRoutePosition:
171+ return wire.UnmarshalRoutePosition(frame.Payload)
172+ case wire.MsgSamePeerSwitchFailure:
173+ return wire.UnmarshalSamePeerSwitchFailure(frame.Payload)
174+ case wire.MsgParkedRouteResponse:
175+ return wire.UnmarshalParkedRouteResponse(frame.Payload)
176+ default:
177+ return nil, errors.New("test client connection: unsupported server frame")
178+ }
179+}
180+
181+func testClientCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
182+ _, dgram := raw.(ports.DatagramTransport)
183+ _, link := raw.(ports.LinkStateReporter)
184+ window := uint8(8)
185+ if dgram {
186+ window = 1
187+ }
188+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, LinkState: link}
189+}
190+func testClientLinkState(raw ports.Transport) ports.LinkState {
191+ if r, ok := raw.(ports.LinkStateReporter); ok {
192+ return r.LinkState()
193+ }
194+ return ports.LinkStateConnected
195+}
196+func testClientLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
197+ if r, ok := raw.(ports.LinkStateReporter); ok {
198+ return r.LinkEvents()
199+ }
200+ return nil
201+}
202+
203+func TestRunRecvIgnoresUnknownTypedBoundaryFailures(t *testing.T) {
204+ connection := portsmocks.NewMockClientConnection(t)
205+ connection.EXPECT().ReceiveServer().Return(nil, &protocol.DecodeFailure{Category: protocol.DecodeUnknownType}).Once()
206+ connection.EXPECT().ReceiveServer().Return(protocol.Pong{}, nil).Once()
207+ connection.EXPECT().ReceiveServer().Return(nil, io.EOF).Once()
208+ results := make(chan recvResult, 2)
209+ failed := make(chan struct{})
210+ runRecv(context.Background(), connection, results, failed, slog.New(slog.DiscardHandler))
211+ first := <-results
212+ require.Equal(t, protocol.Pong{}, first.message)
213+ require.NoError(t, first.err)
214+ second := <-results
215+ require.ErrorIs(t, second.err, io.EOF)
216+ select {
217+ case <-failed:
218+ default:
219+ t.Fatal("terminal receive failure was not reported")
220+ }
221+}
222+
223+func (t *ackRecordingTransport) SendClient(m protocol.ClientMessage) error {
224+ f, e := testClientFrame(m)
225+ if e != nil {
226+ return e
227+ }
228+ return t.Send(f)
229+}
230+func (t *ackRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
231+ f, e := t.Recv()
232+ if e != nil {
233+ return nil, e
234+ }
235+ return testServerMessage(f)
236+}
237+func (t *ackRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
238+ return testClientCapabilities(t)
239+}
240+func (t *ackRecordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
241+func (t *ackRecordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
242+
243+func (t *reconnectToastLinkTransport) SendClient(m protocol.ClientMessage) error {
244+ f, e := testClientFrame(m)
245+ if e != nil {
246+ return e
247+ }
248+ return t.Send(f)
249+}
250+func (t *reconnectToastLinkTransport) ReceiveServer() (protocol.ServerMessage, error) {
251+ f, e := t.Recv()
252+ if e != nil {
253+ return nil, e
254+ }
255+ return testServerMessage(f)
256+}
257+func (t *reconnectToastLinkTransport) Capabilities() protocol.ConnectionCapabilities {
258+ return testClientCapabilities(t)
259+}
260+
261+func (t *reconnectToastRecordingTransport) SendClient(m protocol.ClientMessage) error {
262+ f, e := testClientFrame(m)
263+ if e != nil {
264+ return e
265+ }
266+ return t.Send(f)
267+}
268+func (t *reconnectToastRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
269+ f, e := t.Recv()
270+ if e != nil {
271+ return nil, e
272+ }
273+ return testServerMessage(f)
274+}
275+func (t *reconnectToastRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
276+ return testClientCapabilities(t)
277+}
278+func (t *reconnectToastRecordingTransport) LinkState() ports.LinkState {
279+ return ports.LinkStateConnected
280+}
281+func (t *reconnectToastRecordingTransport) LinkEvents() <-chan ports.LinkEvent {
282+ return nil
283+}
284+
285+func (t *reconnectResetBlockingTransport) SendClient(m protocol.ClientMessage) error {
286+ f, e := testClientFrame(m)
287+ if e != nil {
288+ return e
289+ }
290+ return t.Send(f)
291+}
292+func (t *reconnectResetBlockingTransport) ReceiveServer() (protocol.ServerMessage, error) {
293+ f, e := t.Recv()
294+ if e != nil {
295+ return nil, e
296+ }
297+ return testServerMessage(f)
298+}
299+func (t *reconnectResetBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
300+ return testClientCapabilities(t)
301+}
302+func (t *reconnectResetBlockingTransport) LinkState() ports.LinkState {
303+ return ports.LinkStateConnected
304+}
305+func (t *reconnectResetBlockingTransport) LinkEvents() <-chan ports.LinkEvent {
306+ return nil
307+}
308+
309+func (t *reconnectToastBlockingSendTransport) SendClient(m protocol.ClientMessage) error {
310+ f, e := testClientFrame(m)
311+ if e != nil {
312+ return e
313+ }
314+ return t.Send(f)
315+}
316+func (t *reconnectToastBlockingSendTransport) ReceiveServer() (protocol.ServerMessage, error) {
317+ f, e := t.Recv()
318+ if e != nil {
319+ return nil, e
320+ }
321+ return testServerMessage(f)
322+}
323+func (t *reconnectToastBlockingSendTransport) Capabilities() protocol.ConnectionCapabilities {
324+ return testClientCapabilities(t)
325+}
326+
327+func (t *paletteAttachTransport) SendClient(m protocol.ClientMessage) error {
328+ f, e := testClientFrame(m)
329+ if e != nil {
330+ return e
331+ }
332+ return t.Send(f)
333+}
334+func (t *paletteAttachTransport) ReceiveServer() (protocol.ServerMessage, error) {
335+ f, e := t.Recv()
336+ if e != nil {
337+ return nil, e
338+ }
339+ return testServerMessage(f)
340+}
341+func (t *paletteAttachTransport) Capabilities() protocol.ConnectionCapabilities {
342+ return testClientCapabilities(t)
343+}
344+func (t *paletteAttachTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
345+func (t *paletteAttachTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
346+
347+func (t *attachPaletteTransport) SendClient(m protocol.ClientMessage) error {
348+ f, e := testClientFrame(m)
349+ if e != nil {
350+ return e
351+ }
352+ return t.Send(f)
353+}
354+func (t *attachPaletteTransport) ReceiveServer() (protocol.ServerMessage, error) {
355+ f, e := t.Recv()
356+ if e != nil {
357+ return nil, e
358+ }
359+ return testServerMessage(f)
360+}
361+func (t *attachPaletteTransport) Capabilities() protocol.ConnectionCapabilities {
362+ return testClientCapabilities(t)
363+}
364+func (t *attachPaletteTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
365+func (t *attachPaletteTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
@@ -0,0 +1,365 @@
1+package client
2+
3+import (
4+ "context"
5+ "errors"
6+ "io"
7+ "log/slog"
8+ "testing"
9+
10+ "github.com/stretchr/testify/mock"
11+ "github.com/stretchr/testify/require"
12+
13+ "github.com/bnema/vev/internal/ports"
14+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
15+ "github.com/bnema/vev/internal/protocol"
16+ "github.com/bnema/vev/internal/protocol/wire"
17+)
18+
19+type mockTestingT interface {
20+ mock.TestingT
21+ Cleanup(func())
22+}
23+
24+type mockClientConnection struct{ *portsmocks.MockTransport }
25+
26+func newMockClientConnection(t mockTestingT) *mockClientConnection {
27+ return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)}
28+}
29+
30+func (c *mockClientConnection) SendClient(message protocol.ClientMessage) error {
31+ frame, err := testClientFrame(message)
32+ if err != nil {
33+ return err
34+ }
35+ return c.MockTransport.Send(frame)
36+}
37+func (c *mockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
38+ frame, err := c.MockTransport.Recv()
39+ if err != nil {
40+ return nil, err
41+ }
42+ return testServerMessage(frame)
43+}
44+func (c *mockClientConnection) Capabilities() protocol.ConnectionCapabilities {
45+ return testClientCapabilities(c.MockTransport)
46+}
47+func (c *mockClientConnection) LinkState() ports.LinkState {
48+ return testClientLinkState(c.MockTransport)
49+}
50+func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent {
51+ return testClientLinkEvents(c.MockTransport)
52+}
53+
54+type rawClientConnection struct{ raw ports.Transport }
55+
56+func (c *rawClientConnection) SendClient(message protocol.ClientMessage) error {
57+ frame, err := testClientFrame(message)
58+ if err != nil {
59+ return err
60+ }
61+ return c.raw.Send(frame)
62+}
63+func (c *rawClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
64+ frame, err := c.raw.Recv()
65+ if err != nil {
66+ return nil, err
67+ }
68+ return testServerMessage(frame)
69+}
70+func (c *rawClientConnection) Capabilities() protocol.ConnectionCapabilities {
71+ return testClientCapabilities(c.raw)
72+}
73+func (c *rawClientConnection) LinkState() ports.LinkState { return testClientLinkState(c.raw) }
74+func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent { return testClientLinkEvents(c.raw) }
75+func (c *rawClientConnection) Close() error { return c.raw.Close() }
76+
77+type mockClientDialer struct{ *portsmocks.MockDialer }
78+
79+func newMockClientDialer(t mockTestingT) *mockClientDialer {
80+ return &mockClientDialer{MockDialer: portsmocks.NewMockDialer(t)}
81+}
82+func (d *mockClientDialer) Dial(ctx context.Context) (ports.ClientConnection, error) {
83+ raw, err := d.MockDialer.Dial(ctx)
84+ if err != nil {
85+ return nil, err
86+ }
87+ return &rawClientConnection{raw: raw}, nil
88+}
89+
90+func testClientFrame(message protocol.ClientMessage) (wire.Frame, error) {
91+ switch m := message.(type) {
92+ case protocol.Hello:
93+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(m)}, nil
94+ case protocol.Input:
95+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(m)}, nil
96+ case protocol.Resize:
97+ p, e := wire.MarshalResize(m)
98+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
99+ case protocol.Detach:
100+ return wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(m)}, nil
101+ case protocol.Ping:
102+ return wire.Frame{Type: wire.MsgPing, Payload: wire.MarshalPing(m)}, nil
103+ case protocol.List:
104+ return wire.Frame{Type: wire.MsgList, Payload: wire.MarshalList(m)}, nil
105+ case protocol.Kill:
106+ return wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(m)}, nil
107+ case protocol.Theme:
108+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(m)}, nil
109+ case protocol.Ack:
110+ p, e := wire.MarshalAck(m)
111+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
112+ case protocol.ImagePush:
113+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(m)}, nil
114+ case protocol.ClientNotice:
115+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(m)}, nil
116+ case protocol.CommandRequest:
117+ p, e := wire.MarshalCommandRequest(m)
118+ return wire.Frame{Type: wire.MsgCommand, Payload: p}, e
119+ case protocol.OutputResetRequest:
120+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
121+ case protocol.RemotePreviewRequest:
122+ return wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: wire.MarshalRemotePreviewRequest(m)}, nil
123+ case protocol.RouteAttentionSubscription:
124+ p, e := wire.MarshalRouteAttentionSubscription(m)
125+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
126+ case protocol.SamePeerSwitchRequest:
127+ p, e := wire.MarshalSamePeerSwitchRequest(m)
128+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
129+ case protocol.ParkedRouteRequest:
130+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(m)}, nil
131+ case protocol.RecentRouteSnapshot:
132+ p, e := wire.MarshalRecentRouteSnapshot(m)
133+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
134+ case protocol.RouteNavigationFailure:
135+ p, e := wire.MarshalRouteNavigationFailure(m)
136+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
137+ default:
138+ return wire.Frame{}, errors.New("test client connection: unsupported client message")
139+ }
140+}
141+
142+func testServerMessage(frame wire.Frame) (protocol.ServerMessage, error) {
143+ switch frame.Type {
144+ case wire.MsgWelcome:
145+ return wire.UnmarshalWelcome(frame.Payload)
146+ case wire.MsgError:
147+ return wire.UnmarshalErrorMsg(frame.Payload)
148+ case wire.MsgOutput:
149+ return wire.UnmarshalOutput(frame.Payload)
150+ case wire.MsgDetached:
151+ return wire.UnmarshalDetached(frame.Payload)
152+ case wire.MsgPong:
153+ return wire.UnmarshalPong(frame.Payload)
154+ case wire.MsgSessions:
155+ return wire.UnmarshalSessions(frame.Payload)
156+ case wire.MsgCommandResult:
157+ return wire.UnmarshalCommandResult(frame.Payload)
158+ case wire.MsgNavigationAction:
159+ return wire.UnmarshalNavigationDirective(frame.Payload)
160+ case wire.MsgAttachTarget:
161+ return wire.UnmarshalAttachTarget(frame.Payload)
162+ case wire.MsgRemotePreviewResponse:
163+ return wire.UnmarshalRemotePreview(frame.Payload)
164+ case wire.MsgCommittedRouteIdentity:
165+ return wire.UnmarshalCommittedRouteIdentity(frame.Payload)
166+ case wire.MsgNavigateRecentRoute:
167+ return wire.UnmarshalRouteNavigationAction(frame.Payload)
168+ case wire.MsgRouteNavigationFailure:
169+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
170+ case wire.MsgRoutePosition:
171+ return wire.UnmarshalRoutePosition(frame.Payload)
172+ case wire.MsgSamePeerSwitchFailure:
173+ return wire.UnmarshalSamePeerSwitchFailure(frame.Payload)
174+ case wire.MsgParkedRouteResponse:
175+ return wire.UnmarshalParkedRouteResponse(frame.Payload)
176+ default:
177+ return nil, errors.New("test client connection: unsupported server frame")
178+ }
179+}
180+
181+func testClientCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
182+ _, dgram := raw.(ports.DatagramTransport)
183+ _, link := raw.(ports.LinkStateReporter)
184+ window := uint8(8)
185+ if dgram {
186+ window = 1
187+ }
188+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, LinkState: link}
189+}
190+func testClientLinkState(raw ports.Transport) ports.LinkState {
191+ if r, ok := raw.(ports.LinkStateReporter); ok {
192+ return r.LinkState()
193+ }
194+ return ports.LinkStateConnected
195+}
196+func testClientLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
197+ if r, ok := raw.(ports.LinkStateReporter); ok {
198+ return r.LinkEvents()
199+ }
200+ return nil
201+}
202+
203+func TestRunRecvIgnoresUnknownTypedBoundaryFailures(t *testing.T) {
204+ connection := portsmocks.NewMockClientConnection(t)
205+ connection.EXPECT().ReceiveServer().Return(nil, &protocol.DecodeFailure{Category: protocol.DecodeUnknownType}).Once()
206+ connection.EXPECT().ReceiveServer().Return(protocol.Pong{}, nil).Once()
207+ connection.EXPECT().ReceiveServer().Return(nil, io.EOF).Once()
208+ results := make(chan recvResult, 2)
209+ failed := make(chan struct{})
210+ runRecv(context.Background(), connection, results, failed, slog.New(slog.DiscardHandler))
211+ first := <-results
212+ require.Equal(t, protocol.Pong{}, first.message)
213+ require.NoError(t, first.err)
214+ second := <-results
215+ require.ErrorIs(t, second.err, io.EOF)
216+ select {
217+ case <-failed:
218+ default:
219+ t.Fatal("terminal receive failure was not reported")
220+ }
221+}
222+
223+func (t *ackRecordingTransport) SendClient(m protocol.ClientMessage) error {
224+ f, e := testClientFrame(m)
225+ if e != nil {
226+ return e
227+ }
228+ return t.Send(f)
229+}
230+func (t *ackRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
231+ f, e := t.Recv()
232+ if e != nil {
233+ return nil, e
234+ }
235+ return testServerMessage(f)
236+}
237+func (t *ackRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
238+ return testClientCapabilities(t)
239+}
240+func (t *ackRecordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
241+func (t *ackRecordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
242+
243+func (t *reconnectToastLinkTransport) SendClient(m protocol.ClientMessage) error {
244+ f, e := testClientFrame(m)
245+ if e != nil {
246+ return e
247+ }
248+ return t.Send(f)
249+}
250+func (t *reconnectToastLinkTransport) ReceiveServer() (protocol.ServerMessage, error) {
251+ f, e := t.Recv()
252+ if e != nil {
253+ return nil, e
254+ }
255+ return testServerMessage(f)
256+}
257+func (t *reconnectToastLinkTransport) Capabilities() protocol.ConnectionCapabilities {
258+ return testClientCapabilities(t)
259+}
260+
261+func (t *reconnectToastRecordingTransport) SendClient(m protocol.ClientMessage) error {
262+ f, e := testClientFrame(m)
263+ if e != nil {
264+ return e
265+ }
266+ return t.Send(f)
267+}
268+func (t *reconnectToastRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
269+ f, e := t.Recv()
270+ if e != nil {
271+ return nil, e
272+ }
273+ return testServerMessage(f)
274+}
275+func (t *reconnectToastRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
276+ return testClientCapabilities(t)
277+}
278+func (t *reconnectToastRecordingTransport) LinkState() ports.LinkState {
279+ return ports.LinkStateConnected
280+}
281+func (t *reconnectToastRecordingTransport) LinkEvents() <-chan ports.LinkEvent {
282+ return nil
283+}
284+
285+func (t *reconnectResetBlockingTransport) SendClient(m protocol.ClientMessage) error {
286+ f, e := testClientFrame(m)
287+ if e != nil {
288+ return e
289+ }
290+ return t.Send(f)
291+}
292+func (t *reconnectResetBlockingTransport) ReceiveServer() (protocol.ServerMessage, error) {
293+ f, e := t.Recv()
294+ if e != nil {
295+ return nil, e
296+ }
297+ return testServerMessage(f)
298+}
299+func (t *reconnectResetBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
300+ return testClientCapabilities(t)
301+}
302+func (t *reconnectResetBlockingTransport) LinkState() ports.LinkState {
303+ return ports.LinkStateConnected
304+}
305+func (t *reconnectResetBlockingTransport) LinkEvents() <-chan ports.LinkEvent {
306+ return nil
307+}
308+
309+func (t *reconnectToastBlockingSendTransport) SendClient(m protocol.ClientMessage) error {
310+ f, e := testClientFrame(m)
311+ if e != nil {
312+ return e
313+ }
314+ return t.Send(f)
315+}
316+func (t *reconnectToastBlockingSendTransport) ReceiveServer() (protocol.ServerMessage, error) {
317+ f, e := t.Recv()
318+ if e != nil {
319+ return nil, e
320+ }
321+ return testServerMessage(f)
322+}
323+func (t *reconnectToastBlockingSendTransport) Capabilities() protocol.ConnectionCapabilities {
324+ return testClientCapabilities(t)
325+}
326+
327+func (t *paletteAttachTransport) SendClient(m protocol.ClientMessage) error {
328+ f, e := testClientFrame(m)
329+ if e != nil {
330+ return e
331+ }
332+ return t.Send(f)
333+}
334+func (t *paletteAttachTransport) ReceiveServer() (protocol.ServerMessage, error) {
335+ f, e := t.Recv()
336+ if e != nil {
337+ return nil, e
338+ }
339+ return testServerMessage(f)
340+}
341+func (t *paletteAttachTransport) Capabilities() protocol.ConnectionCapabilities {
342+ return testClientCapabilities(t)
343+}
344+func (t *paletteAttachTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
345+func (t *paletteAttachTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
346+
347+func (t *attachPaletteTransport) SendClient(m protocol.ClientMessage) error {
348+ f, e := testClientFrame(m)
349+ if e != nil {
350+ return e
351+ }
352+ return t.Send(f)
353+}
354+func (t *attachPaletteTransport) ReceiveServer() (protocol.ServerMessage, error) {
355+ f, e := t.Recv()
356+ if e != nil {
357+ return nil, e
358+ }
359+ return testServerMessage(f)
360+}
361+func (t *attachPaletteTransport) Capabilities() protocol.ConnectionCapabilities {
362+ return testClientCapabilities(t)
363+}
364+func (t *attachPaletteTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
365+func (t *attachPaletteTransport) LinkEvents() <-chan ports.LinkEvent { 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