test(app): cover attach environment refresh
- commit
- 451ff75073f336111394996551c6cd8e88034442
- author
- brice · · 2026-07-15 05:39 UTC
- parent
- db0ee28
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 ( | |||
| 7 | 7 | "errors" | |
| 8 | 8 | "io" | |
| 9 | 9 | "log/slog" | |
| 10 | + | "os" | |
| 10 | 11 | "path/filepath" | |
| 11 | 12 | "strings" | |
| 12 | 13 | "testing" | |
| @@ -73,10 +74,15 @@ func recvPump(tr ports.Transport) *pump { | |||
| 73 | 74 | ||
| 74 | 75 | // attach dials, handshakes, and returns the transport plus its frame pump. | |
| 75 | 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 | 82 | t.Helper() | |
| 77 | 83 | tr, err := ipc.DialContext(context.Background(), dir) | |
| 78 | 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 | 86 | require.NoError(t, tr.Send(ports.Frame{Type: ports.MsgHello, Payload: ports.MarshalHello(hello)})) | |
| 81 | 87 | p := recvPump(tr) | |
| 82 | 88 | select { | |
| @@ -189,6 +195,64 @@ func screenText(s *vt.Screen) string { | |||
| 189 | 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 | 256 | func TestIntegration_AttachFirstOutput(t *testing.T) { | |
| 193 | 257 | sz := domain.Size{Cols: 80, Rows: 24} | |
| 194 | 258 | 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"})) | ||