bar_script_test.go RawHistoryFolder

1package daemon
2
3import (
4	"context"
5	"errors"
6	"slices"
7	"strings"
8	"testing"
9	"time"
10
11	"github.com/bnema/vev/internal/domain"
12	"github.com/bnema/vev/internal/ports"
13	portsmocks "github.com/bnema/vev/internal/ports/mocks"
14	"github.com/stretchr/testify/mock"
15)
16
17func TestBarScriptSanitizeOutput(t *testing.T) {
18	tests := []struct {
19		name  string
20		raw   string
21		limit int
22		want  string
23	}{
24		{name: "first line only", raw: "one\ntwo\n", limit: barScriptOutputLimit, want: "one"},
25		{name: "ansi stripped", raw: "\x1b[31mred\x1b[0m", limit: barScriptOutputLimit, want: "red"},
26		{name: "controls stripped", raw: "a\x00b\x07c", limit: barScriptOutputLimit, want: "abc"},
27		{name: "osc stripped", raw: "a\x1b]0;bad\ab", limit: barScriptOutputLimit, want: "ab"},
28		{name: "charset escape stripped", raw: "a\x1b(Bb\x1b)0c", limit: barScriptOutputLimit, want: "abc"},
29		{name: "non csi escape stripped", raw: "a\x1b7b\x1b8c", limit: barScriptOutputLimit, want: "abc"},
30		{name: "c1 controls stripped", raw: "a\u009b31mb\u009d0;bad\ac", limit: barScriptOutputLimit, want: "abc"},
31		{name: "utf8 and nerd font preserved", raw: "󰍛 14:32 ↑3", limit: barScriptOutputLimit, want: "󰍛 14:32 ↑3"},
32		{name: "display capped", raw: strings.Repeat("a", barScriptDisplayLimit+10), limit: barScriptOutputLimit, want: strings.Repeat("a", barScriptDisplayLimit)},
33		{name: "capture capped before first line", raw: strings.Repeat("a", 8) + "\nignored", limit: 4, want: "aaaa"},
34	}
35
36	for _, tt := range tests {
37		t.Run(tt.name, func(t *testing.T) {
38			if got := sanitizeBarScriptOutput([]byte(tt.raw), tt.limit); got != tt.want {
39				t.Fatalf("sanitizeBarScriptOutput() = %q, want %q", got, tt.want)
40			}
41		})
42	}
43}
44
45func TestBarScriptContextEnv(t *testing.T) {
46	base := []string{
47		"PATH=/bin",
48		"VEV_ANCHOR=old",
49		"VEV_SESSION=old",
50		"VEV_TAB=old",
51		"VEV_PANE=old",
52		"VEV_PANE_CWD=/old",
53		"VEV_COLS=1",
54		"VEV_CLIENT_PID=1",
55	}
56	ctx := barScriptContext{Anchor: "top-right", Session: "work", Tab: "2", Pane: "pane-3", PaneCWD: "/repo", Cols: 120, ClientPID: 4242}
57	got := ctx.env(base)
58
59	want := map[string]string{
60		"VEV_ANCHOR":     "top-right",
61		"VEV_SESSION":    "work",
62		"VEV_TAB":        "2",
63		"VEV_PANE":       "pane-3",
64		"VEV_PANE_CWD":   "/repo",
65		"VEV_COLS":       "120",
66		"VEV_CLIENT_PID": "4242",
67	}
68	seen := map[string]int{}
69	for _, entry := range got {
70		key, val, ok := strings.Cut(entry, "=")
71		if !ok {
72			continue
73		}
74		if _, isVEV := want[key]; isVEV {
75			seen[key]++
76			if val != want[key] {
77				t.Fatalf("%s = %q, want %q in env %v", key, val, want[key], got)
78			}
79		}
80	}
81	for key := range want {
82		if seen[key] != 1 {
83			t.Fatalf("%s appears %d times, want 1 in env %v", key, seen[key], got)
84		}
85	}
86}
87
88func TestBarScriptContextEnvEmptyClientPID(t *testing.T) {
89	got := barScriptContext{}.env([]string{"VEV_CLIENT_PID=99"})
90	if !slices.Equal(got[len(got)-1:], []string{"VEV_CLIENT_PID="}) || slices.Contains(got, "VEV_CLIENT_PID=99") {
91		t.Fatalf("env = %v, want an empty VEV_CLIENT_PID", got)
92	}
93}
94
95func TestBarClientPID(t *testing.T) {
96	client := func(pid uint32, focus domain.TerminalFocus) *attachedClient {
97		ac := &attachedClient{}
98		ac.clientPID.Store(pid)
99		ac.setTerminalFocus(focus)
100		return ac
101	}
102	remote := client(0, domain.TerminalFocusFocused)
103	first := client(10, domain.TerminalFocusUnfocused)
104	focused := client(20, domain.TerminalFocusFocused)
105	for _, tc := range []struct {
106		name string
107		acs  []*attachedClient
108		want uint32
109	}{
110		{"none", nil, 0},
111		{"remote only", []*attachedClient{remote}, 0},
112		{"first local", []*attachedClient{remote, first}, 10},
113		{"focused local wins", []*attachedClient{first, focused}, 20},
114	} {
115		if got := barClientPID(tc.acs); got != tc.want {
116			t.Errorf("%s: got %d, want %d", tc.name, got, tc.want)
117		}
118	}
119}
120
121func TestBarScriptRunner(t *testing.T) {
122	t.Run("nil runner returns explicit error for command", func(t *testing.T) {
123		runner := barScriptRunner{}
124		got, err := runner.run(context.Background(), "date", nil, barScriptContext{})
125		if err == nil || err.Error() != "bar script command runner is nil" {
126			t.Fatalf("run err = %v, want explicit nil runner error", err)
127		}
128		if got != "" {
129			t.Fatalf("run output = %q, want empty", got)
130		}
131	})
132
133	t.Run("empty command skips port runner", func(t *testing.T) {
134		runner := barScriptRunner{}
135		got, err := runner.run(context.Background(), " \t\n ", nil, barScriptContext{})
136		if err != nil {
137			t.Fatalf("run err = %v, want nil", err)
138		}
139		if got != "" {
140			t.Fatalf("run output = %q, want empty", got)
141		}
142	})
143
144	t.Run("success sanitizes first line and passes command spec", func(t *testing.T) {
145		portRunner := portsmocks.NewMockShellCommandRunner(t)
146		portRunner.EXPECT().Run(mock.Anything, mock.MatchedBy(func(spec ports.CommandSpec) bool {
147			return spec.Command == "printf 'ok\\nignored'" &&
148				spec.Timeout == 50*time.Millisecond &&
149				spec.StdoutLimit == barScriptOutputLimit &&
150				containsEnv(spec.Env, "PATH=/bin") &&
151				containsEnv(spec.Env, "VEV_ANCHOR=top-right") &&
152				containsEnv(spec.Env, "VEV_SESSION=work") &&
153				containsEnv(spec.Env, "VEV_TAB=tab-1") &&
154				containsEnv(spec.Env, "VEV_PANE=pane-1") &&
155				containsEnv(spec.Env, "VEV_PANE_CWD=/repo") &&
156				containsEnv(spec.Env, "VEV_COLS=120") &&
157				!containsEnv(spec.Env, "VEV_ANCHOR=old")
158		})).Return(ports.ShellCommandResult{Stdout: []byte("\x1b[31mok\x1b[0m\nignored")}, nil)
159		runner := barScriptRunner{runner: portRunner, timeout: 50 * time.Millisecond}
160
161		got, err := runner.run(context.Background(), "printf 'ok\\nignored'",
162			[]string{"PATH=/bin", "VEV_ANCHOR=old"},
163			barScriptContext{Anchor: "top-right", Session: "work", Tab: "tab-1", Pane: "pane-1", PaneCWD: "/repo", Cols: 120})
164		if err != nil {
165			t.Fatalf("run err = %v, want nil", err)
166		}
167		if got != "ok" {
168			t.Fatalf("run output = %q, want %q", got, "ok")
169		}
170	})
171
172	t.Run("port failure returns sanitized partial stdout and error", func(t *testing.T) {
173		wantErr := errors.New("exit 1")
174		portRunner := portsmocks.NewMockShellCommandRunner(t)
175		portRunner.EXPECT().Run(mock.Anything, mock.Anything).
176			Return(ports.ShellCommandResult{Stdout: []byte("partial\x00 output\nignored"), ExitCode: 1}, wantErr)
177		runner := barScriptRunner{runner: portRunner, timeout: time.Second}
178
179		got, err := runner.run(context.Background(), "false", nil, barScriptContext{})
180		if !errors.Is(err, wantErr) {
181			t.Fatalf("run err = %v, want %v", err, wantErr)
182		}
183		if got != "partial output" {
184			t.Fatalf("run output = %q, want %q", got, "partial output")
185		}
186	})
187
188	t.Run("timeout propagates deadline", func(t *testing.T) {
189		portRunner := portsmocks.NewMockShellCommandRunner(t)
190		portRunner.EXPECT().Run(mock.Anything, mock.Anything).RunAndReturn(
191			func(ctx context.Context, _ ports.CommandSpec) (ports.ShellCommandResult, error) {
192				<-ctx.Done()
193				return ports.ShellCommandResult{ExitCode: -1}, ctx.Err()
194			})
195		runner := barScriptRunner{runner: portRunner, timeout: time.Nanosecond}
196
197		_, err := runner.run(context.Background(), "sleep 1", nil, barScriptContext{})
198		if !errors.Is(err, context.DeadlineExceeded) {
199			t.Fatalf("run err = %v, want %v", err, context.DeadlineExceeded)
200		}
201	})
202}
203
204func TestBarScriptRunnerBoundsStdoutBeforeSanitize(t *testing.T) {
205	portRunner := portsmocks.NewMockShellCommandRunner(t)
206	portRunner.EXPECT().Run(mock.Anything, mock.MatchedBy(func(spec ports.CommandSpec) bool {
207		return spec.StdoutLimit == barScriptOutputLimit
208	})).Return(ports.ShellCommandResult{Stdout: []byte(strings.Repeat("a", 4096))}, nil)
209	runner := barScriptRunner{runner: portRunner, timeout: time.Second}
210
211	got, err := runner.run(context.Background(), "long-output", nil, barScriptContext{})
212	if err != nil {
213		t.Fatalf("run bounded output: %v", err)
214	}
215	if len(got) > barScriptDisplayLimit {
216		t.Fatalf("output length = %d, want <= %d", len(got), barScriptDisplayLimit)
217	}
218}
219
220func containsEnv(env []string, entry string) bool {
221	return slices.Contains(env, entry)
222}

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