detached 451ff75

test(app): cover attach environment refresh

commit
451ff75073f336111394996551c6cd8e88034442
author
brice · · 2026-07-15 05:39 UTC
parent
db0ee28
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
1 file · +65 -1
M internal/app/daemon_integration_test.go +65 -1
@@ -7,6 +7,7 @@ import (
77 "errors"
88 "io"
99 "log/slog"
10+ "os"
1011 "path/filepath"
1112 "strings"
1213 "testing"
@@ -73,10 +74,15 @@ func recvPump(tr ports.Transport) *pump {
7374
7475 // attach dials, handshakes, and returns the transport plus its frame pump.
7576 func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size) (ports.Transport, *pump) {
77+ t.Helper()
78+ return attachWithEnvironment(t, dir, intent, name, sz, nil)
79+}
80+
81+func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string, sz domain.Size, env []string) (ports.Transport, *pump) {
7682 t.Helper()
7783 tr, err := ipc.DialContext(context.Background(), dir)
7884 require.NoError(t, err)
79- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color"}
85+ hello := ports.Hello{Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env}
8086 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}))
8187 p := recvPump(tr)
8288 select {
@@ -189,6 +195,64 @@ func screenText(s *vt.Screen) string {
189195 return b.String()
190196 }
191197
198+func shellFixture(t *testing.T, label string) string {
199+ t.Helper()
200+ path := filepath.Join(t.TempDir(), "shell")
201+ require.NoError(t, os.WriteFile(path, []byte("#!/bin/sh\nprintf 'SHELL_COMMAND="+label+"\\n'\nexec /bin/sh\n"), 0o700))
202+ return path
203+}
204+
205+func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain.Size, wantTestEnv, wantShell, wantRuntimeDir, wantWayland string) {
206+ t.Helper()
207+ command := "printf '\\033[2J\\033[H'; printf 'VEV_TEST_ENV=%s SHELL=%s XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s TERM=%s COLORTERM=%s TERM_PROGRAM=%s VEV_PREFIX=%.24s\\n' \"$VEV_TEST_ENV\" \"${SHELL##*/}\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$TERM\" \"$COLORTERM\" \"$TERM_PROGRAM\" \"$VEV\"\n"
208+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(command)})}))
209+ text := awaitScreenText(t, p, sz, "TERM_PROGRAM=vev")
210+ for _, want := range []string{
211+ "VEV_TEST_ENV=" + wantTestEnv,
212+ "SHELL=" + filepath.Base(wantShell),
213+ "XDG_RUNTIME_DIR=" + wantRuntimeDir,
214+ "WAYLAND_DISPLAY=" + wantWayland,
215+ "TERM=xterm-direct",
216+ "COLORTERM=truecolor",
217+ "TERM_PROGRAM=vev",
218+ "VEV_PREFIX=session=environment,tab=",
219+ } {
220+ require.Contains(t, text, want)
221+ }
222+}
223+
224+func TestIntegration_AttachEnvironmentRefreshesFuturePTYChildren(t *testing.T) {
225+ sz := domain.Size{Cols: 80, Rows: 24}
226+ dir, _ := startDaemon(t)
227+ firstShell := shellFixture(t, "first")
228+ secondShell := shellFixture(t, "second")
229+ firstEnv := []string{
230+ "VEV_TEST_ENV=first", "SHELL=" + firstShell, "XDG_RUNTIME_DIR=/run/first", "WAYLAND_DISPLAY=wayland-first",
231+ "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
232+ }
233+
234+ tr1, p1 := attachWithEnvironment(t, dir, ports.IntentNew, "environment", sz, firstEnv)
235+ defer func() { _ = tr1.Close() }()
236+ awaitText(t, p1, sz, "SHELL_COMMAND=first")
237+ assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")
238+
239+ secondEnv := []string{
240+ "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",
241+ "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
242+ }
243+ tr2, p2 := attachWithEnvironment(t, dir, ports.IntentAttach, "environment", sz, secondEnv)
244+ defer func() { _ = tr2.Close() }()
245+
246+ // The first shell was already running, so it retains its original environment.
247+ assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")
248+
249+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
250+ awaitText(t, p2, sz, "Commands")
251+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))
252+ awaitText(t, p2, sz, "SHELL_COMMAND=second")
253+ assertChildEnvironment(t, tr2, p2, sz, "second", secondShell, "/run/second", "wayland-second")
254+}
255+
192256 func TestIntegration_AttachFirstOutput(t *testing.T) {
193257 sz := domain.Size{Cols: 80, Rows: 24}
194258 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf HELLO; sleep 30"}))
@@ -7,6 +7,7 @@ import (
7 "errors"7 "errors"
8 "io"8 "io"
9 "log/slog"9 "log/slog"
10+ "os"
10 "path/filepath"11 "path/filepath"
11 "strings"12 "strings"
12 "testing"13 "testing"
@@ -73,10 +74,15 @@ func recvPump(tr ports.Transport) *pump {
73 74
74 // attach dials, handshakes, and returns the transport plus its frame pump.75 // attach dials, handshakes, and returns the transport plus its frame pump.
75 func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size) (ports.Transport, *pump) {76 func attach(t *testing.T, dir string, intent uint8, name string, sz domain.Size) (ports.Transport, *pump) {
77+ t.Helper()
78+ return attachWithEnvironment(t, dir, intent, name, sz, nil)
79+}
80+
81+func attachWithEnvironment(t *testing.T, dir string, intent uint8, name string, sz domain.Size, env []string) (ports.Transport, *pump) {
76 t.Helper()82 t.Helper()
77 tr, err := ipc.DialContext(context.Background(), dir)83 tr, err := ipc.DialContext(context.Background(), dir)
78 require.NoError(t, err)84 require.NoError(t, err)
79- hello := ports.Hello{Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color"}85+ hello := ports.Hello{Version: ports.ProtocolVersion, Intent: intent, Name: name, Size: sz, TermEnv: "xterm-256color", TrueColor: true, Env: env}
80 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}))86 require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)}))
81 p := recvPump(tr)87 p := recvPump(tr)
82 select {88 select {
@@ -189,6 +195,64 @@ func screenText(s *vt.Screen) string {
189 return b.String()195 return b.String()
190 }196 }
191 197
198+func shellFixture(t *testing.T, label string) string {
199+ t.Helper()
200+ path := filepath.Join(t.TempDir(), "shell")
201+ require.NoError(t, os.WriteFile(path, []byte("#!/bin/sh\nprintf 'SHELL_COMMAND="+label+"\\n'\nexec /bin/sh\n"), 0o700))
202+ return path
203+}
204+
205+func assertChildEnvironment(t *testing.T, tr ports.Transport, p *pump, sz domain.Size, wantTestEnv, wantShell, wantRuntimeDir, wantWayland string) {
206+ t.Helper()
207+ command := "printf '\\033[2J\\033[H'; printf 'VEV_TEST_ENV=%s SHELL=%s XDG_RUNTIME_DIR=%s WAYLAND_DISPLAY=%s TERM=%s COLORTERM=%s TERM_PROGRAM=%s VEV_PREFIX=%.24s\\n' \"$VEV_TEST_ENV\" \"${SHELL##*/}\" \"$XDG_RUNTIME_DIR\" \"$WAYLAND_DISPLAY\" \"$TERM\" \"$COLORTERM\" \"$TERM_PROGRAM\" \"$VEV\"\n"
208+ require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte(command)})}))
209+ text := awaitScreenText(t, p, sz, "TERM_PROGRAM=vev")
210+ for _, want := range []string{
211+ "VEV_TEST_ENV=" + wantTestEnv,
212+ "SHELL=" + filepath.Base(wantShell),
213+ "XDG_RUNTIME_DIR=" + wantRuntimeDir,
214+ "WAYLAND_DISPLAY=" + wantWayland,
215+ "TERM=xterm-direct",
216+ "COLORTERM=truecolor",
217+ "TERM_PROGRAM=vev",
218+ "VEV_PREFIX=session=environment,tab=",
219+ } {
220+ require.Contains(t, text, want)
221+ }
222+}
223+
224+func TestIntegration_AttachEnvironmentRefreshesFuturePTYChildren(t *testing.T) {
225+ sz := domain.Size{Cols: 80, Rows: 24}
226+ dir, _ := startDaemon(t)
227+ firstShell := shellFixture(t, "first")
228+ secondShell := shellFixture(t, "second")
229+ firstEnv := []string{
230+ "VEV_TEST_ENV=first", "SHELL=" + firstShell, "XDG_RUNTIME_DIR=/run/first", "WAYLAND_DISPLAY=wayland-first",
231+ "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
232+ }
233+
234+ tr1, p1 := attachWithEnvironment(t, dir, ports.IntentNew, "environment", sz, firstEnv)
235+ defer func() { _ = tr1.Close() }()
236+ awaitText(t, p1, sz, "SHELL_COMMAND=first")
237+ assertChildEnvironment(t, tr1, p1, sz, "first", firstShell, "/run/first", "wayland-first")
238+
239+ secondEnv := []string{
240+ "VEV_TEST_ENV=second", "SHELL=" + secondShell, "XDG_RUNTIME_DIR=/run/second", "WAYLAND_DISPLAY=wayland-second",
241+ "TERM=client", "COLORTERM=client", "TERM_PROGRAM=client", "VEV=client",
242+ }
243+ tr2, p2 := attachWithEnvironment(t, dir, ports.IntentAttach, "environment", sz, secondEnv)
244+ defer func() { _ = tr2.Close() }()
245+
246+ // The first shell was already running, so it retains its original environment.
247+ assertChildEnvironment(t, tr2, p2, sz, "first", firstShell, "/run/first", "wayland-first")
248+
249+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("\x1b ")})}))
250+ awaitText(t, p2, sz, "Commands")
251+ require.NoError(t, tr2.Send(ports.Frame{Type: ports.MsgInput, Payload: ports.MarshalInput(ports.Input{Data: []byte("CNT\r")})}))
252+ awaitText(t, p2, sz, "SHELL_COMMAND=second")
253+ assertChildEnvironment(t, tr2, p2, sz, "second", secondShell, "/run/second", "wayland-second")
254+}
255+
192 func TestIntegration_AttachFirstOutput(t *testing.T) {256 func TestIntegration_AttachFirstOutput(t *testing.T) {
193 sz := domain.Size{Cols: 80, Rows: 24}257 sz := domain.Size{Cols: 80, Rows: 24}
194 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf HELLO; sleep 30"}))258 dir, _ := startDaemon(t, daemon.WithShell("/bin/sh", []string{"-c", "printf HELLO; sleep 30"}))

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