detached 9af94c6

Merge pull request #230 from bnema/refactor/client-session-channel

refactor(client): use typed session connections
commit
9af94c66f40e24642eb545e67fbee8b1da152105
author
bnema · · 2026-08-28 14:54 UTC
committer
GitHub · · 2026-08-28 14:54 UTC
merge of
c130909 + 9d20550 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
24 files · +1746 -363
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 +140 -0
@@ -0,0 +1,140 @@
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{Targets: []protocol.RouteAttentionTarget{}}, 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+ got, err := decodeClient(raw.sent)
50+ require.NoError(t, err)
51+ require.Equal(t, tt.message, got)
52+ })
53+ }
54+}
55+
56+func TestClientConnectionDecodesEveryServerMessage(t *testing.T) {
57+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
58+ lease := protocol.ParkedRouteLeaseID{1}
59+ tests := []struct {
60+ name string
61+ message protocol.ServerMessage
62+ }{
63+ {name: "welcome", message: protocol.Welcome{SessionID: "session"}},
64+ {name: "error", message: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"}},
65+ {name: "output", message: protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}},
66+ {name: "detached", message: protocol.Detached{Reason: protocol.ReasonDetach}},
67+ {name: "pong", message: protocol.Pong{}},
68+ {name: "sessions", message: protocol.Sessions{Sessions: []protocol.SessionInfo{}}},
69+ {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}},
70+ {name: "navigation", message: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}},
71+ {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},
72+ {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},
73+ {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},
74+ {name: "route action", message: protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 1, Generation: 1}},
75+ {name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
76+ {name: "route position", message: protocol.RoutePosition{Target: exact, ActiveTabID: "tab-1"}},
77+ {name: "switch failure", message: protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchUnavailable}},
78+ {name: "parked response", message: protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}},
79+ }
80+ for _, tt := range tests {
81+ t.Run(tt.name, func(t *testing.T) {
82+ frame, err := encodeServer(tt.message)
83+ require.NoError(t, err)
84+ got, err := NewClientConnection(&scriptedTransport{recv: frame}).ReceiveServer()
85+ require.NoError(t, err)
86+ require.Equal(t, tt.message, got)
87+ })
88+ }
89+}
90+
91+func TestClientConnectionClassifiesFailuresAndPreservesCapabilities(t *testing.T) {
92+ validError := wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"})
93+ for _, tt := range []struct {
94+ name string
95+ frame wire.Frame
96+ category protocol.DecodeCategory
97+ }{
98+ {name: "wrong direction", frame: wire.Frame{Type: wire.MsgInput}, category: protocol.DecodeWrongDirection},
99+ {name: "truncated payload", frame: wire.Frame{Type: wire.MsgError, Payload: validError[:1]}, category: protocol.DecodeMalformed},
100+ {name: "trailing garbage", frame: wire.Frame{Type: wire.MsgError, Payload: append(append([]byte(nil), validError...), 0xff)}, category: protocol.DecodeMalformed},
101+ } {
102+ t.Run(tt.name, func(t *testing.T) {
103+ _, err := NewClientConnection(&scriptedTransport{recv: tt.frame}).ReceiveServer()
104+ var failure *protocol.DecodeFailure
105+ require.ErrorAs(t, err, &failure)
106+ require.Equal(t, tt.category, failure.Category)
107+ })
108+ }
109+
110+ events := make(chan ports.LinkEvent, 1)
111+ raw := &capableTransport{events: events}
112+ connection := NewClientConnection(raw)
113+ require.Equal(t, uint8(1), connection.Capabilities().PreferredOutputWindow)
114+ require.True(t, connection.Capabilities().LinkState)
115+ require.Equal(t, ports.LinkStateDegraded, connection.LinkState())
116+ require.Equal(t, (<-chan ports.LinkEvent)(events), connection.LinkEvents())
117+}
118+
119+func TestClientDialerWrapsEachConnectionAndPreservesErrors(t *testing.T) {
120+ dialErr := errors.New("dial failed")
121+ dialer := NewClientDialer(&scriptedDialer{err: dialErr})
122+ _, err := dialer.Dial(context.Background())
123+ require.ErrorIs(t, err, dialErr)
124+
125+ raw := &scriptedTransport{}
126+ dialer = NewClientDialer(&scriptedDialer{transport: raw})
127+ connection, err := dialer.Dial(context.Background())
128+ require.NoError(t, err)
129+ require.NoError(t, connection.SendClient(protocol.Ping{}))
130+ require.Equal(t, wire.MsgPing, raw.sent.Type)
131+}
132+
133+type scriptedDialer struct {
134+ transport ports.Transport
135+ err error
136+}
137+
138+func (d *scriptedDialer) Dial(context.Context) (ports.Transport, error) {
139+ return d.transport, d.err
140+}
@@ -0,0 +1,140 @@
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{Targets: []protocol.RouteAttentionTarget{}}, 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+ got, err := decodeClient(raw.sent)
50+ require.NoError(t, err)
51+ require.Equal(t, tt.message, got)
52+ })
53+ }
54+}
55+
56+func TestClientConnectionDecodesEveryServerMessage(t *testing.T) {
57+ exact := protocol.ExactSessionTarget{LifecycleID: domain.SessionLifecycleID{1}, SessionName: "work"}
58+ lease := protocol.ParkedRouteLeaseID{1}
59+ tests := []struct {
60+ name string
61+ message protocol.ServerMessage
62+ }{
63+ {name: "welcome", message: protocol.Welcome{SessionID: "session"}},
64+ {name: "error", message: protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"}},
65+ {name: "output", message: protocol.Output{Epoch: 1, New: 1, Full: true, Size: domain.Size{Cols: 80, Rows: 24}}},
66+ {name: "detached", message: protocol.Detached{Reason: protocol.ReasonDetach}},
67+ {name: "pong", message: protocol.Pong{}},
68+ {name: "sessions", message: protocol.Sessions{Sessions: []protocol.SessionInfo{}}},
69+ {name: "command result", message: protocol.CommandResult{RequestID: 1, OK: true}},
70+ {name: "navigation", message: protocol.NavigationDirective{Action: protocol.NavigationOpenHomePicker, LeaseID: lease}},
71+ {name: "attach target", message: protocol.AttachTarget{Session: "work", Intent: protocol.IntentAttach}},
72+ {name: "preview", message: protocol.RemotePreview{Version: protocol.RemotePreviewSchemaVersion, Status: protocol.RemotePreviewUnavailable}},
73+ {name: "identity", message: protocol.CommittedRouteIdentity{Target: exact}},
74+ {name: "route action", message: protocol.RouteNavigationAction{SnapshotGeneration: 1, Key: 1, Generation: 1}},
75+ {name: "route failure", message: protocol.RouteNavigationFailure{Key: 1, Generation: 1, Code: protocol.RouteFailureUnavailable}},
76+ {name: "route position", message: protocol.RoutePosition{Target: exact, ActiveTabID: "tab-1"}},
77+ {name: "switch failure", message: protocol.SamePeerSwitchFailure{RequestID: 1, Code: protocol.SamePeerSwitchUnavailable}},
78+ {name: "parked response", message: protocol.ParkedRouteResponse{RequestID: 1, Status: protocol.ParkedRouteReady}},
79+ }
80+ for _, tt := range tests {
81+ t.Run(tt.name, func(t *testing.T) {
82+ frame, err := encodeServer(tt.message)
83+ require.NoError(t, err)
84+ got, err := NewClientConnection(&scriptedTransport{recv: frame}).ReceiveServer()
85+ require.NoError(t, err)
86+ require.Equal(t, tt.message, got)
87+ })
88+ }
89+}
90+
91+func TestClientConnectionClassifiesFailuresAndPreservesCapabilities(t *testing.T) {
92+ validError := wire.MarshalErrorMsg(protocol.ErrorMsg{Code: protocol.ErrInternal, Text: "error"})
93+ for _, tt := range []struct {
94+ name string
95+ frame wire.Frame
96+ category protocol.DecodeCategory
97+ }{
98+ {name: "wrong direction", frame: wire.Frame{Type: wire.MsgInput}, category: protocol.DecodeWrongDirection},
99+ {name: "truncated payload", frame: wire.Frame{Type: wire.MsgError, Payload: validError[:1]}, category: protocol.DecodeMalformed},
100+ {name: "trailing garbage", frame: wire.Frame{Type: wire.MsgError, Payload: append(append([]byte(nil), validError...), 0xff)}, category: protocol.DecodeMalformed},
101+ } {
102+ t.Run(tt.name, func(t *testing.T) {
103+ _, err := NewClientConnection(&scriptedTransport{recv: tt.frame}).ReceiveServer()
104+ var failure *protocol.DecodeFailure
105+ require.ErrorAs(t, err, &failure)
106+ require.Equal(t, tt.category, failure.Category)
107+ })
108+ }
109+
110+ events := make(chan ports.LinkEvent, 1)
111+ raw := &capableTransport{events: events}
112+ connection := NewClientConnection(raw)
113+ require.Equal(t, uint8(1), connection.Capabilities().PreferredOutputWindow)
114+ require.True(t, connection.Capabilities().LinkState)
115+ require.Equal(t, ports.LinkStateDegraded, connection.LinkState())
116+ require.Equal(t, (<-chan ports.LinkEvent)(events), connection.LinkEvents())
117+}
118+
119+func TestClientDialerWrapsEachConnectionAndPreservesErrors(t *testing.T) {
120+ dialErr := errors.New("dial failed")
121+ dialer := NewClientDialer(&scriptedDialer{err: dialErr})
122+ _, err := dialer.Dial(context.Background())
123+ require.ErrorIs(t, err, dialErr)
124+
125+ raw := &scriptedTransport{}
126+ dialer = NewClientDialer(&scriptedDialer{transport: raw})
127+ connection, err := dialer.Dial(context.Background())
128+ require.NoError(t, err)
129+ require.NoError(t, connection.SendClient(protocol.Ping{}))
130+ require.Equal(t, wire.MsgPing, raw.sent.Type)
131+}
132+
133+type scriptedDialer struct {
134+ transport ports.Transport
135+ err error
136+}
137+
138+func (d *scriptedDialer) Dial(context.Context) (ports.Transport, error) {
139+ return d.transport, d.err
140+}
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 +16 -14
@@ -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, ctx context.Context, dialer ports.ClientDialer, name string) {
816+ t.Helper()
817+ _, err := dialer.Dial(ctx)
818+ require.EqualError(t, err, "not used: "+name)
811819 }
812820
813821 // fakeClipboardReader is a distinguishable ports.ClipboardReader used only to
@@ -838,12 +846,9 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
838846 remoteDialerFactory: factory,
839847 selectedRemoteTransport: tt.selectedTransport,
840848 clipboard: clip,
841- 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
849+ runClient: func(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error {
850+ requireNamedClientDialer(t, ctx, 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")
@@ -1064,12 +1069,9 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
10641069 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },
10651070 remoteDialerFactory: factory,
10661071 clipboard: &fakeClipboardReader{}, // must NOT reach runClient for a local attach
1067- 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
1072+ runClient: func(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error {
1073+ requireNamedClientDialer(t, ctx, 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, ctx context.Context, dialer ports.ClientDialer, name string) {
816+ t.Helper()
817+ _, err := dialer.Dial(ctx)
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
@@ -838,12 +846,9 @@ func TestRunAttachWithDepsSelectsRemoteTransport(t *testing.T) {
838 remoteDialerFactory: factory,846 remoteDialerFactory: factory,
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(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error {
842- nd, ok := deps.Dialer.(namedDialer)850+ requireNamedClientDialer(t, ctx, 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")
@@ -1064,12 +1069,9 @@ func TestRunAttachWithDepsBuildsLocalDialer(t *testing.T) {
1064 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },1069 localDialer: func() ports.Dialer { return namedDialer{name: "local"} },
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(ctx context.Context, deps client.Dependencies, request client.AttachRequest) error {
1068- nd, ok := deps.Dialer.(namedDialer)1073+ requireNamedClientDialer(t, ctx, 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 +2 -4
@@ -236,13 +236,11 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) {
236236 t.Cleanup(func() { newRemoteDialerFactoryWithRuntimeObserver = originalFactory })
237237
238238 originalRunClient := runClientWithDeps
239- runClientWithDeps = func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {
239+ runClientWithDeps = func(ctx context.Context, deps client.Dependencies, _ client.AttachRequest) error {
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, ctx, deps.Dialer, "remote")
246244 return nil
247245 }
248246 t.Cleanup(func() { runClientWithDeps = originalRunClient })
@@ -236,13 +236,11 @@ func TestRunAttachPropagatesOneObserverToRemoteTransportFactory(t *testing.T) {
236 t.Cleanup(func() { newRemoteDialerFactoryWithRuntimeObserver = originalFactory })236 t.Cleanup(func() { newRemoteDialerFactoryWithRuntimeObserver = originalFactory })
237 237
238 originalRunClient := runClientWithDeps238 originalRunClient := runClientWithDeps
239- runClientWithDeps = func(_ context.Context, deps client.Dependencies, _ client.AttachRequest) error {239+ runClientWithDeps = func(ctx context.Context, deps client.Dependencies, _ client.AttachRequest) error {
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, ctx, 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 +139 -199
@@ -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,11 @@ 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
1490+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1491+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1492+ return fmt.Errorf("validating terminal size for reconnect reconciliation: %w", err)
15101493 }
1511- return transport.Send(wire.Frame{Type: wire.MsgResize, Payload: payload})
1494+ return transport.SendClient(message)
15121495 })
15131496 if resetErr != nil {
15141497 resetErr = handshakeContextError(ctx, handshakeTimedOut, resetErr)
@@ -1630,12 +1613,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16301613 return fmt.Errorf("reading terminal geometry for parked route: %w", err)
16311614 }
16321615 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)
1616+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1617+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1618+ return fmt.Errorf("validating terminal size for parked route: %w", err)
16361619 }
16371620 select {
1638- case controlCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:
1621+ case controlCh <- message:
16391622 return nil
16401623 case <-loopCtx.Done():
16411624 return context.Canceled
@@ -1647,12 +1630,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
16471630 }
16481631 nextParkedRequestID++
16491632 request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}
1650- payload := wire.MarshalParkedRouteRequest(request)
1651- if payload == nil {
1633+ if protocol.ValidateParkedRouteRequest(request) != nil {
16521634 return errors.New("vev: invalid parked-route request")
16531635 }
16541636 select {
1655- case controlCh <- wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: payload}:
1637+ case controlCh <- request:
16561638 parkedPending = &parkedRoutePending{
16571639 requestID: request.RequestID, action: action, leaseID: leaseID,
16581640 timer: clk.NewTimer(protocol.HandshakeTimeout),
@@ -1727,12 +1709,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17271709 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)
17281710 }
17291711 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)
1712+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1713+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1714+ return fmt.Errorf("validating terminal size for reconnect reconciliation: %w", err)
17331715 }
17341716 select {
1735- case sendCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:
1717+ case sendCh <- message:
17361718 awaitingReconnectReset = true
17371719 return nil
17381720 case <-loopCtx.Done():
@@ -1743,20 +1725,13 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17431725 if a.runner.ledger == nil {
17441726 return errors.New("vev: route ledger unavailable")
17451727 }
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
1728+ messages := []protocol.ClientMessage{
1729+ a.runner.ledger.snapshot(),
1730+ a.runner.ledger.attentionSubscription(),
17531731 }
1754- for _, frame := range []wire.Frame{
1755- {Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload},
1756- {Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload},
1757- } {
1732+ for _, message := range messages {
17581733 select {
1759- case sendCh <- frame:
1734+ case sendCh <- message:
17601735 case <-loopCtx.Done():
17611736 return context.Canceled
17621737 }
@@ -1764,16 +1739,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
17641739 return nil
17651740 }
17661741 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
1742+ failure := protocol.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: code}
1743+ if failure.Code.Validate() != nil || failure.Key == 0 || failure.Generation == 0 {
1744+ return protocol.ErrInvalidRouteWire
17741745 }
17751746 select {
1776- case sendCh <- wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}:
1747+ case sendCh <- failure:
17771748 return nil
17781749 case <-loopCtx.Done():
17791750 return context.Canceled
@@ -1828,7 +1799,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18281799 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))
18291800 }
18301801 select {
1831- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}:
1802+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}:
18321803 case <-loopCtx.Done():
18331804 return welcomedResult(nil)
18341805 }
@@ -1837,7 +1808,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18371808 if ev.State == ports.LinkStateDegraded {
18381809 log.Warn("UDP link degraded")
18391810 select {
1840- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})}:
1811+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded}:
18411812 case <-loopCtx.Done():
18421813 return welcomedResult(nil)
18431814 }
@@ -1884,12 +1855,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18841855 }
18851856 return welcomedResult(fmt.Errorf("vev: receiving from daemon: %w", r.err))
18861857 }
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- }
1858+ switch message := r.message.(type) {
1859+ case protocol.Output:
1860+ o := message
18931861 nextState, accepted := outputState.next(o)
18941862 if !accepted {
18951863 // A discarded frame was never written and must never be ACKed.
@@ -1897,7 +1865,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
18971865 // repeated gaps while that reset is in flight.
18981866 if !outputResetRequested {
18991867 select {
1900- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
1868+ case sendCh <- protocol.OutputResetRequest{}:
19011869 outputResetRequested = true
19021870 case <-loopCtx.Done():
19031871 return loopCanceledResult()
@@ -1947,11 +1915,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19471915 log.Debug("received first output")
19481916 endHandshake()
19491917 }
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- }
1918+ case protocol.AttachTarget:
1919+ target := message
19551920 if a.returnAttachTargets {
19561921 handoff := welcomedResult(nil)
19571922 handoff.target = &target
@@ -1967,18 +1932,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19671932 }
19681933 nextSamePeerRequestID++
19691934 request := a.runner.ledger.samePeerHandoff(request, target)
1970- payload, err := wire.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{
1935+ switchRequest := protocol.SamePeerSwitchRequest{
19711936 RequestID: nextSamePeerRequestID,
19721937 Target: *target.ExactTarget,
19731938 PreferredTabID: request.PreferredTabID,
1974- })
1975- if err != nil {
1976- return welcomedResult(fmt.Errorf("vev: encoding same-peer switch: %w", err))
1939+ }
1940+ if switchRequest.Validate() != nil {
1941+ return welcomedResult(errors.New("vev: invalid same-peer switch"))
19771942 }
19781943 samePeerSwitch = &samePeerSwitchPending{requestID: nextSamePeerRequestID, target: *target.ExactTarget}
19791944 inputGate.setPaused(true)
19801945 select {
1981- case controlCh <- wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: payload}:
1946+ case controlCh <- switchRequest:
19821947 continue
19831948 case <-loopCtx.Done():
19841949 return loopCanceledResult()
@@ -1987,11 +1952,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
19871952 handoff := welcomedResult(nil)
19881953 handoff.target = &target
19891954 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- }
1955+ case protocol.ParkedRouteResponse:
1956+ response := message
19951957 if parkedPending == nil || response.RequestID != parkedPending.requestID {
19961958 return welcomedResult(errors.New("vev: unexpected parked-route response"))
19971959 }
@@ -2034,12 +1996,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20341996 return fallback
20351997 }
20361998 }
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)
1999+ case protocol.NavigationDirective:
2000+ directive := message
2001+ datagramRoute := transport.Capabilities().PreferredOutputWindow == 1
20432002 if directive.Action == protocol.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {
20442003 stopForeground()
20452004 if err := flushSender(); err != nil {
@@ -2053,11 +2012,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20532012 navigation := welcomedResult(nil)
20542013 navigation.action = directive.Action
20552014 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- }
2015+ case protocol.RouteNavigationAction:
2016+ action := message
20612017 if a.runner.ledger == nil {
20622018 if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {
20632019 return welcomedResult(ferr)
@@ -2077,11 +2033,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
20772033 navigation := welcomedResult(nil)
20782034 navigation.routeAction = &action
20792035 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- }
2036+ case protocol.CommittedRouteIdentity:
2037+ identity := message
20852038 if samePeerSwitch != nil && identity.Target != samePeerSwitch.target {
20862039 return welcomedResult(errors.New("vev: same-peer switch committed an unexpected target"))
20872040 }
@@ -2100,11 +2053,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21002053 samePeerSwitch = nil
21012054 inputGate.setPaused(false)
21022055 }
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- }
2056+ case protocol.SamePeerSwitchFailure:
2057+ failure := message
21082058 if samePeerSwitch != nil && failure.RequestID == samePeerSwitch.requestID {
21092059 samePeerSwitch = nil
21102060 inputGate.setPaused(false)
@@ -2112,7 +2062,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21122062 transitionWaitingFull = true
21132063 if !outputResetRequested {
21142064 select {
2115- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:
2065+ case sendCh <- protocol.OutputResetRequest{}:
21162066 outputResetRequested = true
21172067 case <-loopCtx.Done():
21182068 return loopCanceledResult()
@@ -2122,11 +2072,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21222072 transition.stop()
21232073 }
21242074 }
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- }
2075+ case protocol.RoutePosition:
2076+ position := message
21302077 if a.runner.ledger == nil {
21312078 return welcomedResult(errors.New("vev: route ledger unavailable"))
21322079 }
@@ -2134,17 +2081,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21342081 return welcomedResult(fmt.Errorf("vev: remembering route position: %w", derr))
21352082 }
21362083 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- }
2084+ case protocol.RouteNavigationFailure:
2085+ failure := message
21422086 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- }
2087+ case protocol.Detached:
2088+ d := message
21482089 ms.detached = true
21492090 if d.Reason == protocol.ReasonDetach {
21502091 log.Info("detached cleanly", "reason", d.Reason)
@@ -2152,34 +2093,43 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
21522093 log.Warn("detached by daemon", "reason", d.Reason)
21532094 }
21542095 return welcomedResult(detachedResult(d.Reason))
2155- case wire.MsgPong:
2096+ case protocol.Pong:
21562097 // 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.
21602098 }
21612099 }
21622100 }
21632101 }
21642102
2165-// recvResult carries one framed message (or a read error) from the receive
2166-// pump to the main loop.
2103+// recvResult carries one typed message (or a read error) from the receive pump.
21672104 type recvResult struct {
2168- frame wire.Frame
2169- err error
2105+ message protocol.ServerMessage
2106+ err error
21702107 }
21712108
2172-// runRecv reads frames until an error, forwarding each to the main loop.
2109+// runRecv reads typed messages until an error, forwarding each to the main loop.
21732110 // 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) {
2111+func runRecv(ctx context.Context, transport ports.ClientConnection, out chan<- recvResult, failed chan<- struct{}, log *slog.Logger) {
21752112 defer log.Debug("receive pump exited")
21762113 for {
2177- f, err := transport.Recv()
2114+ message, err := transport.ReceiveServer()
2115+ if err != nil {
2116+ var failure *protocol.DecodeFailure
2117+ if errors.As(err, &failure) && (failure.Category == protocol.DecodeUnknownType || failure.Category == protocol.DecodeWrongDirection) {
2118+ log.Debug("ignoring rejected server message",
2119+ "category", failure.Category,
2120+ "kind", failure.Kind,
2121+ "type", failure.Type,
2122+ "version", failure.Version,
2123+ "request", failure.RequestID,
2124+ )
2125+ continue
2126+ }
2127+ }
21782128 if err != nil && failed != nil {
21792129 close(failed)
21802130 }
21812131 select {
2182- case out <- recvResult{frame: f, err: err}:
2132+ case out <- recvResult{message: message, err: err}:
21832133 case <-ctx.Done():
21842134 return
21852135 }
@@ -2189,9 +2139,6 @@ func runRecv(ctx context.Context, transport ports.Transport, out chan<- recvResu
21892139 }
21902140 }
21912141
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.
21952142 type cumulativeAckQueue struct {
21962143 mu sync.Mutex
21972144 latestEpoch uint64
@@ -2226,12 +2173,15 @@ func (q *cumulativeAckQueue) take() (uint64, uint64) {
22262173 return epoch, state
22272174 }
22282175
2229-// 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.
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) {
2176+// runSender is the sole caller of SendClient: serialising the input and resize
2177+// pumps through one goroutine keeps the connection's single-writer contract.
2178+// It preserves normal-frame order while allowing switch control and output ACKs
2179+// to make progress when raw input is held for a same-peer switch. A send failure
2180+// cancels the loop and is surfaced once.
2181+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) {
22322182 defer log.Debug("sender pump exited")
2233- send := func(f wire.Frame) bool {
2234- if err := transport.Send(f); err != nil {
2183+ send := func(message protocol.ClientMessage) bool {
2184+ if err := transport.SendClient(message); err != nil {
22352185 select {
22362186 case errCh <- err:
22372187 default:
@@ -2246,16 +2196,7 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
22462196 if state == 0 {
22472197 return true
22482198 }
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})
2199+ return send(protocol.Ack{Epoch: epoch, State: state})
22592200 }
22602201 flushPending := func() bool {
22612202 for {
@@ -2287,12 +2228,12 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
22872228 close(done)
22882229 return true
22892230 }
2290- var heldInput *wire.Frame
2231+ var heldInput protocol.ClientMessage
22912232 for {
22922233 if heldInput != nil {
22932234 paused, changed := inputGate.snapshot()
22942235 if !paused {
2295- if !send(*heldInput) {
2236+ if !send(heldInput) {
22962237 return
22972238 }
22982239 heldInput = nil
@@ -2348,23 +2289,23 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
23482289 if !completeBarrier(done) {
23492290 return
23502291 }
2351- case frame := <-in:
2292+ case message := <-in:
23522293 select {
23532294 case <-ctx.Done():
23542295 return
23552296 default:
23562297 }
2357- if frame.Type == wire.MsgInput || frame.Type == wire.MsgImagePush {
2298+ switch message.(type) {
2299+ case protocol.Input, protocol.ImagePush:
23582300 if paused, _ := inputGate.snapshot(); paused {
2359- held := frame
2360- heldInput = &held
2301+ heldInput = message
23612302 if inputGate != nil && inputGate.afterInputHeld != nil {
23622303 inputGate.afterInputHeld()
23632304 }
23642305 continue
23652306 }
23662307 }
2367- if !send(frame) {
2308+ if !send(message) {
23682309 return
23692310 }
23702311 case <-ctx.Done():
@@ -2726,7 +2667,7 @@ type stdinPump struct {
27262667 in io.Reader // compatibility for isolated scanner tests
27272668 input *terminalInputPump
27282669 consumer uint64
2729- out chan<- wire.Frame
2670+ out chan<- protocol.ClientMessage
27302671 clock ports.Clock
27312672 clipboard ports.ClipboardReader
27322673 logger *slog.Logger
@@ -2747,13 +2688,13 @@ func (p *stdinPump) run() {
27472688 var undeliveredMu sync.Mutex
27482689 var undelivered []byte
27492690 sendOK.Store(true)
2750- send := func(frame wire.Frame) bool {
2691+ send := func(message protocol.ClientMessage) bool {
27512692 if !sendOK.Load() {
27522693 return false
27532694 }
27542695 return p.sendLease.send(func() bool {
27552696 select {
2756- case p.out <- frame:
2697+ case p.out <- message:
27572698 if p.afterInputDelivered != nil {
27582699 p.afterInputDelivered()
27592700 }
@@ -2786,7 +2727,7 @@ func (p *stdinPump) run() {
27862727 return
27872728 }
27882729 inputSeq++
2789- if !send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {
2730+ if !send(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)}) {
27902731 undeliveredMu.Lock()
27912732 undelivered = append(undelivered, data...)
27922733 undeliveredMu.Unlock()
@@ -2800,11 +2741,11 @@ func (p *stdinPump) run() {
28002741 reader: p.clipboard,
28012742 log: p.logger,
28022743 sendNotice: func(action uint8) {
2803- send(wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: action})})
2744+ send(protocol.ClientNotice{Action: action})
28042745 },
28052746 sendImage: func(mime string, data []byte) {
28062747 inputSeq++
2807- send(wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})
2748+ send(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})
28082749 },
28092750 next: coalescer.Scan,
28102751 }
@@ -2946,7 +2887,7 @@ func (p *stdinPump) run() {
29462887 disarmMarkerDeadline()
29472888 markers.flush(sink)
29482889 select {
2949- case p.out <- wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(protocol.Detach{})}:
2890+ case p.out <- protocol.Detach{}:
29502891 case <-p.ctx.Done():
29512892 }
29522893 p.cancel()
@@ -2967,7 +2908,7 @@ func (p *stdinPump) run() {
29672908 // runResize forwards coalesced terminal resize events to the daemon. It
29682909 // tolerates an already-closed resize channel (which the terminal adapter
29692910 // 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) {
2911+func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- protocol.ClientMessage, sendLease *foregroundSendLease, log *slog.Logger) {
29712912 defer log.Debug("resize pump exited")
29722913 for {
29732914 select {
@@ -2976,15 +2917,14 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- wi
29762917 return
29772918 }
29782919 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)
2920+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
2921+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
2922+ log.Error("validating terminal resize", "error", err)
29822923 continue
29832924 }
2984- frame := wire.Frame{Type: wire.MsgResize, Payload: payload}
29852925 if !sendLease.send(func() bool {
29862926 select {
2987- case out <- frame:
2927+ case out <- message:
29882928 return true
29892929 case <-ctx.Done():
29902930 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,11 @@ 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+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1508- if err != nil {1491+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1509- return err1492+ return fmt.Errorf("validating terminal size for reconnect reconciliation: %w", err)
1510 }1493 }
1511- return transport.Send(wire.Frame{Type: wire.MsgResize, Payload: payload})1494+ return transport.SendClient(message)
1512 })1495 })
1513 if resetErr != nil {1496 if resetErr != nil {
1514 resetErr = handshakeContextError(ctx, handshakeTimedOut, resetErr)1497 resetErr = handshakeContextError(ctx, handshakeTimedOut, resetErr)
@@ -1630,12 +1613,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1630 return fmt.Errorf("reading terminal geometry for parked route: %w", err)1613 return fmt.Errorf("reading terminal geometry for parked route: %w", err)
1631 }1614 }
1632 geometry = geometry.NormalizePixels()1615 geometry = geometry.NormalizePixels()
1633- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1616+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1634- if err != nil {1617+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1635- return fmt.Errorf("encoding terminal size for parked route: %w", err)1618+ return fmt.Errorf("validating terminal size for parked route: %w", err)
1636 }1619 }
1637 select {1620 select {
1638- case controlCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:1621+ case controlCh <- message:
1639 return nil1622 return nil
1640 case <-loopCtx.Done():1623 case <-loopCtx.Done():
1641 return context.Canceled1624 return context.Canceled
@@ -1647,12 +1630,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1647 }1630 }
1648 nextParkedRequestID++1631 nextParkedRequestID++
1649 request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}1632 request := protocol.ParkedRouteRequest{RequestID: nextParkedRequestID, LeaseID: leaseID, Action: action, Target: target}
1650- payload := wire.MarshalParkedRouteRequest(request)1633+ if protocol.ValidateParkedRouteRequest(request) != nil {
1651- if payload == nil {
1652 return errors.New("vev: invalid parked-route request")1634 return errors.New("vev: invalid parked-route request")
1653 }1635 }
1654 select {1636 select {
1655- case controlCh <- wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: payload}:1637+ case controlCh <- request:
1656 parkedPending = &parkedRoutePending{1638 parkedPending = &parkedRoutePending{
1657 requestID: request.RequestID, action: action, leaseID: leaseID,1639 requestID: request.RequestID, action: action, leaseID: leaseID,
1658 timer: clk.NewTimer(protocol.HandshakeTimeout),1640 timer: clk.NewTimer(protocol.HandshakeTimeout),
@@ -1727,12 +1709,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1727 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)1709 return fmt.Errorf("reading terminal geometry for reconnect reconciliation: %w", serr)
1728 }1710 }
1729 geometry = geometry.NormalizePixels()1711 geometry = geometry.NormalizePixels()
1730- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})1712+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
1731- if err != nil {1713+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
1732- return fmt.Errorf("encoding terminal size for reconnect reconciliation: %w", err)1714+ return fmt.Errorf("validating terminal size for reconnect reconciliation: %w", err)
1733 }1715 }
1734 select {1716 select {
1735- case sendCh <- wire.Frame{Type: wire.MsgResize, Payload: payload}:1717+ case sendCh <- message:
1736 awaitingReconnectReset = true1718 awaitingReconnectReset = true
1737 return nil1719 return nil
1738 case <-loopCtx.Done():1720 case <-loopCtx.Done():
@@ -1743,20 +1725,13 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1743 if a.runner.ledger == nil {1725 if a.runner.ledger == nil {
1744 return errors.New("vev: route ledger unavailable")1726 return errors.New("vev: route ledger unavailable")
1745 }1727 }
1746- snapshotPayload, err := wire.MarshalRecentRouteSnapshot(a.runner.ledger.snapshot())1728+ messages := []protocol.ClientMessage{
1747- if err != nil {1729+ a.runner.ledger.snapshot(),
1748- return err1730+ a.runner.ledger.attentionSubscription(),
1749- }
1750- attentionPayload, err := wire.MarshalRouteAttentionSubscription(a.runner.ledger.attentionSubscription())
1751- if err != nil {
1752- return err
1753 }1731 }
1754- for _, frame := range []wire.Frame{1732+ for _, message := range messages {
1755- {Type: wire.MsgRecentRouteSnapshot, Payload: snapshotPayload},
1756- {Type: wire.MsgRouteAttentionSubscription, Payload: attentionPayload},
1757- } {
1758 select {1733 select {
1759- case sendCh <- frame:1734+ case sendCh <- message:
1760 case <-loopCtx.Done():1735 case <-loopCtx.Done():
1761 return context.Canceled1736 return context.Canceled
1762 }1737 }
@@ -1764,16 +1739,12 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1764 return nil1739 return nil
1765 }1740 }
1766 sendRouteFailure := func(action protocol.RouteNavigationAction, code protocol.RouteFailureCode) error {1741 sendRouteFailure := func(action protocol.RouteNavigationAction, code protocol.RouteFailureCode) error {
1767- payload, err := wire.MarshalRouteNavigationFailure(protocol.RouteNavigationFailure{1742+ failure := protocol.RouteNavigationFailure{Key: action.Key, Generation: action.Generation, Code: code}
1768- Key: action.Key,1743+ if failure.Code.Validate() != nil || failure.Key == 0 || failure.Generation == 0 {
1769- Generation: action.Generation,1744+ return protocol.ErrInvalidRouteWire
1770- Code: code,
1771- })
1772- if err != nil {
1773- return err
1774 }1745 }
1775 select {1746 select {
1776- case sendCh <- wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: payload}:1747+ case sendCh <- failure:
1777 return nil1748 return nil
1778 case <-loopCtx.Done():1749 case <-loopCtx.Done():
1779 return context.Canceled1750 return context.Canceled
@@ -1828,7 +1799,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1828 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))1799 return welcomedResult(fmt.Errorf("dismissing reconnect toast: %w", err))
1829 }1800 }
1830 select {1801 select {
1831- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected})}:1802+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkConnected}:
1832 case <-loopCtx.Done():1803 case <-loopCtx.Done():
1833 return welcomedResult(nil)1804 return welcomedResult(nil)
1834 }1805 }
@@ -1837,7 +1808,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1837 if ev.State == ports.LinkStateDegraded {1808 if ev.State == ports.LinkStateDegraded {
1838 log.Warn("UDP link degraded")1809 log.Warn("UDP link degraded")
1839 select {1810 select {
1840- case sendCh <- wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded})}:1811+ case sendCh <- protocol.ClientNotice{Action: protocol.ClientNoticeLinkDegraded}:
1841 case <-loopCtx.Done():1812 case <-loopCtx.Done():
1842 return welcomedResult(nil)1813 return welcomedResult(nil)
1843 }1814 }
@@ -1884,12 +1855,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1884 }1855 }
1885 return welcomedResult(fmt.Errorf("vev: receiving from daemon: %w", r.err))1856 return welcomedResult(fmt.Errorf("vev: receiving from daemon: %w", r.err))
1886 }1857 }
1887- switch r.frame.Type {1858+ switch message := r.message.(type) {
1888- case wire.MsgOutput:1859+ case protocol.Output:
1889- o, derr := wire.UnmarshalOutput(r.frame.Payload)1860+ o := message
1890- if derr != nil {
1891- return welcomedResult(fmt.Errorf("vev: decoding output: %w", derr))
1892- }
1893 nextState, accepted := outputState.next(o)1861 nextState, accepted := outputState.next(o)
1894 if !accepted {1862 if !accepted {
1895 // A discarded frame was never written and must never be ACKed.1863 // A discarded frame was never written and must never be ACKed.
@@ -1897,7 +1865,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1897 // repeated gaps while that reset is in flight.1865 // repeated gaps while that reset is in flight.
1898 if !outputResetRequested {1866 if !outputResetRequested {
1899 select {1867 select {
1900- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:1868+ case sendCh <- protocol.OutputResetRequest{}:
1901 outputResetRequested = true1869 outputResetRequested = true
1902 case <-loopCtx.Done():1870 case <-loopCtx.Done():
1903 return loopCanceledResult()1871 return loopCanceledResult()
@@ -1947,11 +1915,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1947 log.Debug("received first output")1915 log.Debug("received first output")
1948 endHandshake()1916 endHandshake()
1949 }1917 }
1950- case wire.MsgAttachTarget:1918+ case protocol.AttachTarget:
1951- target, derr := wire.UnmarshalAttachTarget(r.frame.Payload)1919+ target := message
1952- if derr != nil {
1953- return welcomedResult(fmt.Errorf("vev: decoding attach target: %w", derr))
1954- }
1955 if a.returnAttachTargets {1920 if a.returnAttachTargets {
1956 handoff := welcomedResult(nil)1921 handoff := welcomedResult(nil)
1957 handoff.target = &target1922 handoff.target = &target
@@ -1967,18 +1932,18 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1967 }1932 }
1968 nextSamePeerRequestID++1933 nextSamePeerRequestID++
1969 request := a.runner.ledger.samePeerHandoff(request, target)1934 request := a.runner.ledger.samePeerHandoff(request, target)
1970- payload, err := wire.MarshalSamePeerSwitchRequest(protocol.SamePeerSwitchRequest{1935+ switchRequest := protocol.SamePeerSwitchRequest{
1971 RequestID: nextSamePeerRequestID,1936 RequestID: nextSamePeerRequestID,
1972 Target: *target.ExactTarget,1937 Target: *target.ExactTarget,
1973 PreferredTabID: request.PreferredTabID,1938 PreferredTabID: request.PreferredTabID,
1974- })1939+ }
1975- if err != nil {1940+ if switchRequest.Validate() != nil {
1976- return welcomedResult(fmt.Errorf("vev: encoding same-peer switch: %w", err))1941+ return welcomedResult(errors.New("vev: invalid same-peer switch"))
1977 }1942 }
1978 samePeerSwitch = &samePeerSwitchPending{requestID: nextSamePeerRequestID, target: *target.ExactTarget}1943 samePeerSwitch = &samePeerSwitchPending{requestID: nextSamePeerRequestID, target: *target.ExactTarget}
1979 inputGate.setPaused(true)1944 inputGate.setPaused(true)
1980 select {1945 select {
1981- case controlCh <- wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: payload}:1946+ case controlCh <- switchRequest:
1982 continue1947 continue
1983 case <-loopCtx.Done():1948 case <-loopCtx.Done():
1984 return loopCanceledResult()1949 return loopCanceledResult()
@@ -1987,11 +1952,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
1987 handoff := welcomedResult(nil)1952 handoff := welcomedResult(nil)
1988 handoff.target = &target1953 handoff.target = &target
1989 return handoff1954 return handoff
1990- case wire.MsgParkedRouteResponse:1955+ case protocol.ParkedRouteResponse:
1991- response, derr := wire.UnmarshalParkedRouteResponse(r.frame.Payload)1956+ 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 {1957 if parkedPending == nil || response.RequestID != parkedPending.requestID {
1996 return welcomedResult(errors.New("vev: unexpected parked-route response"))1958 return welcomedResult(errors.New("vev: unexpected parked-route response"))
1997 }1959 }
@@ -2034,12 +1996,9 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2034 return fallback1996 return fallback
2035 }1997 }
2036 }1998 }
2037- case wire.MsgNavigationAction:1999+ case protocol.NavigationDirective:
2038- directive, derr := wire.UnmarshalNavigationDirective(r.frame.Payload)2000+ directive := message
2039- if derr != nil {2001+ 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 {2002 if directive.Action == protocol.NavigationOpenHomePicker && a.openHomePicker != nil && datagramRoute {
2044 stopForeground()2003 stopForeground()
2045 if err := flushSender(); err != nil {2004 if err := flushSender(); err != nil {
@@ -2053,11 +2012,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2053 navigation := welcomedResult(nil)2012 navigation := welcomedResult(nil)
2054 navigation.action = directive.Action2013 navigation.action = directive.Action
2055 return navigation2014 return navigation
2056- case wire.MsgNavigateRecentRoute:2015+ case protocol.RouteNavigationAction:
2057- action, derr := wire.UnmarshalRouteNavigationAction(r.frame.Payload)2016+ 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 {2017 if a.runner.ledger == nil {
2062 if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {2018 if ferr := sendRouteFailure(action, protocol.RouteFailureUnavailable); ferr != nil {
2063 return welcomedResult(ferr)2019 return welcomedResult(ferr)
@@ -2077,11 +2033,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2077 navigation := welcomedResult(nil)2033 navigation := welcomedResult(nil)
2078 navigation.routeAction = &action2034 navigation.routeAction = &action
2079 return navigation2035 return navigation
2080- case wire.MsgCommittedRouteIdentity:2036+ case protocol.CommittedRouteIdentity:
2081- identity, derr := wire.UnmarshalCommittedRouteIdentity(r.frame.Payload)2037+ 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 {2038 if samePeerSwitch != nil && identity.Target != samePeerSwitch.target {
2086 return welcomedResult(errors.New("vev: same-peer switch committed an unexpected target"))2039 return welcomedResult(errors.New("vev: same-peer switch committed an unexpected target"))
2087 }2040 }
@@ -2100,11 +2053,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2100 samePeerSwitch = nil2053 samePeerSwitch = nil
2101 inputGate.setPaused(false)2054 inputGate.setPaused(false)
2102 }2055 }
2103- case wire.MsgSamePeerSwitchFailure:2056+ case protocol.SamePeerSwitchFailure:
2104- failure, derr := wire.UnmarshalSamePeerSwitchFailure(r.frame.Payload)2057+ 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 {2058 if samePeerSwitch != nil && failure.RequestID == samePeerSwitch.requestID {
2109 samePeerSwitch = nil2059 samePeerSwitch = nil
2110 inputGate.setPaused(false)2060 inputGate.setPaused(false)
@@ -2112,7 +2062,7 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2112 transitionWaitingFull = true2062 transitionWaitingFull = true
2113 if !outputResetRequested {2063 if !outputResetRequested {
2114 select {2064 select {
2115- case sendCh <- wire.Frame{Type: wire.MsgOutputResetRequest, Payload: wire.MarshalOutputResetRequest(protocol.OutputResetRequest{})}:2065+ case sendCh <- protocol.OutputResetRequest{}:
2116 outputResetRequested = true2066 outputResetRequested = true
2117 case <-loopCtx.Done():2067 case <-loopCtx.Done():
2118 return loopCanceledResult()2068 return loopCanceledResult()
@@ -2122,11 +2072,8 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2122 transition.stop()2072 transition.stop()
2123 }2073 }
2124 }2074 }
2125- case wire.MsgRoutePosition:2075+ case protocol.RoutePosition:
2126- position, derr := wire.UnmarshalRoutePosition(r.frame.Payload)2076+ position := message
2127- if derr != nil {
2128- return welcomedResult(fmt.Errorf("vev: decoding route position: %w", derr))
2129- }
2130 if a.runner.ledger == nil {2077 if a.runner.ledger == nil {
2131 return welcomedResult(errors.New("vev: route ledger unavailable"))2078 return welcomedResult(errors.New("vev: route ledger unavailable"))
2132 }2079 }
@@ -2134,17 +2081,11 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2134 return welcomedResult(fmt.Errorf("vev: remembering route position: %w", derr))2081 return welcomedResult(fmt.Errorf("vev: remembering route position: %w", derr))
2135 }2082 }
2136 routePosition = cloneRoutePosition(&position)2083 routePosition = cloneRoutePosition(&position)
2137- case wire.MsgRouteNavigationFailure:2084+ case protocol.RouteNavigationFailure:
2138- failure, derr := wire.UnmarshalRouteNavigationFailure(r.frame.Payload)2085+ 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)2086 log.Warn("route navigation rejected", "key", failure.Key, "generation", failure.Generation, "code", failure.Code)
2143- case wire.MsgDetached:2087+ case protocol.Detached:
2144- d, derr := wire.UnmarshalDetached(r.frame.Payload)2088+ d := message
2145- if derr != nil {
2146- return welcomedResult(fmt.Errorf("vev: decoding detached: %w", derr))
2147- }
2148 ms.detached = true2089 ms.detached = true
2149 if d.Reason == protocol.ReasonDetach {2090 if d.Reason == protocol.ReasonDetach {
2150 log.Info("detached cleanly", "reason", d.Reason)2091 log.Info("detached cleanly", "reason", d.Reason)
@@ -2152,34 +2093,43 @@ func (a *attachAttempt) run(ctx context.Context) attachResult {
2152 log.Warn("detached by daemon", "reason", d.Reason)2093 log.Warn("detached by daemon", "reason", d.Reason)
2153 }2094 }
2154 return welcomedResult(detachedResult(d.Reason))2095 return welcomedResult(detachedResult(d.Reason))
2155- case wire.MsgPong:2096+ case protocol.Pong:
2156 // Liveness reply; nothing to do.2097 // 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 }2098 }
2161 }2099 }
2162 }2100 }
2163 }2101 }
2164 2102
2165-// recvResult carries one framed message (or a read error) from the receive2103+// recvResult carries one typed message (or a read error) from the receive pump.
2166-// pump to the main loop.
2167 type recvResult struct {2104 type recvResult struct {
2168- frame wire.Frame2105+ message protocol.ServerMessage
2169- err error2106+ err error
2170 }2107 }
2171 2108
2172-// runRecv reads frames until an error, forwarding each to the main loop.2109+// 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.2110 // 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) {2111+func runRecv(ctx context.Context, transport ports.ClientConnection, out chan<- recvResult, failed chan<- struct{}, log *slog.Logger) {
2175 defer log.Debug("receive pump exited")2112 defer log.Debug("receive pump exited")
2176 for {2113 for {
2177- f, err := transport.Recv()2114+ message, err := transport.ReceiveServer()
2115+ if err != nil {
2116+ var failure *protocol.DecodeFailure
2117+ if errors.As(err, &failure) && (failure.Category == protocol.DecodeUnknownType || failure.Category == protocol.DecodeWrongDirection) {
2118+ log.Debug("ignoring rejected server message",
2119+ "category", failure.Category,
2120+ "kind", failure.Kind,
2121+ "type", failure.Type,
2122+ "version", failure.Version,
2123+ "request", failure.RequestID,
2124+ )
2125+ continue
2126+ }
2127+ }
2178 if err != nil && failed != nil {2128 if err != nil && failed != nil {
2179 close(failed)2129 close(failed)
2180 }2130 }
2181 select {2131 select {
2182- case out <- recvResult{frame: f, err: err}:2132+ case out <- recvResult{message: message, err: err}:
2183 case <-ctx.Done():2133 case <-ctx.Done():
2184 return2134 return
2185 }2135 }
@@ -2189,9 +2139,6 @@ func runRecv(ctx context.Context, transport ports.Transport, out chan<- recvResu
2189 }2139 }
2190 }2140 }
2191 2141
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.
2195 type cumulativeAckQueue struct {2142 type cumulativeAckQueue struct {
2196 mu sync.Mutex2143 mu sync.Mutex
2197 latestEpoch uint642144 latestEpoch uint64
@@ -2226,12 +2173,15 @@ func (q *cumulativeAckQueue) take() (uint64, uint64) {
2226 return epoch, state2173 return epoch, state
2227 }2174 }
2228 2175
2229-// runSender preserves normal-frame order while allowing switch control and2176+// runSender is the sole caller of SendClient: serialising the input and resize
2230-// output ACKs to make progress when raw input is held for a same-peer switch.2177+// pumps through one goroutine keeps the connection's single-writer contract.
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) {2178+// It preserves normal-frame order while allowing switch control and output ACKs
2179+// to make progress when raw input is held for a same-peer switch. A send failure
2180+// cancels the loop and is surfaced once.
2181+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")2182 defer log.Debug("sender pump exited")
2233- send := func(f wire.Frame) bool {2183+ send := func(message protocol.ClientMessage) bool {
2234- if err := transport.Send(f); err != nil {2184+ if err := transport.SendClient(message); err != nil {
2235 select {2185 select {
2236 case errCh <- err:2186 case errCh <- err:
2237 default:2187 default:
@@ -2246,16 +2196,7 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
2246 if state == 0 {2196 if state == 0 {
2247 return true2197 return true
2248 }2198 }
2249- payload, err := wire.MarshalAck(protocol.Ack{Epoch: epoch, State: state})2199+ 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 }2200 }
2260 flushPending := func() bool {2201 flushPending := func() bool {
2261 for {2202 for {
@@ -2287,12 +2228,12 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
2287 close(done)2228 close(done)
2288 return true2229 return true
2289 }2230 }
2290- var heldInput *wire.Frame2231+ var heldInput protocol.ClientMessage
2291 for {2232 for {
2292 if heldInput != nil {2233 if heldInput != nil {
2293 paused, changed := inputGate.snapshot()2234 paused, changed := inputGate.snapshot()
2294 if !paused {2235 if !paused {
2295- if !send(*heldInput) {2236+ if !send(heldInput) {
2296 return2237 return
2297 }2238 }
2298 heldInput = nil2239 heldInput = nil
@@ -2348,23 +2289,23 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
2348 if !completeBarrier(done) {2289 if !completeBarrier(done) {
2349 return2290 return
2350 }2291 }
2351- case frame := <-in:2292+ case message := <-in:
2352 select {2293 select {
2353 case <-ctx.Done():2294 case <-ctx.Done():
2354 return2295 return
2355 default:2296 default:
2356 }2297 }
2357- if frame.Type == wire.MsgInput || frame.Type == wire.MsgImagePush {2298+ switch message.(type) {
2299+ case protocol.Input, protocol.ImagePush:
2358 if paused, _ := inputGate.snapshot(); paused {2300 if paused, _ := inputGate.snapshot(); paused {
2359- held := frame2301+ heldInput = message
2360- heldInput = &held
2361 if inputGate != nil && inputGate.afterInputHeld != nil {2302 if inputGate != nil && inputGate.afterInputHeld != nil {
2362 inputGate.afterInputHeld()2303 inputGate.afterInputHeld()
2363 }2304 }
2364 continue2305 continue
2365 }2306 }
2366 }2307 }
2367- if !send(frame) {2308+ if !send(message) {
2368 return2309 return
2369 }2310 }
2370 case <-ctx.Done():2311 case <-ctx.Done():
@@ -2726,7 +2667,7 @@ type stdinPump struct {
2726 in io.Reader // compatibility for isolated scanner tests2667 in io.Reader // compatibility for isolated scanner tests
2727 input *terminalInputPump2668 input *terminalInputPump
2728 consumer uint642669 consumer uint64
2729- out chan<- wire.Frame2670+ out chan<- protocol.ClientMessage
2730 clock ports.Clock2671 clock ports.Clock
2731 clipboard ports.ClipboardReader2672 clipboard ports.ClipboardReader
2732 logger *slog.Logger2673 logger *slog.Logger
@@ -2747,13 +2688,13 @@ func (p *stdinPump) run() {
2747 var undeliveredMu sync.Mutex2688 var undeliveredMu sync.Mutex
2748 var undelivered []byte2689 var undelivered []byte
2749 sendOK.Store(true)2690 sendOK.Store(true)
2750- send := func(frame wire.Frame) bool {2691+ send := func(message protocol.ClientMessage) bool {
2751 if !sendOK.Load() {2692 if !sendOK.Load() {
2752 return false2693 return false
2753 }2694 }
2754 return p.sendLease.send(func() bool {2695 return p.sendLease.send(func() bool {
2755 select {2696 select {
2756- case p.out <- frame:2697+ case p.out <- message:
2757 if p.afterInputDelivered != nil {2698 if p.afterInputDelivered != nil {
2758 p.afterInputDelivered()2699 p.afterInputDelivered()
2759 }2700 }
@@ -2786,7 +2727,7 @@ func (p *stdinPump) run() {
2786 return2727 return
2787 }2728 }
2788 inputSeq++2729 inputSeq++
2789- if !send(wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)})}) {2730+ if !send(protocol.Input{InputSeq: inputSeq, Data: append([]byte(nil), data...)}) {
2790 undeliveredMu.Lock()2731 undeliveredMu.Lock()
2791 undelivered = append(undelivered, data...)2732 undelivered = append(undelivered, data...)
2792 undeliveredMu.Unlock()2733 undeliveredMu.Unlock()
@@ -2800,11 +2741,11 @@ func (p *stdinPump) run() {
2800 reader: p.clipboard,2741 reader: p.clipboard,
2801 log: p.logger,2742 log: p.logger,
2802 sendNotice: func(action uint8) {2743 sendNotice: func(action uint8) {
2803- send(wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(protocol.ClientNotice{Action: action})})2744+ send(protocol.ClientNotice{Action: action})
2804 },2745 },
2805 sendImage: func(mime string, data []byte) {2746 sendImage: func(mime string, data []byte) {
2806 inputSeq++2747 inputSeq++
2807- send(wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})})2748+ send(protocol.ImagePush{InputSeq: inputSeq, Mime: mime, Data: data})
2808 },2749 },
2809 next: coalescer.Scan,2750 next: coalescer.Scan,
2810 }2751 }
@@ -2946,7 +2887,7 @@ func (p *stdinPump) run() {
2946 disarmMarkerDeadline()2887 disarmMarkerDeadline()
2947 markers.flush(sink)2888 markers.flush(sink)
2948 select {2889 select {
2949- case p.out <- wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(protocol.Detach{})}:2890+ case p.out <- protocol.Detach{}:
2950 case <-p.ctx.Done():2891 case <-p.ctx.Done():
2951 }2892 }
2952 p.cancel()2893 p.cancel()
@@ -2967,7 +2908,7 @@ func (p *stdinPump) run() {
2967 // runResize forwards coalesced terminal resize events to the daemon. It2908 // runResize forwards coalesced terminal resize events to the daemon. It
2968 // tolerates an already-closed resize channel (which the terminal adapter2909 // tolerates an already-closed resize channel (which the terminal adapter
2969 // hands back when restore ran before ResizeEvents was first called).2910 // 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) {2911+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")2912 defer log.Debug("resize pump exited")
2972 for {2913 for {
2973 select {2914 select {
@@ -2976,15 +2917,14 @@ func runResize(ctx context.Context, events <-chan domain.Geometry, out chan<- wi
2976 return2917 return
2977 }2918 }
2978 geometry = geometry.NormalizePixels()2919 geometry = geometry.NormalizePixels()
2979- payload, err := wire.MarshalResize(protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight})2920+ message := protocol.Resize{Size: geometry.Size, PixelWidth: geometry.PixelWidth, PixelHeight: geometry.PixelHeight}
2980- if err != nil {2921+ if err := protocol.ValidateGeometry(message.Geometry()); err != nil {
2981- log.Error("encoding terminal resize", "error", err)2922+ log.Error("validating terminal resize", "error", err)
2982 continue2923 continue
2983 }2924 }
2984- frame := wire.Frame{Type: wire.MsgResize, Payload: payload}
2985 if !sendLease.send(func() bool {2925 if !sendLease.send(func() bool {
2986 select {2926 select {
2987- case out <- frame:2927+ case out <- message:
2988 return true2928 return true
2989 case <-ctx.Done():2929 case <-ctx.Done():
2990 return false2930 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 +5 -9
@@ -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,7 @@ 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 {
87- return boundedHandshakeOperationWithTransition(ctx, transport, operation, nil)
88-}
89-
90-func boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.Transport, operation func() error, transition *transitionUI) error {
86+func boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.ClientConnection, operation func() error, transition *transitionUI) error {
9187 if err := ctx.Err(); err != nil {
9288 _ = transport.Close()
9389 return err
@@ -116,16 +112,16 @@ func boundedHandshakeOperationWithTransition(ctx context.Context, transport port
116112 }
117113 }
118114
119-func boundedDial(ctx context.Context, dialer ports.Dialer) (ports.Transport, error) {
115+func boundedDial(ctx context.Context, dialer ports.ClientDialer) (ports.ClientConnection, error) {
120116 return boundedDialWithTransition(ctx, dialer, nil)
121117 }
122118
123-func boundedDialWithTransition(ctx context.Context, dialer ports.Dialer, transition *transitionUI) (ports.Transport, error) {
119+func boundedDialWithTransition(ctx context.Context, dialer ports.ClientDialer, transition *transitionUI) (ports.ClientConnection, error) {
124120 // The dial context only bounds startup. A successful carriage outlives the
125121 // handshake context; canceling that context after Welcome must not kill SSH.
126122 dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
127123 type dialResult struct {
128- transport ports.Transport
124+ transport ports.ClientConnection
129125 err error
130126 }
131127 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,7 @@ 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 boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.ClientConnection, operation func() error, transition *transitionUI) error {
87- return boundedHandshakeOperationWithTransition(ctx, transport, operation, nil)
88-}
89-
90-func boundedHandshakeOperationWithTransition(ctx context.Context, transport ports.Transport, operation func() error, transition *transitionUI) error {
91 if err := ctx.Err(); err != nil {87 if err := ctx.Err(); err != nil {
92 _ = transport.Close()88 _ = transport.Close()
93 return err89 return err
@@ -116,16 +112,16 @@ func boundedHandshakeOperationWithTransition(ctx context.Context, transport port
116 }112 }
117 }113 }
118 114
119-func boundedDial(ctx context.Context, dialer ports.Dialer) (ports.Transport, error) {115+func boundedDial(ctx context.Context, dialer ports.ClientDialer) (ports.ClientConnection, error) {
120 return boundedDialWithTransition(ctx, dialer, nil)116 return boundedDialWithTransition(ctx, dialer, nil)
121 }117 }
122 118
123-func boundedDialWithTransition(ctx context.Context, dialer ports.Dialer, transition *transitionUI) (ports.Transport, error) {119+func boundedDialWithTransition(ctx context.Context, dialer ports.ClientDialer, transition *transitionUI) (ports.ClientConnection, error) {
124 // The dial context only bounds startup. A successful carriage outlives the120 // The dial context only bounds startup. A successful carriage outlives the
125 // handshake context; canceling that context after Welcome must not kill SSH.121 // handshake context; canceling that context after Welcome must not kill SSH.
126 dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))122 dialCtx, cancel := context.WithCancel(context.WithoutCancel(ctx))
127 type dialResult struct {123 type dialResult struct {
128- transport ports.Transport124+ transport ports.ClientConnection
129 err error125 err error
130 }126 }
131 result := make(chan dialResult, 1)127 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 +237 -0
@@ -0,0 +1,237 @@
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+ _, dgram := raw.(ports.DatagramTransport)
93+ _, link := raw.(ports.LinkStateReporter)
94+ window := uint8(protocol.MaxOutputWindow)
95+ if dgram {
96+ window = 1
97+ }
98+ return protocol.ConnectionCapabilities{
99+ OutputDataLimit: protocol.MaxOutputDataLen,
100+ PreferredOutputWindow: window,
101+ LinkState: link,
102+ }
103+}
104+func externalClientFrame(m protocol.ClientMessage) (wire.Frame, error) {
105+ switch x := m.(type) {
106+ case protocol.Hello:
107+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(x)}, nil
108+ case protocol.Input:
109+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(x)}, nil
110+ case protocol.Resize:
111+ p, e := wire.MarshalResize(x)
112+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
113+ case protocol.Detach:
114+ return wire.Frame{Type: wire.MsgDetach}, nil
115+ case protocol.Theme:
116+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(x)}, nil
117+ case protocol.Ack:
118+ p, e := wire.MarshalAck(x)
119+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
120+ case protocol.ImagePush:
121+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(x)}, nil
122+ case protocol.ClientNotice:
123+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(x)}, nil
124+ case protocol.OutputResetRequest:
125+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
126+ case protocol.RecentRouteSnapshot:
127+ p, e := wire.MarshalRecentRouteSnapshot(x)
128+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
129+ case protocol.RouteAttentionSubscription:
130+ p, e := wire.MarshalRouteAttentionSubscription(x)
131+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
132+ case protocol.SamePeerSwitchRequest:
133+ p, e := wire.MarshalSamePeerSwitchRequest(x)
134+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
135+ case protocol.ParkedRouteRequest:
136+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(x)}, nil
137+ case protocol.RouteNavigationFailure:
138+ p, e := wire.MarshalRouteNavigationFailure(x)
139+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
140+ default:
141+ return wire.Frame{}, errors.New("test client: unsupported message")
142+ }
143+}
144+func externalServerMessage(f wire.Frame) (protocol.ServerMessage, error) {
145+ switch f.Type {
146+ case wire.MsgWelcome:
147+ return wire.UnmarshalWelcome(f.Payload)
148+ case wire.MsgError:
149+ return wire.UnmarshalErrorMsg(f.Payload)
150+ case wire.MsgOutput:
151+ return wire.UnmarshalOutput(f.Payload)
152+ case wire.MsgDetached:
153+ return wire.UnmarshalDetached(f.Payload)
154+ case wire.MsgPong:
155+ return wire.UnmarshalPong(f.Payload)
156+ case wire.MsgAttachTarget:
157+ return wire.UnmarshalAttachTarget(f.Payload)
158+ case wire.MsgNavigationAction:
159+ return wire.UnmarshalNavigationDirective(f.Payload)
160+ case wire.MsgParkedRouteResponse:
161+ return wire.UnmarshalParkedRouteResponse(f.Payload)
162+ case wire.MsgNavigateRecentRoute:
163+ return wire.UnmarshalRouteNavigationAction(f.Payload)
164+ case wire.MsgCommittedRouteIdentity:
165+ return wire.UnmarshalCommittedRouteIdentity(f.Payload)
166+ case wire.MsgSamePeerSwitchFailure:
167+ return wire.UnmarshalSamePeerSwitchFailure(f.Payload)
168+ case wire.MsgRoutePosition:
169+ return wire.UnmarshalRoutePosition(f.Payload)
170+ case wire.MsgRouteNavigationFailure:
171+ return wire.UnmarshalRouteNavigationFailure(f.Payload)
172+ default:
173+ return nil, errors.New("test client: unsupported server frame")
174+ }
175+}
176+
177+func (t *markedDatagramTransport) SendClient(m protocol.ClientMessage) error {
178+ f, e := externalClientFrame(m)
179+ if e != nil {
180+ return e
181+ }
182+ return t.Send(f)
183+}
184+func (t *markedDatagramTransport) ReceiveServer() (protocol.ServerMessage, error) {
185+ f, e := t.Recv()
186+ if e != nil {
187+ return nil, e
188+ }
189+ return externalServerMessage(f)
190+}
191+func (t *markedDatagramTransport) Capabilities() protocol.ConnectionCapabilities {
192+ return externalCapabilities(t)
193+}
194+func (t *markedDatagramTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
195+func (t *markedDatagramTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
196+
197+func (t *recordingTransport) SendClient(m protocol.ClientMessage) error {
198+ f, e := externalClientFrame(m)
199+ if e != nil {
200+ return e
201+ }
202+ return t.Send(f)
203+}
204+func (t *recordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
205+ f, e := t.Recv()
206+ if e != nil {
207+ return nil, e
208+ }
209+ return externalServerMessage(f)
210+}
211+func (t *recordingTransport) Capabilities() protocol.ConnectionCapabilities {
212+ return externalCapabilities(t)
213+}
214+func (t *recordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
215+func (t *recordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
216+
217+func (t *clipboardToastLifecycleTransport) SendClient(m protocol.ClientMessage) error {
218+ f, e := externalClientFrame(m)
219+ if e != nil {
220+ return e
221+ }
222+ return t.Send(f)
223+}
224+func (t *clipboardToastLifecycleTransport) ReceiveServer() (protocol.ServerMessage, error) {
225+ f, e := t.Recv()
226+ if e != nil {
227+ return nil, e
228+ }
229+ return externalServerMessage(f)
230+}
231+func (t *clipboardToastLifecycleTransport) Capabilities() protocol.ConnectionCapabilities {
232+ return externalCapabilities(t)
233+}
234+func (t *clipboardToastLifecycleTransport) LinkState() ports.LinkState {
235+ return ports.LinkStateConnected
236+}
237+func (t *clipboardToastLifecycleTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
@@ -0,0 +1,237 @@
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+ _, dgram := raw.(ports.DatagramTransport)
93+ _, link := raw.(ports.LinkStateReporter)
94+ window := uint8(protocol.MaxOutputWindow)
95+ if dgram {
96+ window = 1
97+ }
98+ return protocol.ConnectionCapabilities{
99+ OutputDataLimit: protocol.MaxOutputDataLen,
100+ PreferredOutputWindow: window,
101+ LinkState: link,
102+ }
103+}
104+func externalClientFrame(m protocol.ClientMessage) (wire.Frame, error) {
105+ switch x := m.(type) {
106+ case protocol.Hello:
107+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(x)}, nil
108+ case protocol.Input:
109+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(x)}, nil
110+ case protocol.Resize:
111+ p, e := wire.MarshalResize(x)
112+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
113+ case protocol.Detach:
114+ return wire.Frame{Type: wire.MsgDetach}, nil
115+ case protocol.Theme:
116+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(x)}, nil
117+ case protocol.Ack:
118+ p, e := wire.MarshalAck(x)
119+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
120+ case protocol.ImagePush:
121+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(x)}, nil
122+ case protocol.ClientNotice:
123+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(x)}, nil
124+ case protocol.OutputResetRequest:
125+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
126+ case protocol.RecentRouteSnapshot:
127+ p, e := wire.MarshalRecentRouteSnapshot(x)
128+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
129+ case protocol.RouteAttentionSubscription:
130+ p, e := wire.MarshalRouteAttentionSubscription(x)
131+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
132+ case protocol.SamePeerSwitchRequest:
133+ p, e := wire.MarshalSamePeerSwitchRequest(x)
134+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
135+ case protocol.ParkedRouteRequest:
136+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(x)}, nil
137+ case protocol.RouteNavigationFailure:
138+ p, e := wire.MarshalRouteNavigationFailure(x)
139+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
140+ default:
141+ return wire.Frame{}, errors.New("test client: unsupported message")
142+ }
143+}
144+func externalServerMessage(f wire.Frame) (protocol.ServerMessage, error) {
145+ switch f.Type {
146+ case wire.MsgWelcome:
147+ return wire.UnmarshalWelcome(f.Payload)
148+ case wire.MsgError:
149+ return wire.UnmarshalErrorMsg(f.Payload)
150+ case wire.MsgOutput:
151+ return wire.UnmarshalOutput(f.Payload)
152+ case wire.MsgDetached:
153+ return wire.UnmarshalDetached(f.Payload)
154+ case wire.MsgPong:
155+ return wire.UnmarshalPong(f.Payload)
156+ case wire.MsgAttachTarget:
157+ return wire.UnmarshalAttachTarget(f.Payload)
158+ case wire.MsgNavigationAction:
159+ return wire.UnmarshalNavigationDirective(f.Payload)
160+ case wire.MsgParkedRouteResponse:
161+ return wire.UnmarshalParkedRouteResponse(f.Payload)
162+ case wire.MsgNavigateRecentRoute:
163+ return wire.UnmarshalRouteNavigationAction(f.Payload)
164+ case wire.MsgCommittedRouteIdentity:
165+ return wire.UnmarshalCommittedRouteIdentity(f.Payload)
166+ case wire.MsgSamePeerSwitchFailure:
167+ return wire.UnmarshalSamePeerSwitchFailure(f.Payload)
168+ case wire.MsgRoutePosition:
169+ return wire.UnmarshalRoutePosition(f.Payload)
170+ case wire.MsgRouteNavigationFailure:
171+ return wire.UnmarshalRouteNavigationFailure(f.Payload)
172+ default:
173+ return nil, errors.New("test client: unsupported server frame")
174+ }
175+}
176+
177+func (t *markedDatagramTransport) SendClient(m protocol.ClientMessage) error {
178+ f, e := externalClientFrame(m)
179+ if e != nil {
180+ return e
181+ }
182+ return t.Send(f)
183+}
184+func (t *markedDatagramTransport) ReceiveServer() (protocol.ServerMessage, error) {
185+ f, e := t.Recv()
186+ if e != nil {
187+ return nil, e
188+ }
189+ return externalServerMessage(f)
190+}
191+func (t *markedDatagramTransport) Capabilities() protocol.ConnectionCapabilities {
192+ return externalCapabilities(t)
193+}
194+func (t *markedDatagramTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
195+func (t *markedDatagramTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
196+
197+func (t *recordingTransport) SendClient(m protocol.ClientMessage) error {
198+ f, e := externalClientFrame(m)
199+ if e != nil {
200+ return e
201+ }
202+ return t.Send(f)
203+}
204+func (t *recordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
205+ f, e := t.Recv()
206+ if e != nil {
207+ return nil, e
208+ }
209+ return externalServerMessage(f)
210+}
211+func (t *recordingTransport) Capabilities() protocol.ConnectionCapabilities {
212+ return externalCapabilities(t)
213+}
214+func (t *recordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
215+func (t *recordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
216+
217+func (t *clipboardToastLifecycleTransport) SendClient(m protocol.ClientMessage) error {
218+ f, e := externalClientFrame(m)
219+ if e != nil {
220+ return e
221+ }
222+ return t.Send(f)
223+}
224+func (t *clipboardToastLifecycleTransport) ReceiveServer() (protocol.ServerMessage, error) {
225+ f, e := t.Recv()
226+ if e != nil {
227+ return nil, e
228+ }
229+ return externalServerMessage(f)
230+}
231+func (t *clipboardToastLifecycleTransport) Capabilities() protocol.ConnectionCapabilities {
232+ return externalCapabilities(t)
233+}
234+func (t *clipboardToastLifecycleTransport) LinkState() ports.LinkState {
235+ return ports.LinkStateConnected
236+}
237+func (t *clipboardToastLifecycleTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
A internal/usecase/client/typed_connection_test.go +374 -0
@@ -0,0 +1,374 @@
1+package client
2+
3+import (
4+ "bytes"
5+ "context"
6+ "errors"
7+ "io"
8+ "log/slog"
9+ "testing"
10+
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+
14+ "github.com/bnema/vev/internal/ports"
15+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
16+ "github.com/bnema/vev/internal/protocol"
17+ "github.com/bnema/vev/internal/protocol/wire"
18+)
19+
20+type mockTestingT interface {
21+ mock.TestingT
22+ Cleanup(func())
23+}
24+
25+type mockClientConnection struct{ *portsmocks.MockTransport }
26+
27+func newMockClientConnection(t mockTestingT) *mockClientConnection {
28+ return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)}
29+}
30+
31+func (c *mockClientConnection) SendClient(message protocol.ClientMessage) error {
32+ frame, err := testClientFrame(message)
33+ if err != nil {
34+ return err
35+ }
36+ return c.MockTransport.Send(frame)
37+}
38+func (c *mockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
39+ frame, err := c.MockTransport.Recv()
40+ if err != nil {
41+ return nil, err
42+ }
43+ return testServerMessage(frame)
44+}
45+func (c *mockClientConnection) Capabilities() protocol.ConnectionCapabilities {
46+ return testClientCapabilities(c.MockTransport)
47+}
48+func (c *mockClientConnection) LinkState() ports.LinkState {
49+ return testClientLinkState(c.MockTransport)
50+}
51+func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent {
52+ return testClientLinkEvents(c.MockTransport)
53+}
54+
55+type rawClientConnection struct{ raw ports.Transport }
56+
57+func (c *rawClientConnection) SendClient(message protocol.ClientMessage) error {
58+ frame, err := testClientFrame(message)
59+ if err != nil {
60+ return err
61+ }
62+ return c.raw.Send(frame)
63+}
64+func (c *rawClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
65+ frame, err := c.raw.Recv()
66+ if err != nil {
67+ return nil, err
68+ }
69+ return testServerMessage(frame)
70+}
71+func (c *rawClientConnection) Capabilities() protocol.ConnectionCapabilities {
72+ return testClientCapabilities(c.raw)
73+}
74+func (c *rawClientConnection) LinkState() ports.LinkState { return testClientLinkState(c.raw) }
75+func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent { return testClientLinkEvents(c.raw) }
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, err := d.MockDialer.Dial(ctx)
85+ if err != nil {
86+ return nil, err
87+ }
88+ return &rawClientConnection{raw: raw}, nil
89+}
90+
91+func testClientFrame(message protocol.ClientMessage) (wire.Frame, error) {
92+ switch m := message.(type) {
93+ case protocol.Hello:
94+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(m)}, nil
95+ case protocol.Input:
96+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(m)}, nil
97+ case protocol.Resize:
98+ p, e := wire.MarshalResize(m)
99+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
100+ case protocol.Detach:
101+ return wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(m)}, nil
102+ case protocol.Ping:
103+ return wire.Frame{Type: wire.MsgPing, Payload: wire.MarshalPing(m)}, nil
104+ case protocol.List:
105+ return wire.Frame{Type: wire.MsgList, Payload: wire.MarshalList(m)}, nil
106+ case protocol.Kill:
107+ return wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(m)}, nil
108+ case protocol.Theme:
109+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(m)}, nil
110+ case protocol.Ack:
111+ p, e := wire.MarshalAck(m)
112+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
113+ case protocol.ImagePush:
114+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(m)}, nil
115+ case protocol.ClientNotice:
116+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(m)}, nil
117+ case protocol.CommandRequest:
118+ p, e := wire.MarshalCommandRequest(m)
119+ return wire.Frame{Type: wire.MsgCommand, Payload: p}, e
120+ case protocol.OutputResetRequest:
121+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
122+ case protocol.RemotePreviewRequest:
123+ return wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: wire.MarshalRemotePreviewRequest(m)}, nil
124+ case protocol.RouteAttentionSubscription:
125+ p, e := wire.MarshalRouteAttentionSubscription(m)
126+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
127+ case protocol.SamePeerSwitchRequest:
128+ p, e := wire.MarshalSamePeerSwitchRequest(m)
129+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
130+ case protocol.ParkedRouteRequest:
131+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(m)}, nil
132+ case protocol.RecentRouteSnapshot:
133+ p, e := wire.MarshalRecentRouteSnapshot(m)
134+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
135+ case protocol.RouteNavigationFailure:
136+ p, e := wire.MarshalRouteNavigationFailure(m)
137+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
138+ default:
139+ return wire.Frame{}, errors.New("test client connection: unsupported client message")
140+ }
141+}
142+
143+func testServerMessage(frame wire.Frame) (protocol.ServerMessage, error) {
144+ switch frame.Type {
145+ case wire.MsgWelcome:
146+ return wire.UnmarshalWelcome(frame.Payload)
147+ case wire.MsgError:
148+ return wire.UnmarshalErrorMsg(frame.Payload)
149+ case wire.MsgOutput:
150+ return wire.UnmarshalOutput(frame.Payload)
151+ case wire.MsgDetached:
152+ return wire.UnmarshalDetached(frame.Payload)
153+ case wire.MsgPong:
154+ return wire.UnmarshalPong(frame.Payload)
155+ case wire.MsgSessions:
156+ return wire.UnmarshalSessions(frame.Payload)
157+ case wire.MsgCommandResult:
158+ return wire.UnmarshalCommandResult(frame.Payload)
159+ case wire.MsgNavigationAction:
160+ return wire.UnmarshalNavigationDirective(frame.Payload)
161+ case wire.MsgAttachTarget:
162+ return wire.UnmarshalAttachTarget(frame.Payload)
163+ case wire.MsgRemotePreviewResponse:
164+ return wire.UnmarshalRemotePreview(frame.Payload)
165+ case wire.MsgCommittedRouteIdentity:
166+ return wire.UnmarshalCommittedRouteIdentity(frame.Payload)
167+ case wire.MsgNavigateRecentRoute:
168+ return wire.UnmarshalRouteNavigationAction(frame.Payload)
169+ case wire.MsgRouteNavigationFailure:
170+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
171+ case wire.MsgRoutePosition:
172+ return wire.UnmarshalRoutePosition(frame.Payload)
173+ case wire.MsgSamePeerSwitchFailure:
174+ return wire.UnmarshalSamePeerSwitchFailure(frame.Payload)
175+ case wire.MsgParkedRouteResponse:
176+ return wire.UnmarshalParkedRouteResponse(frame.Payload)
177+ default:
178+ return nil, errors.New("test client connection: unsupported server frame")
179+ }
180+}
181+
182+func testClientCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
183+ _, dgram := raw.(ports.DatagramTransport)
184+ _, link := raw.(ports.LinkStateReporter)
185+ window := uint8(protocol.MaxOutputWindow)
186+ if dgram {
187+ window = 1
188+ }
189+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, LinkState: link}
190+}
191+func testClientLinkState(raw ports.Transport) ports.LinkState {
192+ if r, ok := raw.(ports.LinkStateReporter); ok {
193+ return r.LinkState()
194+ }
195+ return ports.LinkStateConnected
196+}
197+func testClientLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
198+ if r, ok := raw.(ports.LinkStateReporter); ok {
199+ return r.LinkEvents()
200+ }
201+ return nil
202+}
203+
204+func TestRunRecvLogsIgnoredTypedBoundaryFailures(t *testing.T) {
205+ connection := portsmocks.NewMockClientConnection(t)
206+ connection.EXPECT().ReceiveServer().Return(nil, &protocol.DecodeFailure{Category: protocol.DecodeUnknownType, Type: 255}).Once()
207+ connection.EXPECT().ReceiveServer().Return(nil, &protocol.DecodeFailure{Category: protocol.DecodeWrongDirection, Type: uint8(wire.MsgInput)}).Once()
208+ connection.EXPECT().ReceiveServer().Return(protocol.Pong{}, nil).Once()
209+ connection.EXPECT().ReceiveServer().Return(nil, io.EOF).Once()
210+ results := make(chan recvResult, 2)
211+ failed := make(chan struct{})
212+ var logs bytes.Buffer
213+ log := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))
214+ runRecv(context.Background(), connection, results, failed, log)
215+ first := <-results
216+ require.Equal(t, protocol.Pong{}, first.message)
217+ require.NoError(t, first.err)
218+ second := <-results
219+ require.ErrorIs(t, second.err, io.EOF)
220+ select {
221+ case <-failed:
222+ default:
223+ t.Fatal("terminal receive failure was not reported")
224+ }
225+ require.Contains(t, logs.String(), "ignoring rejected server message")
226+ require.Contains(t, logs.String(), "category=2")
227+ require.Contains(t, logs.String(), "category=3")
228+ require.Contains(t, logs.String(), "type=255")
229+ require.Contains(t, logs.String(), "type=2")
230+}
231+
232+func (t *ackRecordingTransport) SendClient(m protocol.ClientMessage) error {
233+ f, e := testClientFrame(m)
234+ if e != nil {
235+ return e
236+ }
237+ return t.Send(f)
238+}
239+func (t *ackRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
240+ f, e := t.Recv()
241+ if e != nil {
242+ return nil, e
243+ }
244+ return testServerMessage(f)
245+}
246+func (t *ackRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
247+ return testClientCapabilities(t)
248+}
249+func (t *ackRecordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
250+func (t *ackRecordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
251+
252+func (t *reconnectToastLinkTransport) SendClient(m protocol.ClientMessage) error {
253+ f, e := testClientFrame(m)
254+ if e != nil {
255+ return e
256+ }
257+ return t.Send(f)
258+}
259+func (t *reconnectToastLinkTransport) ReceiveServer() (protocol.ServerMessage, error) {
260+ f, e := t.Recv()
261+ if e != nil {
262+ return nil, e
263+ }
264+ return testServerMessage(f)
265+}
266+func (t *reconnectToastLinkTransport) Capabilities() protocol.ConnectionCapabilities {
267+ return testClientCapabilities(t)
268+}
269+
270+func (t *reconnectToastRecordingTransport) SendClient(m protocol.ClientMessage) error {
271+ f, e := testClientFrame(m)
272+ if e != nil {
273+ return e
274+ }
275+ return t.Send(f)
276+}
277+func (t *reconnectToastRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
278+ f, e := t.Recv()
279+ if e != nil {
280+ return nil, e
281+ }
282+ return testServerMessage(f)
283+}
284+func (t *reconnectToastRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
285+ return testClientCapabilities(t)
286+}
287+func (t *reconnectToastRecordingTransport) LinkState() ports.LinkState {
288+ return ports.LinkStateConnected
289+}
290+func (t *reconnectToastRecordingTransport) LinkEvents() <-chan ports.LinkEvent {
291+ return nil
292+}
293+
294+func (t *reconnectResetBlockingTransport) SendClient(m protocol.ClientMessage) error {
295+ f, e := testClientFrame(m)
296+ if e != nil {
297+ return e
298+ }
299+ return t.Send(f)
300+}
301+func (t *reconnectResetBlockingTransport) ReceiveServer() (protocol.ServerMessage, error) {
302+ f, e := t.Recv()
303+ if e != nil {
304+ return nil, e
305+ }
306+ return testServerMessage(f)
307+}
308+func (t *reconnectResetBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
309+ return testClientCapabilities(t)
310+}
311+func (t *reconnectResetBlockingTransport) LinkState() ports.LinkState {
312+ return ports.LinkStateConnected
313+}
314+func (t *reconnectResetBlockingTransport) LinkEvents() <-chan ports.LinkEvent {
315+ return nil
316+}
317+
318+func (t *reconnectToastBlockingSendTransport) SendClient(m protocol.ClientMessage) error {
319+ f, e := testClientFrame(m)
320+ if e != nil {
321+ return e
322+ }
323+ return t.Send(f)
324+}
325+func (t *reconnectToastBlockingSendTransport) ReceiveServer() (protocol.ServerMessage, error) {
326+ f, e := t.Recv()
327+ if e != nil {
328+ return nil, e
329+ }
330+ return testServerMessage(f)
331+}
332+func (t *reconnectToastBlockingSendTransport) Capabilities() protocol.ConnectionCapabilities {
333+ return testClientCapabilities(t)
334+}
335+
336+func (t *paletteAttachTransport) SendClient(m protocol.ClientMessage) error {
337+ f, e := testClientFrame(m)
338+ if e != nil {
339+ return e
340+ }
341+ return t.Send(f)
342+}
343+func (t *paletteAttachTransport) ReceiveServer() (protocol.ServerMessage, error) {
344+ f, e := t.Recv()
345+ if e != nil {
346+ return nil, e
347+ }
348+ return testServerMessage(f)
349+}
350+func (t *paletteAttachTransport) Capabilities() protocol.ConnectionCapabilities {
351+ return testClientCapabilities(t)
352+}
353+func (t *paletteAttachTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
354+func (t *paletteAttachTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
355+
356+func (t *attachPaletteTransport) SendClient(m protocol.ClientMessage) error {
357+ f, e := testClientFrame(m)
358+ if e != nil {
359+ return e
360+ }
361+ return t.Send(f)
362+}
363+func (t *attachPaletteTransport) ReceiveServer() (protocol.ServerMessage, error) {
364+ f, e := t.Recv()
365+ if e != nil {
366+ return nil, e
367+ }
368+ return testServerMessage(f)
369+}
370+func (t *attachPaletteTransport) Capabilities() protocol.ConnectionCapabilities {
371+ return testClientCapabilities(t)
372+}
373+func (t *attachPaletteTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
374+func (t *attachPaletteTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
@@ -0,0 +1,374 @@
1+package client
2+
3+import (
4+ "bytes"
5+ "context"
6+ "errors"
7+ "io"
8+ "log/slog"
9+ "testing"
10+
11+ "github.com/stretchr/testify/mock"
12+ "github.com/stretchr/testify/require"
13+
14+ "github.com/bnema/vev/internal/ports"
15+ portsmocks "github.com/bnema/vev/internal/ports/mocks"
16+ "github.com/bnema/vev/internal/protocol"
17+ "github.com/bnema/vev/internal/protocol/wire"
18+)
19+
20+type mockTestingT interface {
21+ mock.TestingT
22+ Cleanup(func())
23+}
24+
25+type mockClientConnection struct{ *portsmocks.MockTransport }
26+
27+func newMockClientConnection(t mockTestingT) *mockClientConnection {
28+ return &mockClientConnection{MockTransport: portsmocks.NewMockTransport(t)}
29+}
30+
31+func (c *mockClientConnection) SendClient(message protocol.ClientMessage) error {
32+ frame, err := testClientFrame(message)
33+ if err != nil {
34+ return err
35+ }
36+ return c.MockTransport.Send(frame)
37+}
38+func (c *mockClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
39+ frame, err := c.MockTransport.Recv()
40+ if err != nil {
41+ return nil, err
42+ }
43+ return testServerMessage(frame)
44+}
45+func (c *mockClientConnection) Capabilities() protocol.ConnectionCapabilities {
46+ return testClientCapabilities(c.MockTransport)
47+}
48+func (c *mockClientConnection) LinkState() ports.LinkState {
49+ return testClientLinkState(c.MockTransport)
50+}
51+func (c *mockClientConnection) LinkEvents() <-chan ports.LinkEvent {
52+ return testClientLinkEvents(c.MockTransport)
53+}
54+
55+type rawClientConnection struct{ raw ports.Transport }
56+
57+func (c *rawClientConnection) SendClient(message protocol.ClientMessage) error {
58+ frame, err := testClientFrame(message)
59+ if err != nil {
60+ return err
61+ }
62+ return c.raw.Send(frame)
63+}
64+func (c *rawClientConnection) ReceiveServer() (protocol.ServerMessage, error) {
65+ frame, err := c.raw.Recv()
66+ if err != nil {
67+ return nil, err
68+ }
69+ return testServerMessage(frame)
70+}
71+func (c *rawClientConnection) Capabilities() protocol.ConnectionCapabilities {
72+ return testClientCapabilities(c.raw)
73+}
74+func (c *rawClientConnection) LinkState() ports.LinkState { return testClientLinkState(c.raw) }
75+func (c *rawClientConnection) LinkEvents() <-chan ports.LinkEvent { return testClientLinkEvents(c.raw) }
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, err := d.MockDialer.Dial(ctx)
85+ if err != nil {
86+ return nil, err
87+ }
88+ return &rawClientConnection{raw: raw}, nil
89+}
90+
91+func testClientFrame(message protocol.ClientMessage) (wire.Frame, error) {
92+ switch m := message.(type) {
93+ case protocol.Hello:
94+ return wire.Frame{Type: wire.MsgHello, Payload: wire.MarshalHello(m)}, nil
95+ case protocol.Input:
96+ return wire.Frame{Type: wire.MsgInput, Payload: wire.MarshalInput(m)}, nil
97+ case protocol.Resize:
98+ p, e := wire.MarshalResize(m)
99+ return wire.Frame{Type: wire.MsgResize, Payload: p}, e
100+ case protocol.Detach:
101+ return wire.Frame{Type: wire.MsgDetach, Payload: wire.MarshalDetach(m)}, nil
102+ case protocol.Ping:
103+ return wire.Frame{Type: wire.MsgPing, Payload: wire.MarshalPing(m)}, nil
104+ case protocol.List:
105+ return wire.Frame{Type: wire.MsgList, Payload: wire.MarshalList(m)}, nil
106+ case protocol.Kill:
107+ return wire.Frame{Type: wire.MsgKill, Payload: wire.MarshalKill(m)}, nil
108+ case protocol.Theme:
109+ return wire.Frame{Type: wire.MsgTheme, Payload: wire.MarshalTheme(m)}, nil
110+ case protocol.Ack:
111+ p, e := wire.MarshalAck(m)
112+ return wire.Frame{Type: wire.MsgAck, Payload: p}, e
113+ case protocol.ImagePush:
114+ return wire.Frame{Type: wire.MsgImagePush, Payload: wire.MarshalImagePush(m)}, nil
115+ case protocol.ClientNotice:
116+ return wire.Frame{Type: wire.MsgClientNotice, Payload: wire.MarshalClientNotice(m)}, nil
117+ case protocol.CommandRequest:
118+ p, e := wire.MarshalCommandRequest(m)
119+ return wire.Frame{Type: wire.MsgCommand, Payload: p}, e
120+ case protocol.OutputResetRequest:
121+ return wire.Frame{Type: wire.MsgOutputResetRequest}, nil
122+ case protocol.RemotePreviewRequest:
123+ return wire.Frame{Type: wire.MsgRemotePreviewRequest, Payload: wire.MarshalRemotePreviewRequest(m)}, nil
124+ case protocol.RouteAttentionSubscription:
125+ p, e := wire.MarshalRouteAttentionSubscription(m)
126+ return wire.Frame{Type: wire.MsgRouteAttentionSubscription, Payload: p}, e
127+ case protocol.SamePeerSwitchRequest:
128+ p, e := wire.MarshalSamePeerSwitchRequest(m)
129+ return wire.Frame{Type: wire.MsgSamePeerSwitchRequest, Payload: p}, e
130+ case protocol.ParkedRouteRequest:
131+ return wire.Frame{Type: wire.MsgParkedRouteRequest, Payload: wire.MarshalParkedRouteRequest(m)}, nil
132+ case protocol.RecentRouteSnapshot:
133+ p, e := wire.MarshalRecentRouteSnapshot(m)
134+ return wire.Frame{Type: wire.MsgRecentRouteSnapshot, Payload: p}, e
135+ case protocol.RouteNavigationFailure:
136+ p, e := wire.MarshalRouteNavigationFailure(m)
137+ return wire.Frame{Type: wire.MsgRouteNavigationFailure, Payload: p}, e
138+ default:
139+ return wire.Frame{}, errors.New("test client connection: unsupported client message")
140+ }
141+}
142+
143+func testServerMessage(frame wire.Frame) (protocol.ServerMessage, error) {
144+ switch frame.Type {
145+ case wire.MsgWelcome:
146+ return wire.UnmarshalWelcome(frame.Payload)
147+ case wire.MsgError:
148+ return wire.UnmarshalErrorMsg(frame.Payload)
149+ case wire.MsgOutput:
150+ return wire.UnmarshalOutput(frame.Payload)
151+ case wire.MsgDetached:
152+ return wire.UnmarshalDetached(frame.Payload)
153+ case wire.MsgPong:
154+ return wire.UnmarshalPong(frame.Payload)
155+ case wire.MsgSessions:
156+ return wire.UnmarshalSessions(frame.Payload)
157+ case wire.MsgCommandResult:
158+ return wire.UnmarshalCommandResult(frame.Payload)
159+ case wire.MsgNavigationAction:
160+ return wire.UnmarshalNavigationDirective(frame.Payload)
161+ case wire.MsgAttachTarget:
162+ return wire.UnmarshalAttachTarget(frame.Payload)
163+ case wire.MsgRemotePreviewResponse:
164+ return wire.UnmarshalRemotePreview(frame.Payload)
165+ case wire.MsgCommittedRouteIdentity:
166+ return wire.UnmarshalCommittedRouteIdentity(frame.Payload)
167+ case wire.MsgNavigateRecentRoute:
168+ return wire.UnmarshalRouteNavigationAction(frame.Payload)
169+ case wire.MsgRouteNavigationFailure:
170+ return wire.UnmarshalRouteNavigationFailure(frame.Payload)
171+ case wire.MsgRoutePosition:
172+ return wire.UnmarshalRoutePosition(frame.Payload)
173+ case wire.MsgSamePeerSwitchFailure:
174+ return wire.UnmarshalSamePeerSwitchFailure(frame.Payload)
175+ case wire.MsgParkedRouteResponse:
176+ return wire.UnmarshalParkedRouteResponse(frame.Payload)
177+ default:
178+ return nil, errors.New("test client connection: unsupported server frame")
179+ }
180+}
181+
182+func testClientCapabilities(raw ports.Transport) protocol.ConnectionCapabilities {
183+ _, dgram := raw.(ports.DatagramTransport)
184+ _, link := raw.(ports.LinkStateReporter)
185+ window := uint8(protocol.MaxOutputWindow)
186+ if dgram {
187+ window = 1
188+ }
189+ return protocol.ConnectionCapabilities{OutputDataLimit: protocol.MaxOutputDataLen, PreferredOutputWindow: window, LinkState: link}
190+}
191+func testClientLinkState(raw ports.Transport) ports.LinkState {
192+ if r, ok := raw.(ports.LinkStateReporter); ok {
193+ return r.LinkState()
194+ }
195+ return ports.LinkStateConnected
196+}
197+func testClientLinkEvents(raw ports.Transport) <-chan ports.LinkEvent {
198+ if r, ok := raw.(ports.LinkStateReporter); ok {
199+ return r.LinkEvents()
200+ }
201+ return nil
202+}
203+
204+func TestRunRecvLogsIgnoredTypedBoundaryFailures(t *testing.T) {
205+ connection := portsmocks.NewMockClientConnection(t)
206+ connection.EXPECT().ReceiveServer().Return(nil, &protocol.DecodeFailure{Category: protocol.DecodeUnknownType, Type: 255}).Once()
207+ connection.EXPECT().ReceiveServer().Return(nil, &protocol.DecodeFailure{Category: protocol.DecodeWrongDirection, Type: uint8(wire.MsgInput)}).Once()
208+ connection.EXPECT().ReceiveServer().Return(protocol.Pong{}, nil).Once()
209+ connection.EXPECT().ReceiveServer().Return(nil, io.EOF).Once()
210+ results := make(chan recvResult, 2)
211+ failed := make(chan struct{})
212+ var logs bytes.Buffer
213+ log := slog.New(slog.NewTextHandler(&logs, &slog.HandlerOptions{Level: slog.LevelDebug}))
214+ runRecv(context.Background(), connection, results, failed, log)
215+ first := <-results
216+ require.Equal(t, protocol.Pong{}, first.message)
217+ require.NoError(t, first.err)
218+ second := <-results
219+ require.ErrorIs(t, second.err, io.EOF)
220+ select {
221+ case <-failed:
222+ default:
223+ t.Fatal("terminal receive failure was not reported")
224+ }
225+ require.Contains(t, logs.String(), "ignoring rejected server message")
226+ require.Contains(t, logs.String(), "category=2")
227+ require.Contains(t, logs.String(), "category=3")
228+ require.Contains(t, logs.String(), "type=255")
229+ require.Contains(t, logs.String(), "type=2")
230+}
231+
232+func (t *ackRecordingTransport) SendClient(m protocol.ClientMessage) error {
233+ f, e := testClientFrame(m)
234+ if e != nil {
235+ return e
236+ }
237+ return t.Send(f)
238+}
239+func (t *ackRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
240+ f, e := t.Recv()
241+ if e != nil {
242+ return nil, e
243+ }
244+ return testServerMessage(f)
245+}
246+func (t *ackRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
247+ return testClientCapabilities(t)
248+}
249+func (t *ackRecordingTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
250+func (t *ackRecordingTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
251+
252+func (t *reconnectToastLinkTransport) SendClient(m protocol.ClientMessage) error {
253+ f, e := testClientFrame(m)
254+ if e != nil {
255+ return e
256+ }
257+ return t.Send(f)
258+}
259+func (t *reconnectToastLinkTransport) ReceiveServer() (protocol.ServerMessage, error) {
260+ f, e := t.Recv()
261+ if e != nil {
262+ return nil, e
263+ }
264+ return testServerMessage(f)
265+}
266+func (t *reconnectToastLinkTransport) Capabilities() protocol.ConnectionCapabilities {
267+ return testClientCapabilities(t)
268+}
269+
270+func (t *reconnectToastRecordingTransport) SendClient(m protocol.ClientMessage) error {
271+ f, e := testClientFrame(m)
272+ if e != nil {
273+ return e
274+ }
275+ return t.Send(f)
276+}
277+func (t *reconnectToastRecordingTransport) ReceiveServer() (protocol.ServerMessage, error) {
278+ f, e := t.Recv()
279+ if e != nil {
280+ return nil, e
281+ }
282+ return testServerMessage(f)
283+}
284+func (t *reconnectToastRecordingTransport) Capabilities() protocol.ConnectionCapabilities {
285+ return testClientCapabilities(t)
286+}
287+func (t *reconnectToastRecordingTransport) LinkState() ports.LinkState {
288+ return ports.LinkStateConnected
289+}
290+func (t *reconnectToastRecordingTransport) LinkEvents() <-chan ports.LinkEvent {
291+ return nil
292+}
293+
294+func (t *reconnectResetBlockingTransport) SendClient(m protocol.ClientMessage) error {
295+ f, e := testClientFrame(m)
296+ if e != nil {
297+ return e
298+ }
299+ return t.Send(f)
300+}
301+func (t *reconnectResetBlockingTransport) ReceiveServer() (protocol.ServerMessage, error) {
302+ f, e := t.Recv()
303+ if e != nil {
304+ return nil, e
305+ }
306+ return testServerMessage(f)
307+}
308+func (t *reconnectResetBlockingTransport) Capabilities() protocol.ConnectionCapabilities {
309+ return testClientCapabilities(t)
310+}
311+func (t *reconnectResetBlockingTransport) LinkState() ports.LinkState {
312+ return ports.LinkStateConnected
313+}
314+func (t *reconnectResetBlockingTransport) LinkEvents() <-chan ports.LinkEvent {
315+ return nil
316+}
317+
318+func (t *reconnectToastBlockingSendTransport) SendClient(m protocol.ClientMessage) error {
319+ f, e := testClientFrame(m)
320+ if e != nil {
321+ return e
322+ }
323+ return t.Send(f)
324+}
325+func (t *reconnectToastBlockingSendTransport) ReceiveServer() (protocol.ServerMessage, error) {
326+ f, e := t.Recv()
327+ if e != nil {
328+ return nil, e
329+ }
330+ return testServerMessage(f)
331+}
332+func (t *reconnectToastBlockingSendTransport) Capabilities() protocol.ConnectionCapabilities {
333+ return testClientCapabilities(t)
334+}
335+
336+func (t *paletteAttachTransport) SendClient(m protocol.ClientMessage) error {
337+ f, e := testClientFrame(m)
338+ if e != nil {
339+ return e
340+ }
341+ return t.Send(f)
342+}
343+func (t *paletteAttachTransport) ReceiveServer() (protocol.ServerMessage, error) {
344+ f, e := t.Recv()
345+ if e != nil {
346+ return nil, e
347+ }
348+ return testServerMessage(f)
349+}
350+func (t *paletteAttachTransport) Capabilities() protocol.ConnectionCapabilities {
351+ return testClientCapabilities(t)
352+}
353+func (t *paletteAttachTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
354+func (t *paletteAttachTransport) LinkEvents() <-chan ports.LinkEvent { return nil }
355+
356+func (t *attachPaletteTransport) SendClient(m protocol.ClientMessage) error {
357+ f, e := testClientFrame(m)
358+ if e != nil {
359+ return e
360+ }
361+ return t.Send(f)
362+}
363+func (t *attachPaletteTransport) ReceiveServer() (protocol.ServerMessage, error) {
364+ f, e := t.Recv()
365+ if e != nil {
366+ return nil, e
367+ }
368+ return testServerMessage(f)
369+}
370+func (t *attachPaletteTransport) Capabilities() protocol.ConnectionCapabilities {
371+ return testClientCapabilities(t)
372+}
373+func (t *attachPaletteTransport) LinkState() ports.LinkState { return ports.LinkStateConnected }
374+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