v0.2.0 80c61e3

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

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