main ece7682

mux_test.go RawHistoryFolder

1package sshstdio
2
3import (
4	"bytes"
5	"context"
6	"log/slog"
7	"os"
8	"path/filepath"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/bnema/vev/internal/protocol/wire"
14	"github.com/stretchr/testify/require"
15)
16
17// TestBuildCommandForMuxIsStdioOnlyAndTrustPreserving proves the documented mux
18// argv requests no PTY while leaving host-key, authentication, and proxy policy
19// entirely to the target's effective OpenSSH configuration.
20func TestBuildCommandForMuxIsStdioOnlyAndTrustPreserving(t *testing.T) {
21	spec := BuildCommandForMux("user@host; touch /tmp/pwn", "vev", "_mux", "--flag")
22	require.Equal(t, "ssh", spec.Path)
23	require.Equal(t, []string{"-T", "--", "user@host; touch /tmp/pwn", "'vev' '_mux' '--flag'"}, spec.Args)
24
25	flat := strings.Join(spec.Args, " ")
26	require.Contains(t, flat, "-T")
27	for _, forbidden := range []string{"StrictHostKeyChecking", "UserKnownHostsFile", "BatchMode", "ProxyCommand"} {
28		require.NotContains(t, flat, forbidden)
29	}
30}
31
32func TestDialMuxContextRejectsEmptyCommandSpec(t *testing.T) {
33	transport, err := DialMuxContext(context.Background(), CommandSpec{}, nil)
34	require.ErrorIs(t, err, ErrMuxConfig)
35	require.Nil(t, transport)
36}
37
38// TestDialMuxContextCanceledBeforeStart proves one canceled setup context is
39// checked before the subprocess starts: nothing is executed and the cancellation
40// remains recognizable.
41func TestDialMuxContextCanceledBeforeStart(t *testing.T) {
42	marker := filepath.Join(t.TempDir(), "started")
43	ctx, cancel := context.WithCancel(context.Background())
44	cancel()
45	transport, err := DialMuxContext(ctx, CommandSpec{Path: "sh", Args: []string{"-c", "touch " + marker + "; sleep 30"}}, nil)
46	require.ErrorIs(t, err, context.Canceled)
47	require.Nil(t, transport)
48	_, statErr := os.Stat(marker)
49	require.ErrorIs(t, statErr, os.ErrNotExist)
50}
51
52// TestDialMuxContextBoundedSanitizedStderr proves a non-clean mux child never
53// leaks raw remote stderr into the public error: the captured text is bounded
54// and sanitized for the logger, and the returned error carries only the typed
55// outcome.
56func TestDialMuxContextBoundedSanitizedStderr(t *testing.T) {
57	const marker = "remote-secret-value"
58	// A terminal-colored diagnostic, and a long tail the ceiling must drop.
59	script := "printf '\\033[31m" + marker + "\\033[0m' >&2; printf 'A%.0s' $(seq 1 20000) >&2; exit 3"
60
61	var logBuf bytes.Buffer
62	logger := slog.New(slog.NewJSONHandler(&logBuf, nil))
63	transport, err := DialMuxContext(context.Background(), CommandSpec{Path: "sh", Args: []string{"-c", script}}, logger)
64	require.NoError(t, err)
65	t.Cleanup(func() { _ = transport.Close() })
66
67	_, recvErr := transport.RecvBounded(wire.PreambleLimit)
68	require.ErrorIs(t, recvErr, ErrMuxSSHExit)
69	require.NotContains(t, recvErr.Error(), marker)
70	require.NotContains(t, recvErr.Error(), "\x1b")
71	require.Less(t, len(recvErr.Error()), muxDiagnosticLimit)
72
73	logged := logBuf.String()
74	require.Contains(t, logged, "ssh exited non-cleanly")
75	require.NotContains(t, logged, `\u001b`)
76	require.Less(t, len(logged), muxStderrLimit+2048, "log must stay bounded")
77}
78
79// TestCappedDiagnosticBoundsAndSanitizes proves the diagnostic capture both
80// caps its retained bytes and strips terminal escapes and control bytes.
81func TestCappedDiagnosticBoundsAndSanitizes(t *testing.T) {
82	sink := newCappedDiagnostic(16)
83	input := []byte("\x1b[31mok\x1b[0m\x00" + strings.Repeat("z", 100))
84	written, err := sink.Write(input)
85	require.NoError(t, err)
86	require.Equal(t, len(input), written)
87	require.True(t, sink.overflow)
88	require.LessOrEqual(t, sink.buf.Len(), 16)
89	require.Equal(t, "okz", sink.String()[:3])
90	require.NotContains(t, sink.String(), "\x1b")
91	require.NotContains(t, sink.String(), "\x00")
92	require.LessOrEqual(t, len(sink.String()), muxDiagnosticLimit)
93}
94
95// TestDialMuxContextCloseTerminatesProcess proves Close owns the subprocess
96// lifetime: it stops a child that ignores its stdin and returns without
97// abandoning the wait.
98func TestDialMuxContextCloseTerminatesProcess(t *testing.T) {
99	transport, err := DialMuxContext(context.Background(), CommandSpec{Path: "sh", Args: []string{"-c", "sleep 30"}}, nil)
100	require.NoError(t, err)
101
102	started := time.Now()
103	closeErr := transport.Close()
104	require.Error(t, closeErr, "a killed child is a non-clean exit")
105	require.Less(t, time.Since(started), sshCloseTimeout+2*time.Second)
106
107	// A second Close is idempotent and joins the same reaped process.
108	require.Error(t, transport.Close())
109	<-time.After(50 * time.Millisecond)
110}

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