detached 4ab7e23

refactor(ports,ipc): move wire codecs to ports — restore usecase layering + guard

commit
4ab7e23613fded903be5907236386e1542921802
author
brice · · 2026-07-02 11:34 UTC
parent
2795b89
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
8 files · +71 -55
M boundary_test.go +5 -0
@@ -24,6 +24,11 @@ func TestImportBoundaries(t *testing.T) {
2424 root: "pkg",
2525 disallowedPrefix: "github.com/bnema/vev/internal",
2626 },
27+ {
28+ name: "usecase must not import adapters",
29+ root: "internal/usecase",
30+ disallowedPrefix: "github.com/bnema/vev/internal/adapters",
31+ },
2732 }
2833
2934 for _, rule := range rules {
@@ -24,6 +24,11 @@ func TestImportBoundaries(t *testing.T) {
24 root: "pkg",24 root: "pkg",
25 disallowedPrefix: "github.com/bnema/vev/internal",25 disallowedPrefix: "github.com/bnema/vev/internal",
26 },26 },
27+ {
28+ name: "usecase must not import adapters",
29+ root: "internal/usecase",
30+ disallowedPrefix: "github.com/bnema/vev/internal/adapters",
31+ },
27 }32 }
28 33
29 for _, rule := range rules {34 for _, rule := range rules {
M internal/adapters/ipc/transport_test.go +4 -4
@@ -21,8 +21,8 @@ func TestTransportSendRecvBothDirections(t *testing.T) {
2121 client := NewTransport(c1)
2222 server := NewTransport(c2)
2323
24- clientToServer := ports.Frame{Type: ports.MsgHello, Payload: MarshalHello(Hello{Version: 1, Intent: IntentNew, Name: "w0"})}
25- serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: MarshalWelcome(Welcome{SessionID: "s1", SessionName: "main"})}
24+ clientToServer := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{Version: 1, Intent: ports.IntentNew, Name: "w0"})}
25+ serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"})}
2626
2727 var wg sync.WaitGroup
2828 wg.Add(1)
@@ -143,8 +143,8 @@ func TestTransportManySmallFramesBackToBack(t *testing.T) {
143143 frames[i] = ports.Frame{Type: ports.MsgPing, Payload: nil}
144144 }
145145 // Vary a couple to prove ordering and content are both preserved.
146- frames[7] = ports.Frame{Type: ports.MsgInput, Payload: MarshalInput(Input{Data: []byte("hop")})}
147- frames[150] = ports.Frame{Type: ports.MsgResize, Payload: MarshalResize(Resize{Size: domain.Size{Cols: 5, Rows: 6}})}
146+ frames[7] = ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("hop")})}
147+ frames[150] = ports.Frame{Type: ports.MsgResize, Payload: ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 5, Rows: 6}})}
148148
149149 var wg sync.WaitGroup
150150 wg.Add(1)
@@ -21,8 +21,8 @@ func TestTransportSendRecvBothDirections(t *testing.T) {
21 client := NewTransport(c1)21 client := NewTransport(c1)
22 server := NewTransport(c2)22 server := NewTransport(c2)
23 23
24- clientToServer := ports.Frame{Type: ports.MsgHello, Payload: MarshalHello(Hello{Version: 1, Intent: IntentNew, Name: "w0"})}24+ clientToServer := ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(ports.Hello{Version: 1, Intent: ports.IntentNew, Name: "w0"})}
25- serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: MarshalWelcome(Welcome{SessionID: "s1", SessionName: "main"})}25+ serverToClient := ports.Frame{Type: ports.MsgWelcome, Payload: ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"})}
26 26
27 var wg sync.WaitGroup27 var wg sync.WaitGroup
28 wg.Add(1)28 wg.Add(1)
@@ -143,8 +143,8 @@ func TestTransportManySmallFramesBackToBack(t *testing.T) {
143 frames[i] = ports.Frame{Type: ports.MsgPing, Payload: nil}143 frames[i] = ports.Frame{Type: ports.MsgPing, Payload: nil}
144 }144 }
145 // Vary a couple to prove ordering and content are both preserved.145 // Vary a couple to prove ordering and content are both preserved.
146- frames[7] = ports.Frame{Type: ports.MsgInput, Payload: MarshalInput(Input{Data: []byte("hop")})}146+ frames[7] = ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("hop")})}
147- frames[150] = ports.Frame{Type: ports.MsgResize, Payload: MarshalResize(Resize{Size: domain.Size{Cols: 5, Rows: 6}})}147+ frames[150] = ports.Frame{Type: ports.MsgResize, Payload: ports.MarshalResize(ports.Resize{Size: domain.Size{Cols: 5, Rows: 6}})}
148 148
149 var wg sync.WaitGroup149 var wg sync.WaitGroup
150 wg.Add(1)150 wg.Add(1)
M internal/app/run.go +9 -9
@@ -80,7 +80,7 @@ func Run(args []string) error {
8080 // I/O.
8181 func parseArgs(args []string) (command, error) {
8282 if len(args) == 0 {
83- return command{kind: kindAttach, intent: ipc.IntentEphemeral}, nil
83+ return command{kind: kindAttach, intent: ports.IntentEphemeral}, nil
8484 }
8585
8686 switch args[0] {
@@ -90,12 +90,12 @@ func parseArgs(args []string) (command, error) {
9090 if len(args) < 2 || args[1] == "" {
9191 return command{}, usagef("`new` requires a session name")
9292 }
93- return command{kind: kindAttach, intent: ipc.IntentNew, name: args[1]}, nil
93+ return command{kind: kindAttach, intent: ports.IntentNew, name: args[1]}, nil
9494 case "attach", "a":
9595 if len(args) < 2 || args[1] == "" {
9696 return command{}, usagef("`attach` requires a session name")
9797 }
98- return command{kind: kindAttach, intent: ipc.IntentAttach, name: args[1]}, nil
98+ return command{kind: kindAttach, intent: ports.IntentAttach, name: args[1]}, nil
9999 case "ls", "list":
100100 return command{kind: kindList}, nil
101101 case "kill":
@@ -184,7 +184,7 @@ func runList(_ context.Context) error {
184184 }
185185 defer transport.Close()
186186
187- if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ipc.MarshalList(ipc.List{})}); err != nil {
187+ if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}); err != nil {
188188 return fmt.Errorf("vev: requesting session list: %w", err)
189189 }
190190 reply, err := transport.Recv()
@@ -192,13 +192,13 @@ func runList(_ context.Context) error {
192192 return fmt.Errorf("vev: reading session list: %w", err)
193193 }
194194 if reply.Type == ports.MsgError {
195- em, _ := ipc.UnmarshalErrorMsg(reply.Payload)
195+ em, _ := ports.UnmarshalErrorMsg(reply.Payload)
196196 return fmt.Errorf("vev: %s", em.Text)
197197 }
198198 if reply.Type != ports.MsgSessions {
199199 return fmt.Errorf("vev: unexpected reply type %d to list", reply.Type)
200200 }
201- sessions, err := ipc.UnmarshalSessions(reply.Payload)
201+ sessions, err := ports.UnmarshalSessions(reply.Payload)
202202 if err != nil {
203203 return fmt.Errorf("vev: decoding session list: %w", err)
204204 }
@@ -207,7 +207,7 @@ func runList(_ context.Context) error {
207207 }
208208
209209 // printSessions renders a session table (or a friendly note when empty).
210-func printSessions(w io.Writer, sessions []ipc.SessionInfo) {
210+func printSessions(w io.Writer, sessions []ports.SessionInfo) {
211211 if len(sessions) == 0 {
212212 fmt.Fprintln(w, "no sessions")
213213 return
@@ -232,7 +232,7 @@ func runKill(_ context.Context, name string) error {
232232 }
233233 defer transport.Close()
234234
235- if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ipc.MarshalKill(ipc.Kill{Name: name})}); err != nil {
235+ if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{Name: name})}); err != nil {
236236 return fmt.Errorf("vev: requesting kill: %w", err)
237237 }
238238 reply, err := transport.Recv()
@@ -246,7 +246,7 @@ func runKill(_ context.Context, name string) error {
246246 return fmt.Errorf("vev: reading kill reply: %w", err)
247247 }
248248 if reply.Type == ports.MsgError {
249- em, _ := ipc.UnmarshalErrorMsg(reply.Payload)
249+ em, _ := ports.UnmarshalErrorMsg(reply.Payload)
250250 return fmt.Errorf("vev: %s", em.Text)
251251 }
252252 fmt.Printf("killed %s\n", name)
@@ -80,7 +80,7 @@ func Run(args []string) error {
80 // I/O.80 // I/O.
81 func parseArgs(args []string) (command, error) {81 func parseArgs(args []string) (command, error) {
82 if len(args) == 0 {82 if len(args) == 0 {
83- return command{kind: kindAttach, intent: ipc.IntentEphemeral}, nil83+ return command{kind: kindAttach, intent: ports.IntentEphemeral}, nil
84 }84 }
85 85
86 switch args[0] {86 switch args[0] {
@@ -90,12 +90,12 @@ func parseArgs(args []string) (command, error) {
90 if len(args) < 2 || args[1] == "" {90 if len(args) < 2 || args[1] == "" {
91 return command{}, usagef("`new` requires a session name")91 return command{}, usagef("`new` requires a session name")
92 }92 }
93- return command{kind: kindAttach, intent: ipc.IntentNew, name: args[1]}, nil93+ return command{kind: kindAttach, intent: ports.IntentNew, name: args[1]}, nil
94 case "attach", "a":94 case "attach", "a":
95 if len(args) < 2 || args[1] == "" {95 if len(args) < 2 || args[1] == "" {
96 return command{}, usagef("`attach` requires a session name")96 return command{}, usagef("`attach` requires a session name")
97 }97 }
98- return command{kind: kindAttach, intent: ipc.IntentAttach, name: args[1]}, nil98+ return command{kind: kindAttach, intent: ports.IntentAttach, name: args[1]}, nil
99 case "ls", "list":99 case "ls", "list":
100 return command{kind: kindList}, nil100 return command{kind: kindList}, nil
101 case "kill":101 case "kill":
@@ -184,7 +184,7 @@ func runList(_ context.Context) error {
184 }184 }
185 defer transport.Close()185 defer transport.Close()
186 186
187- if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ipc.MarshalList(ipc.List{})}); err != nil {187+ if err := transport.Send(ports.Frame{Type: ports.MsgList, Payload: ports.MarshalList(ports.List{})}); err != nil {
188 return fmt.Errorf("vev: requesting session list: %w", err)188 return fmt.Errorf("vev: requesting session list: %w", err)
189 }189 }
190 reply, err := transport.Recv()190 reply, err := transport.Recv()
@@ -192,13 +192,13 @@ func runList(_ context.Context) error {
192 return fmt.Errorf("vev: reading session list: %w", err)192 return fmt.Errorf("vev: reading session list: %w", err)
193 }193 }
194 if reply.Type == ports.MsgError {194 if reply.Type == ports.MsgError {
195- em, _ := ipc.UnmarshalErrorMsg(reply.Payload)195+ em, _ := ports.UnmarshalErrorMsg(reply.Payload)
196 return fmt.Errorf("vev: %s", em.Text)196 return fmt.Errorf("vev: %s", em.Text)
197 }197 }
198 if reply.Type != ports.MsgSessions {198 if reply.Type != ports.MsgSessions {
199 return fmt.Errorf("vev: unexpected reply type %d to list", reply.Type)199 return fmt.Errorf("vev: unexpected reply type %d to list", reply.Type)
200 }200 }
201- sessions, err := ipc.UnmarshalSessions(reply.Payload)201+ sessions, err := ports.UnmarshalSessions(reply.Payload)
202 if err != nil {202 if err != nil {
203 return fmt.Errorf("vev: decoding session list: %w", err)203 return fmt.Errorf("vev: decoding session list: %w", err)
204 }204 }
@@ -207,7 +207,7 @@ func runList(_ context.Context) error {
207 }207 }
208 208
209 // printSessions renders a session table (or a friendly note when empty).209 // printSessions renders a session table (or a friendly note when empty).
210-func printSessions(w io.Writer, sessions []ipc.SessionInfo) {210+func printSessions(w io.Writer, sessions []ports.SessionInfo) {
211 if len(sessions) == 0 {211 if len(sessions) == 0 {
212 fmt.Fprintln(w, "no sessions")212 fmt.Fprintln(w, "no sessions")
213 return213 return
@@ -232,7 +232,7 @@ func runKill(_ context.Context, name string) error {
232 }232 }
233 defer transport.Close()233 defer transport.Close()
234 234
235- if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ipc.MarshalKill(ipc.Kill{Name: name})}); err != nil {235+ if err := transport.Send(ports.Frame{Type: ports.MsgKill, Payload: ports.MarshalKill(ports.Kill{Name: name})}); err != nil {
236 return fmt.Errorf("vev: requesting kill: %w", err)236 return fmt.Errorf("vev: requesting kill: %w", err)
237 }237 }
238 reply, err := transport.Recv()238 reply, err := transport.Recv()
@@ -246,7 +246,7 @@ func runKill(_ context.Context, name string) error {
246 return fmt.Errorf("vev: reading kill reply: %w", err)246 return fmt.Errorf("vev: reading kill reply: %w", err)
247 }247 }
248 if reply.Type == ports.MsgError {248 if reply.Type == ports.MsgError {
249- em, _ := ipc.UnmarshalErrorMsg(reply.Payload)249+ em, _ := ports.UnmarshalErrorMsg(reply.Payload)
250 return fmt.Errorf("vev: %s", em.Text)250 return fmt.Errorf("vev: %s", em.Text)
251 }251 }
252 fmt.Printf("killed %s\n", name)252 fmt.Printf("killed %s\n", name)
M internal/app/run_test.go +6 -6
@@ -4,7 +4,7 @@ import (
44 "errors"
55 "testing"
66
7- "github.com/bnema/vev/internal/adapters/ipc"
7+ "github.com/bnema/vev/internal/ports"
88 )
99
1010 func TestParseArgs(t *testing.T) {
@@ -16,13 +16,13 @@ func TestParseArgs(t *testing.T) {
1616 wantName string
1717 wantErr bool
1818 }{
19- {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: ipc.IntentEphemeral},
20- {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: ipc.IntentEphemeral},
21- {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: ipc.IntentNew, wantName: "work"},
19+ {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},
20+ {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},
21+ {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: ports.IntentNew, wantName: "work"},
2222 {name: "new without name", args: []string{"new"}, wantErr: true},
2323 {name: "new empty name", args: []string{"new", ""}, wantErr: true},
24- {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: ipc.IntentAttach, wantName: "work"},
25- {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: ipc.IntentAttach, wantName: "work"},
24+ {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},
25+ {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},
2626 {name: "attach without name", args: []string{"attach"}, wantErr: true},
2727 {name: "ls", args: []string{"ls"}, wantKind: kindList},
2828 {name: "list", args: []string{"list"}, wantKind: kindList},
@@ -4,7 +4,7 @@ import (
4 "errors"4 "errors"
5 "testing"5 "testing"
6 6
7- "github.com/bnema/vev/internal/adapters/ipc"7+ "github.com/bnema/vev/internal/ports"
8 )8 )
9 9
10 func TestParseArgs(t *testing.T) {10 func TestParseArgs(t *testing.T) {
@@ -16,13 +16,13 @@ func TestParseArgs(t *testing.T) {
16 wantName string16 wantName string
17 wantErr bool17 wantErr bool
18 }{18 }{
19- {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: ipc.IntentEphemeral},19+ {name: "no args -> ephemeral attach", args: nil, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},
20- {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: ipc.IntentEphemeral},20+ {name: "empty slice -> ephemeral attach", args: []string{}, wantKind: kindAttach, wantIntent: ports.IntentEphemeral},
21- {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: ipc.IntentNew, wantName: "work"},21+ {name: "new named", args: []string{"new", "work"}, wantKind: kindAttach, wantIntent: ports.IntentNew, wantName: "work"},
22 {name: "new without name", args: []string{"new"}, wantErr: true},22 {name: "new without name", args: []string{"new"}, wantErr: true},
23 {name: "new empty name", args: []string{"new", ""}, wantErr: true},23 {name: "new empty name", args: []string{"new", ""}, wantErr: true},
24- {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: ipc.IntentAttach, wantName: "work"},24+ {name: "attach named", args: []string{"attach", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},
25- {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: ipc.IntentAttach, wantName: "work"},25+ {name: "attach alias a", args: []string{"a", "work"}, wantKind: kindAttach, wantIntent: ports.IntentAttach, wantName: "work"},
26 {name: "attach without name", args: []string{"attach"}, wantErr: true},26 {name: "attach without name", args: []string{"attach"}, wantErr: true},
27 {name: "ls", args: []string{"ls"}, wantKind: kindList},27 {name: "ls", args: []string{"ls"}, wantKind: kindList},
28 {name: "list", args: []string{"list"}, wantKind: kindList},28 {name: "list", args: []string{"list"}, wantKind: kindList},
R internal/adapters/ipc/protocol.go → internal/ports/wire.go +9 -3 renamed, 95% similar
@@ -1,4 +1,10 @@
1-package ipc
1+// Wire codecs for vev's IPC message payloads. They live in ports (alongside
2+// Frame and the MsgType constants) because wire encoding is protocol
3+// surface, not I/O: everything here is pure, stdlib-only byte marshalling.
4+// Adapters keep what actually performs I/O (framing over a connection,
5+// listening on a socket) and import these types for the payloads they carry.
6+
7+package ports
28
39 import (
410 "encoding/binary"
@@ -9,12 +15,12 @@ import (
915
1016 // errShortPayload is returned when a payload ends before a required field
1117 // has been fully read.
12-var errShortPayload = errors.New("ipc: payload too short")
18+var errShortPayload = errors.New("ports: payload too short")
1319
1420 // errTrailingBytes is returned when a fixed-shape payload has bytes left
1521 // over after every field has been consumed. Protocol strictness here
1622 // catches version drift early instead of silently ignoring extra data.
17-var errTrailingBytes = errors.New("ipc: unexpected trailing bytes in payload")
23+var errTrailingBytes = errors.New("ports: unexpected trailing bytes in payload")
1824
1925 // Intent values carried by Hello, describing what the client wants to do.
2026 const (
@@ -1,4 +1,10 @@
1-package ipc1+// Wire codecs for vev's IPC message payloads. They live in ports (alongside
2+// Frame and the MsgType constants) because wire encoding is protocol
3+// surface, not I/O: everything here is pure, stdlib-only byte marshalling.
4+// Adapters keep what actually performs I/O (framing over a connection,
5+// listening on a socket) and import these types for the payloads they carry.
6+
7+package ports
2 8
3 import (9 import (
4 "encoding/binary"10 "encoding/binary"
@@ -9,12 +15,12 @@ import (
9 15
10 // errShortPayload is returned when a payload ends before a required field16 // errShortPayload is returned when a payload ends before a required field
11 // has been fully read.17 // has been fully read.
12-var errShortPayload = errors.New("ipc: payload too short")18+var errShortPayload = errors.New("ports: payload too short")
13 19
14 // errTrailingBytes is returned when a fixed-shape payload has bytes left20 // errTrailingBytes is returned when a fixed-shape payload has bytes left
15 // over after every field has been consumed. Protocol strictness here21 // over after every field has been consumed. Protocol strictness here
16 // catches version drift early instead of silently ignoring extra data.22 // catches version drift early instead of silently ignoring extra data.
17-var errTrailingBytes = errors.New("ipc: unexpected trailing bytes in payload")23+var errTrailingBytes = errors.New("ports: unexpected trailing bytes in payload")
18 24
19 // Intent values carried by Hello, describing what the client wants to do.25 // Intent values carried by Hello, describing what the client wants to do.
20 const (26 const (
R internal/adapters/ipc/protocol_test.go → internal/ports/wire_test.go +1 -1 renamed, 99% similar
@@ -1,4 +1,4 @@
1-package ipc
1+package ports
22
33 import (
44 "bytes"
@@ -1,4 +1,4 @@
1-package ipc1+package ports
2 2
3 import (3 import (
4 "bytes"4 "bytes"
M internal/usecase/client/client.go +21 -15
@@ -2,9 +2,9 @@
22 // IPC protocol to a daemon, pumping terminal input, resize events, and PTY
33 // output between the controlling terminal and the session.
44 //
5-// The package depends only on ports (transport, terminal) plus the ipc
6-// message codecs, never on a concrete transport or terminal implementation;
7-// the app layer wires those in.
5+// The package depends only on ports (transport, terminal, wire codecs) and
6+// domain, never on a concrete transport or terminal implementation; the app
7+// layer wires those in.
88 package client
99
1010 import (
@@ -15,7 +15,6 @@ import (
1515 "log/slog"
1616 "os"
1717
18- "github.com/bnema/vev/internal/adapters/ipc"
1918 "github.com/bnema/vev/internal/domain"
2019 "github.com/bnema/vev/internal/ports"
2120 )
@@ -112,14 +111,14 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
112111 if err != nil {
113112 return fmt.Errorf("vev: reading terminal size: %w", err)
114113 }
115- hello := ipc.Hello{
114+ hello := ports.Hello{
116115 Version: ports.ProtocolVersion,
117116 Intent: intent,
118117 Name: name,
119118 Size: size,
120119 TermEnv: os.Getenv("TERM"),
121120 }
122- if err := transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ipc.MarshalHello(hello)}); err != nil {
121+ if err := transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil {
123122 return fmt.Errorf("vev: sending hello: %w", err)
124123 }
125124 ms.helloSent = true
@@ -133,7 +132,7 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
133132 case ports.MsgWelcome:
134133 ms.welcomed = true
135134 case ports.MsgError:
136- em, derr := ipc.UnmarshalErrorMsg(reply.Payload)
135+ em, derr := ports.UnmarshalErrorMsg(reply.Payload)
137136 if derr != nil {
138137 return fmt.Errorf("vev: decoding error reply: %w", derr)
139138 }
@@ -191,7 +190,7 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
191190 }
192191 switch r.frame.Type {
193192 case ports.MsgOutput:
194- o, derr := ipc.UnmarshalOutput(r.frame.Payload)
193+ o, derr := ports.UnmarshalOutput(r.frame.Payload)
195194 if derr != nil {
196195 return fmt.Errorf("vev: decoding output: %w", derr)
197196 }
@@ -203,7 +202,7 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
203202 }
204203 ms.firstOutput = true
205204 case ports.MsgDetached:
206- d, derr := ipc.UnmarshalDetached(r.frame.Payload)
205+ d, derr := ports.UnmarshalDetached(r.frame.Payload)
207206 if derr != nil {
208207 return fmt.Errorf("vev: decoding detached: %w", derr)
209208 }
@@ -266,6 +265,13 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
266265 // runStdin pumps terminal input to the daemon as Input frames. A read error
267266 // or EOF is treated as a detach: it best-effort sends a Detach frame and
268267 // cancels the loop.
268+//
269+// Known MVP limitation: a bare io.Reader cannot be unblocked from outside,
270+// so on shutdown initiated elsewhere (daemon detach, transport loss) this
271+// goroutine stays parked in Read until the next byte arrives or the process
272+// exits. That is harmless here — Attach has already returned and restored
273+// the terminal — and matches the standard pattern for stdin pumps; a
274+// closable stdin duplicate could lift it later if ever needed.
269275 func runStdin(ctx context.Context, cancel context.CancelFunc, in io.Reader, out chan<- ports.Frame) {
270276 buf := make([]byte, stdinBufSize)
271277 for {
@@ -273,7 +279,7 @@ func runStdin(ctx context.Context, cancel context.CancelFunc, in io.Reader, out
273279 if n > 0 {
274280 data := make([]byte, n)
275281 copy(data, buf[:n])
276- frame := ports.Frame{Type: ports.MsgInput, Payload: ipc.MarshalInput(ipc.Input{Data: data})}
282+ frame := ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: data})}
277283 select {
278284 case out <- frame:
279285 case <-ctx.Done():
@@ -283,7 +289,7 @@ func runStdin(ctx context.Context, cancel context.CancelFunc, in io.Reader, out
283289 if rerr != nil {
284290 // Best-effort detach notification, then unwind.
285291 select {
286- case out <- ports.Frame{Type: ports.MsgDetach, Payload: ipc.MarshalDetach(ipc.Detach{})}:
292+ case out <- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}:
287293 case <-ctx.Done():
288294 }
289295 cancel()
@@ -302,7 +308,7 @@ func runResize(ctx context.Context, events <-chan domain.Size, out chan<- ports.
302308 if !ok {
303309 return
304310 }
305- frame := ports.Frame{Type: ports.MsgResize, Payload: ipc.MarshalResize(ipc.Resize{Size: sz})}
311+ frame := ports.Frame{Type: ports.MsgResize, Payload: ports.MarshalResize(ports.Resize{Size: sz})}
306312 select {
307313 case out <- frame:
308314 case <-ctx.Done():
@@ -319,11 +325,11 @@ func runResize(ctx context.Context, events <-chan domain.Size, out chan<- ports.
319325 // the user learns why the session went away.
320326 func detachedResult(reason uint8) error {
321327 switch reason {
322- case ipc.ReasonDetach:
328+ case ports.ReasonDetach:
323329 return nil
324- case ipc.ReasonSessionKilled:
330+ case ports.ReasonSessionKilled:
325331 return &DetachedError{Reason: reason, Text: "session was killed"}
326- case ipc.ReasonServerShutdown:
332+ case ports.ReasonServerShutdown:
327333 return &DetachedError{Reason: reason, Text: "daemon shut down"}
328334 default:
329335 return &DetachedError{Reason: reason, Text: "detached by daemon"}
@@ -2,9 +2,9 @@
2 // IPC protocol to a daemon, pumping terminal input, resize events, and PTY2 // IPC protocol to a daemon, pumping terminal input, resize events, and PTY
3 // output between the controlling terminal and the session.3 // output between the controlling terminal and the session.
4 //4 //
5-// The package depends only on ports (transport, terminal) plus the ipc5+// The package depends only on ports (transport, terminal, wire codecs) and
6-// message codecs, never on a concrete transport or terminal implementation;6+// domain, never on a concrete transport or terminal implementation; the app
7-// the app layer wires those in.7+// layer wires those in.
8 package client8 package client
9 9
10 import (10 import (
@@ -15,7 +15,6 @@ import (
15 "log/slog"15 "log/slog"
16 "os"16 "os"
17 17
18- "github.com/bnema/vev/internal/adapters/ipc"
19 "github.com/bnema/vev/internal/domain"18 "github.com/bnema/vev/internal/domain"
20 "github.com/bnema/vev/internal/ports"19 "github.com/bnema/vev/internal/ports"
21 )20 )
@@ -112,14 +111,14 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
112 if err != nil {111 if err != nil {
113 return fmt.Errorf("vev: reading terminal size: %w", err)112 return fmt.Errorf("vev: reading terminal size: %w", err)
114 }113 }
115- hello := ipc.Hello{114+ hello := ports.Hello{
116 Version: ports.ProtocolVersion,115 Version: ports.ProtocolVersion,
117 Intent: intent,116 Intent: intent,
118 Name: name,117 Name: name,
119 Size: size,118 Size: size,
120 TermEnv: os.Getenv("TERM"),119 TermEnv: os.Getenv("TERM"),
121 }120 }
122- if err := transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ipc.MarshalHello(hello)}); err != nil {121+ if err := transport.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}); err != nil {
123 return fmt.Errorf("vev: sending hello: %w", err)122 return fmt.Errorf("vev: sending hello: %w", err)
124 }123 }
125 ms.helloSent = true124 ms.helloSent = true
@@ -133,7 +132,7 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
133 case ports.MsgWelcome:132 case ports.MsgWelcome:
134 ms.welcomed = true133 ms.welcomed = true
135 case ports.MsgError:134 case ports.MsgError:
136- em, derr := ipc.UnmarshalErrorMsg(reply.Payload)135+ em, derr := ports.UnmarshalErrorMsg(reply.Payload)
137 if derr != nil {136 if derr != nil {
138 return fmt.Errorf("vev: decoding error reply: %w", derr)137 return fmt.Errorf("vev: decoding error reply: %w", derr)
139 }138 }
@@ -191,7 +190,7 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
191 }190 }
192 switch r.frame.Type {191 switch r.frame.Type {
193 case ports.MsgOutput:192 case ports.MsgOutput:
194- o, derr := ipc.UnmarshalOutput(r.frame.Payload)193+ o, derr := ports.UnmarshalOutput(r.frame.Payload)
195 if derr != nil {194 if derr != nil {
196 return fmt.Errorf("vev: decoding output: %w", derr)195 return fmt.Errorf("vev: decoding output: %w", derr)
197 }196 }
@@ -203,7 +202,7 @@ func Attach(ctx context.Context, transport ports.Transport, term ports.Terminal,
203 }202 }
204 ms.firstOutput = true203 ms.firstOutput = true
205 case ports.MsgDetached:204 case ports.MsgDetached:
206- d, derr := ipc.UnmarshalDetached(r.frame.Payload)205+ d, derr := ports.UnmarshalDetached(r.frame.Payload)
207 if derr != nil {206 if derr != nil {
208 return fmt.Errorf("vev: decoding detached: %w", derr)207 return fmt.Errorf("vev: decoding detached: %w", derr)
209 }208 }
@@ -266,6 +265,13 @@ func runSender(ctx context.Context, cancel context.CancelFunc, transport ports.T
266 // runStdin pumps terminal input to the daemon as Input frames. A read error265 // runStdin pumps terminal input to the daemon as Input frames. A read error
267 // or EOF is treated as a detach: it best-effort sends a Detach frame and266 // or EOF is treated as a detach: it best-effort sends a Detach frame and
268 // cancels the loop.267 // cancels the loop.
268+//
269+// Known MVP limitation: a bare io.Reader cannot be unblocked from outside,
270+// so on shutdown initiated elsewhere (daemon detach, transport loss) this
271+// goroutine stays parked in Read until the next byte arrives or the process
272+// exits. That is harmless here — Attach has already returned and restored
273+// the terminal — and matches the standard pattern for stdin pumps; a
274+// closable stdin duplicate could lift it later if ever needed.
269 func runStdin(ctx context.Context, cancel context.CancelFunc, in io.Reader, out chan<- ports.Frame) {275 func runStdin(ctx context.Context, cancel context.CancelFunc, in io.Reader, out chan<- ports.Frame) {
270 buf := make([]byte, stdinBufSize)276 buf := make([]byte, stdinBufSize)
271 for {277 for {
@@ -273,7 +279,7 @@ func runStdin(ctx context.Context, cancel context.CancelFunc, in io.Reader, out
273 if n > 0 {279 if n > 0 {
274 data := make([]byte, n)280 data := make([]byte, n)
275 copy(data, buf[:n])281 copy(data, buf[:n])
276- frame := ports.Frame{Type: ports.MsgInput, Payload: ipc.MarshalInput(ipc.Input{Data: data})}282+ frame := ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: data})}
277 select {283 select {
278 case out <- frame:284 case out <- frame:
279 case <-ctx.Done():285 case <-ctx.Done():
@@ -283,7 +289,7 @@ func runStdin(ctx context.Context, cancel context.CancelFunc, in io.Reader, out
283 if rerr != nil {289 if rerr != nil {
284 // Best-effort detach notification, then unwind.290 // Best-effort detach notification, then unwind.
285 select {291 select {
286- case out <- ports.Frame{Type: ports.MsgDetach, Payload: ipc.MarshalDetach(ipc.Detach{})}:292+ case out <- ports.Frame{Type: ports.MsgDetach, Payload: ports.MarshalDetach(ports.Detach{})}:
287 case <-ctx.Done():293 case <-ctx.Done():
288 }294 }
289 cancel()295 cancel()
@@ -302,7 +308,7 @@ func runResize(ctx context.Context, events <-chan domain.Size, out chan<- ports.
302 if !ok {308 if !ok {
303 return309 return
304 }310 }
305- frame := ports.Frame{Type: ports.MsgResize, Payload: ipc.MarshalResize(ipc.Resize{Size: sz})}311+ frame := ports.Frame{Type: ports.MsgResize, Payload: ports.MarshalResize(ports.Resize{Size: sz})}
306 select {312 select {
307 case out <- frame:313 case out <- frame:
308 case <-ctx.Done():314 case <-ctx.Done():
@@ -319,11 +325,11 @@ func runResize(ctx context.Context, events <-chan domain.Size, out chan<- ports.
319 // the user learns why the session went away.325 // the user learns why the session went away.
320 func detachedResult(reason uint8) error {326 func detachedResult(reason uint8) error {
321 switch reason {327 switch reason {
322- case ipc.ReasonDetach:328+ case ports.ReasonDetach:
323 return nil329 return nil
324- case ipc.ReasonSessionKilled:330+ case ports.ReasonSessionKilled:
325 return &DetachedError{Reason: reason, Text: "session was killed"}331 return &DetachedError{Reason: reason, Text: "session was killed"}
326- case ipc.ReasonServerShutdown:332+ case ports.ReasonServerShutdown:
327 return &DetachedError{Reason: reason, Text: "daemon shut down"}333 return &DetachedError{Reason: reason, Text: "daemon shut down"}
328 default:334 default:
329 return &DetachedError{Reason: reason, Text: "detached by daemon"}335 return &DetachedError{Reason: reason, Text: "detached by daemon"}
M internal/usecase/client/client_test.go +16 -17
@@ -13,7 +13,6 @@ import (
1313 "github.com/stretchr/testify/mock"
1414 "github.com/stretchr/testify/require"
1515
16- "github.com/bnema/vev/internal/adapters/ipc"
1716 "github.com/bnema/vev/internal/domain"
1817 "github.com/bnema/vev/internal/ports"
1918 portsmocks "github.com/bnema/vev/internal/ports/mocks"
@@ -96,14 +95,14 @@ func TestAttachHappyPath(t *testing.T) {
9695 tr := portsmocks.NewMockTransport(t)
9796 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
9897 unblock := scriptRecv(tr,
99- recvItem{f: frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1", SessionName: "main"}))},
100- recvItem{f: frameOf(ports.MsgOutput, ipc.MarshalOutput(ipc.Output{Data: []byte("hello world")}))},
101- recvItem{f: frameOf(ports.MsgDetached, ipc.MarshalDetached(ipc.Detached{Reason: ipc.ReasonDetach}))},
98+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"}))},
99+ recvItem{f: frameOf(ports.MsgOutput, ports.MarshalOutput(ports.Output{Data: []byte("hello world")}))},
100+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
102101 )
103102 defer unblock()
104103 tr.EXPECT().Close().Return(nil).Once()
105104
106- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")
105+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
107106 require.NoError(t, err)
108107 require.Equal(t, "hello world", out.String())
109108 require.Equal(t, int32(1), restoreCount.Load(), "restore must run exactly once")
@@ -117,16 +116,16 @@ func TestAttachVersionMismatch(t *testing.T) {
117116 tr := portsmocks.NewMockTransport(t)
118117 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
119118 tr.EXPECT().Recv().Return(
120- frameOf(ports.MsgError, ipc.MarshalErrorMsg(ipc.ErrorMsg{Code: ipc.ErrVersionMismatch, Text: "version mismatch"})),
119+ frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"})),
121120 nil,
122121 ).Once()
123122 tr.EXPECT().Close().Return(nil).Once()
124123
125- err := client.Attach(context.Background(), tr, tm, ipc.IntentAttach, "main")
124+ err := client.Attach(context.Background(), tr, tm, ports.IntentAttach, "main")
126125 require.Error(t, err)
127126 var pe *client.ProtocolError
128127 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)
129- require.Equal(t, ipc.ErrVersionMismatch, pe.Code)
128+ require.Equal(t, ports.ErrVersionMismatch, pe.Code)
130129 }
131130
132131 func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
@@ -140,13 +139,13 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
140139 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
141140 boom := errors.New("connection reset")
142141 unblock := scriptRecv(tr,
143- recvItem{f: frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1"}))},
142+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
144143 recvItem{err: boom},
145144 )
146145 defer unblock()
147146 tr.EXPECT().Close().Return(nil).Once()
148147
149- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")
148+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
150149 require.Error(t, err)
151150 require.Equal(t, int32(1), restoreCount.Load(), "restore must run after mid-stream Recv error")
152151 }
@@ -161,13 +160,13 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) {
161160 tr := portsmocks.NewMockTransport(t)
162161 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
163162 unblock := scriptRecv(tr,
164- recvItem{f: frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1"}))},
163+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
165164 recvItem{err: io.EOF},
166165 )
167166 defer unblock()
168167 tr.EXPECT().Close().Return(nil).Once()
169168
170- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")
169+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
171170 require.Error(t, err)
172171 require.ErrorIs(t, err, io.EOF)
173172 require.Equal(t, int32(1), restoreCount.Load())
@@ -185,16 +184,16 @@ func TestAttachForwardsResize(t *testing.T) {
185184 tr := portsmocks.NewMockTransport(t)
186185 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
187186 // The resize frame is forwarded via the sender goroutine.
188- gotResize := make(chan ipc.Resize, 1)
187+ gotResize := make(chan ports.Resize, 1)
189188 tr.EXPECT().Send(isType(ports.MsgResize)).RunAndReturn(func(f ports.Frame) error {
190- r, _ := ipc.UnmarshalResize(f.Payload)
189+ r, _ := ports.UnmarshalResize(f.Payload)
191190 gotResize <- r
192191 close(detachAfterResize)
193192 return nil
194193 }).Once()
195194
196- welcome := frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1"}))
197- detached := frameOf(ports.MsgDetached, ipc.MarshalDetached(ipc.Detached{Reason: ipc.ReasonDetach}))
195+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))
196+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
198197 recvCh := make(chan recvItem, 2)
199198 recvCh <- recvItem{f: welcome}
200199 done := make(chan struct{})
@@ -223,7 +222,7 @@ func TestAttachForwardsResize(t *testing.T) {
223222 resizeCh <- domain.Size{Cols: 120, Rows: 40}
224223 }()
225224
226- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")
225+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
227226 require.NoError(t, err)
228227 select {
229228 case r := <-gotResize:
@@ -13,7 +13,6 @@ import (
13 "github.com/stretchr/testify/mock"13 "github.com/stretchr/testify/mock"
14 "github.com/stretchr/testify/require"14 "github.com/stretchr/testify/require"
15 15
16- "github.com/bnema/vev/internal/adapters/ipc"
17 "github.com/bnema/vev/internal/domain"16 "github.com/bnema/vev/internal/domain"
18 "github.com/bnema/vev/internal/ports"17 "github.com/bnema/vev/internal/ports"
19 portsmocks "github.com/bnema/vev/internal/ports/mocks"18 portsmocks "github.com/bnema/vev/internal/ports/mocks"
@@ -96,14 +95,14 @@ func TestAttachHappyPath(t *testing.T) {
96 tr := portsmocks.NewMockTransport(t)95 tr := portsmocks.NewMockTransport(t)
97 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()96 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
98 unblock := scriptRecv(tr,97 unblock := scriptRecv(tr,
99- recvItem{f: frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1", SessionName: "main"}))},98+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1", SessionName: "main"}))},
100- recvItem{f: frameOf(ports.MsgOutput, ipc.MarshalOutput(ipc.Output{Data: []byte("hello world")}))},99+ recvItem{f: frameOf(ports.MsgOutput, ports.MarshalOutput(ports.Output{Data: []byte("hello world")}))},
101- recvItem{f: frameOf(ports.MsgDetached, ipc.MarshalDetached(ipc.Detached{Reason: ipc.ReasonDetach}))},100+ recvItem{f: frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))},
102 )101 )
103 defer unblock()102 defer unblock()
104 tr.EXPECT().Close().Return(nil).Once()103 tr.EXPECT().Close().Return(nil).Once()
105 104
106- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")105+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
107 require.NoError(t, err)106 require.NoError(t, err)
108 require.Equal(t, "hello world", out.String())107 require.Equal(t, "hello world", out.String())
109 require.Equal(t, int32(1), restoreCount.Load(), "restore must run exactly once")108 require.Equal(t, int32(1), restoreCount.Load(), "restore must run exactly once")
@@ -117,16 +116,16 @@ func TestAttachVersionMismatch(t *testing.T) {
117 tr := portsmocks.NewMockTransport(t)116 tr := portsmocks.NewMockTransport(t)
118 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()117 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
119 tr.EXPECT().Recv().Return(118 tr.EXPECT().Recv().Return(
120- frameOf(ports.MsgError, ipc.MarshalErrorMsg(ipc.ErrorMsg{Code: ipc.ErrVersionMismatch, Text: "version mismatch"})),119+ frameOf(ports.MsgError, ports.MarshalErrorMsg(ports.ErrorMsg{Code: ports.ErrVersionMismatch, Text: "version mismatch"})),
121 nil,120 nil,
122 ).Once()121 ).Once()
123 tr.EXPECT().Close().Return(nil).Once()122 tr.EXPECT().Close().Return(nil).Once()
124 123
125- err := client.Attach(context.Background(), tr, tm, ipc.IntentAttach, "main")124+ err := client.Attach(context.Background(), tr, tm, ports.IntentAttach, "main")
126 require.Error(t, err)125 require.Error(t, err)
127 var pe *client.ProtocolError126 var pe *client.ProtocolError
128 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)127 require.True(t, errors.As(err, &pe), "want *client.ProtocolError, got %T", err)
129- require.Equal(t, ipc.ErrVersionMismatch, pe.Code)128+ require.Equal(t, ports.ErrVersionMismatch, pe.Code)
130 }129 }
131 130
132 func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {131 func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
@@ -140,13 +139,13 @@ func TestAttachRestoredOnRecvErrorMidStream(t *testing.T) {
140 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()139 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
141 boom := errors.New("connection reset")140 boom := errors.New("connection reset")
142 unblock := scriptRecv(tr,141 unblock := scriptRecv(tr,
143- recvItem{f: frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1"}))},142+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
144 recvItem{err: boom},143 recvItem{err: boom},
145 )144 )
146 defer unblock()145 defer unblock()
147 tr.EXPECT().Close().Return(nil).Once()146 tr.EXPECT().Close().Return(nil).Once()
148 147
149- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")148+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
150 require.Error(t, err)149 require.Error(t, err)
151 require.Equal(t, int32(1), restoreCount.Load(), "restore must run after mid-stream Recv error")150 require.Equal(t, int32(1), restoreCount.Load(), "restore must run after mid-stream Recv error")
152 }151 }
@@ -161,13 +160,13 @@ func TestAttachDaemonVanishedOnEOF(t *testing.T) {
161 tr := portsmocks.NewMockTransport(t)160 tr := portsmocks.NewMockTransport(t)
162 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()161 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
163 unblock := scriptRecv(tr,162 unblock := scriptRecv(tr,
164- recvItem{f: frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1"}))},163+ recvItem{f: frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))},
165 recvItem{err: io.EOF},164 recvItem{err: io.EOF},
166 )165 )
167 defer unblock()166 defer unblock()
168 tr.EXPECT().Close().Return(nil).Once()167 tr.EXPECT().Close().Return(nil).Once()
169 168
170- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")169+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
171 require.Error(t, err)170 require.Error(t, err)
172 require.ErrorIs(t, err, io.EOF)171 require.ErrorIs(t, err, io.EOF)
173 require.Equal(t, int32(1), restoreCount.Load())172 require.Equal(t, int32(1), restoreCount.Load())
@@ -185,16 +184,16 @@ func TestAttachForwardsResize(t *testing.T) {
185 tr := portsmocks.NewMockTransport(t)184 tr := portsmocks.NewMockTransport(t)
186 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()185 tr.EXPECT().Send(isType(ports.MsgHello)).Return(nil).Once()
187 // The resize frame is forwarded via the sender goroutine.186 // The resize frame is forwarded via the sender goroutine.
188- gotResize := make(chan ipc.Resize, 1)187+ gotResize := make(chan ports.Resize, 1)
189 tr.EXPECT().Send(isType(ports.MsgResize)).RunAndReturn(func(f ports.Frame) error {188 tr.EXPECT().Send(isType(ports.MsgResize)).RunAndReturn(func(f ports.Frame) error {
190- r, _ := ipc.UnmarshalResize(f.Payload)189+ r, _ := ports.UnmarshalResize(f.Payload)
191 gotResize <- r190 gotResize <- r
192 close(detachAfterResize)191 close(detachAfterResize)
193 return nil192 return nil
194 }).Once()193 }).Once()
195 194
196- welcome := frameOf(ports.MsgWelcome, ipc.MarshalWelcome(ipc.Welcome{SessionID: "s1"}))195+ welcome := frameOf(ports.MsgWelcome, ports.MarshalWelcome(ports.Welcome{SessionID: "s1"}))
197- detached := frameOf(ports.MsgDetached, ipc.MarshalDetached(ipc.Detached{Reason: ipc.ReasonDetach}))196+ detached := frameOf(ports.MsgDetached, ports.MarshalDetached(ports.Detached{Reason: ports.ReasonDetach}))
198 recvCh := make(chan recvItem, 2)197 recvCh := make(chan recvItem, 2)
199 recvCh <- recvItem{f: welcome}198 recvCh <- recvItem{f: welcome}
200 done := make(chan struct{})199 done := make(chan struct{})
@@ -223,7 +222,7 @@ func TestAttachForwardsResize(t *testing.T) {
223 resizeCh <- domain.Size{Cols: 120, Rows: 40}222 resizeCh <- domain.Size{Cols: 120, Rows: 40}
224 }()223 }()
225 224
226- err := client.Attach(context.Background(), tr, tm, ipc.IntentEphemeral, "")225+ err := client.Attach(context.Background(), tr, tm, ports.IntentEphemeral, "")
227 require.NoError(t, err)226 require.NoError(t, err)
228 select {227 select {
229 case r := <-gotResize:228 case r := <-gotResize:

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