detached 811c2e4

test(app): cover remote environment proxy paths

commit
811c2e4d92a3ac583e5a4326c7fde3caa9bb9164
author
brice · · 2026-07-15 06:05 UTC
parent
0812bb2
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +122 -3
M internal/app/run.go +20 -3
@@ -794,7 +794,7 @@ func runStdio(ctx context.Context) (retErr error) {
794794 }
795795 defer func() { _ = transport.Close() }()
796796 stdio := sshstdio.NewTransport(os.Stdin, os.Stdout, nil, sshstdio.WithRuntimeObserver(observer))
797- return proxyTransports(ctx, newFirstHelloTransport(stdio, "", os.Environ()), transport, log)
797+ return runStdioProxy(ctx, stdio, transport, os.Environ(), log)
798798 }
799799
800800 var (
@@ -935,8 +935,7 @@ func runUDPProxy(ctx context.Context, session string, ready io.Writer) (retErr e
935935 return err
936936 }
937937
938- dgramTr := newFirstHelloTransport(dg, session, os.Environ())
939- return dgram.ProxyRuntime{Client: dgramTr, Daemon: daemonTr, Log: log, IdleTTL: udpProxyIdleTTL}.Run(ctx)
938+ return runUDPProxyRuntime(ctx, session, dg, daemonTr, os.Environ(), log)
940939 }
941940
942941 const udpProxyIdleTTL = 15 * time.Minute
@@ -1071,6 +1070,24 @@ func (t *firstHelloTransport) Recv() (ports.Frame, error) {
10711070 return f, nil
10721071 }
10731072
1073+// runStdioProxy applies the remote host environment before the stdio stream
1074+// reaches the daemon. Its explicit environment input keeps this host boundary
1075+// independently testable.
1076+func runStdioProxy(ctx context.Context, client, daemon ports.Transport, env []string, log *slog.Logger) error {
1077+ return proxyTransports(ctx, newFirstHelloTransport(client, "", env), daemon, log)
1078+}
1079+
1080+// runUDPProxyRuntime applies the remote host environment and selected session
1081+// before the datagram stream reaches the daemon.
1082+func runUDPProxyRuntime(ctx context.Context, session string, client, daemon ports.Transport, env []string, log *slog.Logger) error {
1083+ return dgram.ProxyRuntime{
1084+ Client: newFirstHelloTransport(client, session, env),
1085+ Daemon: daemon,
1086+ Log: log,
1087+ IdleTTL: udpProxyIdleTTL,
1088+ }.Run(ctx)
1089+}
1090+
10741091 func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger) error {
10751092 return dgram.ProxyRuntime{Client: a, Daemon: b, Log: log}.Run(ctx)
10761093 }
@@ -794,7 +794,7 @@ func runStdio(ctx context.Context) (retErr error) {
794 }794 }
795 defer func() { _ = transport.Close() }()795 defer func() { _ = transport.Close() }()
796 stdio := sshstdio.NewTransport(os.Stdin, os.Stdout, nil, sshstdio.WithRuntimeObserver(observer))796 stdio := sshstdio.NewTransport(os.Stdin, os.Stdout, nil, sshstdio.WithRuntimeObserver(observer))
797- return proxyTransports(ctx, newFirstHelloTransport(stdio, "", os.Environ()), transport, log)797+ return runStdioProxy(ctx, stdio, transport, os.Environ(), log)
798 }798 }
799 799
800 var (800 var (
@@ -935,8 +935,7 @@ func runUDPProxy(ctx context.Context, session string, ready io.Writer) (retErr e
935 return err935 return err
936 }936 }
937 937
938- dgramTr := newFirstHelloTransport(dg, session, os.Environ())938+ return runUDPProxyRuntime(ctx, session, dg, daemonTr, os.Environ(), log)
939- return dgram.ProxyRuntime{Client: dgramTr, Daemon: daemonTr, Log: log, IdleTTL: udpProxyIdleTTL}.Run(ctx)
940 }939 }
941 940
942 const udpProxyIdleTTL = 15 * time.Minute941 const udpProxyIdleTTL = 15 * time.Minute
@@ -1071,6 +1070,24 @@ func (t *firstHelloTransport) Recv() (ports.Frame, error) {
1071 return f, nil1070 return f, nil
1072 }1071 }
1073 1072
1073+// runStdioProxy applies the remote host environment before the stdio stream
1074+// reaches the daemon. Its explicit environment input keeps this host boundary
1075+// independently testable.
1076+func runStdioProxy(ctx context.Context, client, daemon ports.Transport, env []string, log *slog.Logger) error {
1077+ return proxyTransports(ctx, newFirstHelloTransport(client, "", env), daemon, log)
1078+}
1079+
1080+// runUDPProxyRuntime applies the remote host environment and selected session
1081+// before the datagram stream reaches the daemon.
1082+func runUDPProxyRuntime(ctx context.Context, session string, client, daemon ports.Transport, env []string, log *slog.Logger) error {
1083+ return dgram.ProxyRuntime{
1084+ Client: newFirstHelloTransport(client, session, env),
1085+ Daemon: daemon,
1086+ Log: log,
1087+ IdleTTL: udpProxyIdleTTL,
1088+ }.Run(ctx)
1089+}
1090+
1074 func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger) error {1091 func proxyTransports(ctx context.Context, a, b ports.Transport, log *slog.Logger) error {
1075 return dgram.ProxyRuntime{Client: a, Daemon: b, Log: log}.Run(ctx)1092 return dgram.ProxyRuntime{Client: a, Daemon: b, Log: log}.Run(ctx)
1076 }1093 }
M internal/app/run_test.go +102 -0
@@ -10,6 +10,7 @@ import (
1010 "os/exec"
1111 "path/filepath"
1212 "strings"
13+ "sync"
1314 "testing"
1415 "time"
1516
@@ -83,6 +84,107 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
8384 }
8485 }
8586
87+func TestRunStdioProxyReplacesHelloEnvironmentAtDaemonBoundary(t *testing.T) {
88+ original := remoteProxyHello()
89+ client := newRemoteProxyClient(original)
90+ daemon := newRemoteProxyDaemon()
91+
92+ err := runStdioProxy(context.Background(), client, daemon, []string{"REMOTE=one", "TOKEN=a=b=c"}, nil)
93+ require.NoError(t, err)
94+
95+ got := daemon.firstHello(t)
96+ original.Env = []string{"REMOTE=one", "TOKEN=a=b=c"}
97+ require.Equal(t, original, got)
98+}
99+
100+func TestRunUDPProxyRuntimeReplacesHelloEnvironmentAndSessionAtDaemonBoundary(t *testing.T) {
101+ original := remoteProxyHello()
102+ client := newRemoteProxyClient(original)
103+ daemon := newRemoteProxyDaemon()
104+
105+ err := runUDPProxyRuntime(context.Background(), "remote-work", client, daemon, []string{"REMOTE=udp", "TOKEN=a=b=c"}, nil)
106+ require.NoError(t, err)
107+
108+ got := daemon.firstHello(t)
109+ original.Name = "remote-work"
110+ original.Env = []string{"REMOTE=udp", "TOKEN=a=b=c"}
111+ require.Equal(t, original, got)
112+}
113+
114+func remoteProxyHello() ports.Hello {
115+ return ports.Hello{
116+ Version: ports.ProtocolVersion,
117+ Intent: ports.IntentAttach,
118+ ClientID: [16]byte{1},
119+ ResumeToken: 2,
120+ Size: domain.Size{Cols: 80, Rows: 24},
121+ TermEnv: "client-term",
122+ Cwd: "/client",
123+ TrueColor: true,
124+ MaxOutputInFlight: 1,
125+ Env: []string{"CLIENT=value"},
126+ }
127+}
128+
129+type remoteProxyClient struct {
130+ first ports.Frame
131+ done chan struct{}
132+ once sync.Once
133+}
134+
135+func newRemoteProxyClient(hello ports.Hello) *remoteProxyClient {
136+ return &remoteProxyClient{
137+ first: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)},
138+ done: make(chan struct{}),
139+ }
140+}
141+
142+func (t *remoteProxyClient) Send(ports.Frame) error { return nil }
143+func (t *remoteProxyClient) Recv() (ports.Frame, error) {
144+ if t.first.Type != 0 {
145+ frame := t.first
146+ t.first = ports.Frame{}
147+ return frame, nil
148+ }
149+ <-t.done
150+ return ports.Frame{}, io.EOF
151+}
152+func (t *remoteProxyClient) Close() error {
153+ t.once.Do(func() { close(t.done) })
154+ return nil
155+}
156+
157+type remoteProxyDaemon struct {
158+ sent chan ports.Frame
159+ done chan struct{}
160+ once sync.Once
161+}
162+
163+func newRemoteProxyDaemon() *remoteProxyDaemon {
164+ return &remoteProxyDaemon{sent: make(chan ports.Frame, 1), done: make(chan struct{})}
165+}
166+
167+func (t *remoteProxyDaemon) Send(frame ports.Frame) error {
168+ t.sent <- frame
169+ t.once.Do(func() { close(t.done) })
170+ return nil
171+}
172+func (t *remoteProxyDaemon) Recv() (ports.Frame, error) {
173+ <-t.done
174+ return ports.Frame{}, io.EOF
175+}
176+func (t *remoteProxyDaemon) Close() error {
177+ t.once.Do(func() { close(t.done) })
178+ return nil
179+}
180+func (t *remoteProxyDaemon) firstHello(testingT *testing.T) ports.Hello {
181+ testingT.Helper()
182+ frame := <-t.sent
183+ hello, err := ports.UnmarshalHello(frame.Payload)
184+ require.NoError(testingT, err)
185+ return hello
186+}
187+
86188 func TestRunUDPProxyUsesBoundedClientMaxPending(t *testing.T) {
87189 require.Equal(t, 32, udpProxyClientTransportOptions.MaxPending)
88190 }
@@ -10,6 +10,7 @@ import (
10 "os/exec"10 "os/exec"
11 "path/filepath"11 "path/filepath"
12 "strings"12 "strings"
13+ "sync"
13 "testing"14 "testing"
14 "time"15 "time"
15 16
@@ -83,6 +84,107 @@ func TestFirstHelloTransportReplacesRemoteEnvironment(t *testing.T) {
83 }84 }
84 }85 }
85 86
87+func TestRunStdioProxyReplacesHelloEnvironmentAtDaemonBoundary(t *testing.T) {
88+ original := remoteProxyHello()
89+ client := newRemoteProxyClient(original)
90+ daemon := newRemoteProxyDaemon()
91+
92+ err := runStdioProxy(context.Background(), client, daemon, []string{"REMOTE=one", "TOKEN=a=b=c"}, nil)
93+ require.NoError(t, err)
94+
95+ got := daemon.firstHello(t)
96+ original.Env = []string{"REMOTE=one", "TOKEN=a=b=c"}
97+ require.Equal(t, original, got)
98+}
99+
100+func TestRunUDPProxyRuntimeReplacesHelloEnvironmentAndSessionAtDaemonBoundary(t *testing.T) {
101+ original := remoteProxyHello()
102+ client := newRemoteProxyClient(original)
103+ daemon := newRemoteProxyDaemon()
104+
105+ err := runUDPProxyRuntime(context.Background(), "remote-work", client, daemon, []string{"REMOTE=udp", "TOKEN=a=b=c"}, nil)
106+ require.NoError(t, err)
107+
108+ got := daemon.firstHello(t)
109+ original.Name = "remote-work"
110+ original.Env = []string{"REMOTE=udp", "TOKEN=a=b=c"}
111+ require.Equal(t, original, got)
112+}
113+
114+func remoteProxyHello() ports.Hello {
115+ return ports.Hello{
116+ Version: ports.ProtocolVersion,
117+ Intent: ports.IntentAttach,
118+ ClientID: [16]byte{1},
119+ ResumeToken: 2,
120+ Size: domain.Size{Cols: 80, Rows: 24},
121+ TermEnv: "client-term",
122+ Cwd: "/client",
123+ TrueColor: true,
124+ MaxOutputInFlight: 1,
125+ Env: []string{"CLIENT=value"},
126+ }
127+}
128+
129+type remoteProxyClient struct {
130+ first ports.Frame
131+ done chan struct{}
132+ once sync.Once
133+}
134+
135+func newRemoteProxyClient(hello ports.Hello) *remoteProxyClient {
136+ return &remoteProxyClient{
137+ first: ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)},
138+ done: make(chan struct{}),
139+ }
140+}
141+
142+func (t *remoteProxyClient) Send(ports.Frame) error { return nil }
143+func (t *remoteProxyClient) Recv() (ports.Frame, error) {
144+ if t.first.Type != 0 {
145+ frame := t.first
146+ t.first = ports.Frame{}
147+ return frame, nil
148+ }
149+ <-t.done
150+ return ports.Frame{}, io.EOF
151+}
152+func (t *remoteProxyClient) Close() error {
153+ t.once.Do(func() { close(t.done) })
154+ return nil
155+}
156+
157+type remoteProxyDaemon struct {
158+ sent chan ports.Frame
159+ done chan struct{}
160+ once sync.Once
161+}
162+
163+func newRemoteProxyDaemon() *remoteProxyDaemon {
164+ return &remoteProxyDaemon{sent: make(chan ports.Frame, 1), done: make(chan struct{})}
165+}
166+
167+func (t *remoteProxyDaemon) Send(frame ports.Frame) error {
168+ t.sent <- frame
169+ t.once.Do(func() { close(t.done) })
170+ return nil
171+}
172+func (t *remoteProxyDaemon) Recv() (ports.Frame, error) {
173+ <-t.done
174+ return ports.Frame{}, io.EOF
175+}
176+func (t *remoteProxyDaemon) Close() error {
177+ t.once.Do(func() { close(t.done) })
178+ return nil
179+}
180+func (t *remoteProxyDaemon) firstHello(testingT *testing.T) ports.Hello {
181+ testingT.Helper()
182+ frame := <-t.sent
183+ hello, err := ports.UnmarshalHello(frame.Payload)
184+ require.NoError(testingT, err)
185+ return hello
186+}
187+
86 func TestRunUDPProxyUsesBoundedClientMaxPending(t *testing.T) {188 func TestRunUDPProxyUsesBoundedClientMaxPending(t *testing.T) {
87 require.Equal(t, 32, udpProxyClientTransportOptions.MaxPending)189 require.Equal(t, 32, udpProxyClientTransportOptions.MaxPending)
88 }190 }

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