v0.7.0 4c16dd8

env_test.go RawHistoryFolder

1package app
2
3import (
4	"bytes"
5	"context"
6	"errors"
7	"testing"
8
9	"github.com/bnema/vev/internal/ports"
10	"github.com/bnema/vev/internal/protocol"
11	"github.com/bnema/vev/internal/protocol/wire"
12	"github.com/stretchr/testify/require"
13)
14
15func TestParseEnvArgs(t *testing.T) {
16	tests := []struct {
17		name    string
18		args    []string
19		want    envInvocation
20		wantErr string
21	}{
22		{name: "fish", args: []string{"fish"}, want: envInvocation{shell: "fish"}},
23		{name: "sh with session", args: []string{"sh", "-s", "work"}, want: envInvocation{shell: "sh", session: "work"}},
24		{name: "session before shell", args: []string{"-s", "work", "fish"}, want: envInvocation{shell: "fish", session: "work"}},
25		{name: "help without shell", args: []string{"--help"}, want: envInvocation{help: true}},
26		{name: "missing shell", wantErr: "requires a shell"},
27		{name: "two shells", args: []string{"fish", "sh"}, wantErr: "one shell"},
28		{name: "same shell twice", args: []string{"fish", "fish"}, wantErr: "one shell"},
29		{name: "unsupported shell", args: []string{"nu"}, wantErr: `unsupported shell "nu"`},
30		{name: "json belongs to cmd env", args: []string{"--json"}, wantErr: `unknown flag "--json"`},
31		{name: "old shell flag", args: []string{"--fish"}, wantErr: `unknown flag "--fish"`},
32		{name: "missing session", args: []string{"fish", "-s"}, wantErr: "requires a session name"},
33	}
34	for _, tt := range tests {
35		t.Run(tt.name, func(t *testing.T) {
36			got, err := parseEnvArgs(tt.args)
37			if tt.wantErr != "" {
38				require.ErrorContains(t, err, tt.wantErr)
39				require.Equal(t, 2, exitCode(err))
40				return
41			}
42			require.NoError(t, err)
43			require.Equal(t, tt.want, got)
44		})
45	}
46}
47
48func TestWriteShellEnvironment(t *testing.T) {
49	encoded := `{"WAYLAND_DISPLAY":"wayland-1","SSH_AUTH_SOCK":null,"ODD":"it's \\ $HOME"}`
50	tests := []struct {
51		name    string
52		shell   string
53		encoded string
54		want    string
55		wantErr string
56	}{
57		{
58			name:    "fish sets and erases",
59			shell:   "fish",
60			encoded: encoded,
61			want:    "set -gx ODD 'it\\'s \\\\ $HOME'\nset -e -g SSH_AUTH_SOCK\nset -gx WAYLAND_DISPLAY 'wayland-1'\n",
62		},
63		{
64			name:    "sh exports and unsets",
65			shell:   "sh",
66			encoded: encoded,
67			want:    "export ODD='it'\\''s \\ $HOME'\nunset SSH_AUTH_SOCK\nexport WAYLAND_DISPLAY='wayland-1'\n",
68		},
69		{name: "empty object", shell: "sh", encoded: `{}`, want: ""},
70		{name: "injected name", shell: "sh", encoded: `{"BAD;rm":"x"}`, wantErr: "invalid session environment name"},
71		{name: "leading digit", shell: "sh", encoded: `{"1X":"x"}`, wantErr: "invalid session environment name"},
72		{name: "not json", shell: "sh", encoded: `not json`, wantErr: "decoding session environment"},
73	}
74	for _, tt := range tests {
75		t.Run(tt.name, func(t *testing.T) {
76			var out bytes.Buffer
77			err := writeShellEnvironment(&out, tt.shell, tt.encoded)
78			if tt.wantErr != "" {
79				require.ErrorContains(t, err, tt.wantErr)
80				require.Empty(t, out.String())
81				return
82			}
83			require.NoError(t, err)
84			require.Equal(t, tt.want, out.String())
85		})
86	}
87}
88
89func TestRunEnvWithDeps(t *testing.T) {
90	const reply = `{"SSH_AUTH_SOCK":null,"WAYLAND_DISPLAY":"wayland-1"}` + "\n"
91	tests := []struct {
92		name        string
93		invocation  envInvocation
94		vev         string
95		result      protocol.CommandResult
96		want        string
97		wantErr     string
98		wantCode    int
99		wantRequest protocol.CommandRequest
100	}{
101		{
102			name:        "inside a pane follows the pane session",
103			invocation:  envInvocation{shell: "fish"},
104			vev:         "session=work,tab=t_1,pane=p_1",
105			result:      protocol.CommandResult{RequestID: 1, Outcome: protocol.CommandSucceeded, Output: reply},
106			want:        "set -e -g SSH_AUTH_SOCK\nset -gx WAYLAND_DISPLAY 'wayland-1'\n",
107			wantRequest: protocol.CommandRequest{Slug: "env", JSON: true, TargetSession: "work", TargetTab: "t_1", TargetPane: "p_1"},
108		},
109		{
110			name:        "explicit session outside a pane",
111			invocation:  envInvocation{shell: "sh", session: "work"},
112			result:      protocol.CommandResult{RequestID: 1, Outcome: protocol.CommandSucceeded, Output: reply},
113			want:        "unset SSH_AUTH_SOCK\nexport WAYLAND_DISPLAY='wayland-1'\n",
114			wantRequest: protocol.CommandRequest{Slug: "env", JSON: true, TargetSession: "work"},
115		},
116		{
117			name:        "explicit session inside a pane targets that session only",
118			invocation:  envInvocation{shell: "sh", session: "other"},
119			vev:         "session=work,tab=t_1,pane=p_1",
120			result:      protocol.CommandResult{RequestID: 1, Outcome: protocol.CommandSucceeded, Output: reply},
121			want:        "unset SSH_AUTH_SOCK\nexport WAYLAND_DISPLAY='wayland-1'\n",
122			wantRequest: protocol.CommandRequest{Slug: "env", JSON: true, TargetSession: "other"},
123		},
124		{
125			name:       "invalid daemon output prints no shell code",
126			invocation: envInvocation{shell: "sh", session: "work"},
127			result:     protocol.CommandResult{RequestID: 1, Outcome: protocol.CommandSucceeded, Output: `{"BAD;rm":"x"}`},
128			wantErr:    "invalid session environment name",
129			wantCode:   1,
130		},
131		{
132			name:       "outside a pane without a session",
133			invocation: envInvocation{shell: "fish"},
134			wantErr:    "inside a vev pane",
135			wantCode:   2,
136		},
137		{
138			name:       "daemon failure prints no shell code",
139			invocation: envInvocation{shell: "fish", session: "gone"},
140			result:     protocol.CommandResult{RequestID: 1, Outcome: protocol.CommandFailed, Code: protocol.ErrNoSuchTarget, Text: "no such session: gone"},
141			wantErr:    "no such session: gone",
142			wantCode:   1,
143		},
144		{
145			name:       "help",
146			invocation: envInvocation{help: true},
147			want:       envHelp + "\n",
148		},
149	}
150	for _, tt := range tests {
151		t.Run(tt.name, func(t *testing.T) {
152			stream := newSeamBrokerStream(tt.result)
153			service := newSeamBrokerService(stream)
154			var out bytes.Buffer
155			err := runEnvWithDeps(context.Background(), tt.invocation, cmdDeps{
156				stdout:  &out,
157				getenv:  func(string) string { return tt.vev },
158				connect: func(context.Context) (ports.BrokerService, error) { return service, nil },
159				dial: func(context.Context, string) (wire.Transport, error) {
160					return nil, errors.New("unexpected dial")
161				},
162			})
163			if tt.wantErr != "" {
164				require.ErrorContains(t, err, tt.wantErr)
165				require.Equal(t, tt.wantCode, exitCode(err))
166				require.Empty(t, out.String())
167				return
168			}
169			require.NoError(t, err)
170			require.Equal(t, tt.want, out.String())
171			if tt.wantRequest.Slug == "" {
172				require.Zero(t, stream.sentCount())
173				return
174			}
175			stream.mu.Lock()
176			require.Len(t, stream.sent, 1)
177			request, ok := stream.sent[0].(protocol.CommandRequest)
178			stream.mu.Unlock()
179			require.True(t, ok)
180			require.Equal(t, tt.wantRequest.Slug, request.Slug)
181			require.Equal(t, tt.wantRequest.JSON, request.JSON)
182			require.Equal(t, tt.wantRequest.TargetSession, request.TargetSession)
183			require.Equal(t, tt.wantRequest.TargetTab, request.TargetTab)
184			require.Equal(t, tt.wantRequest.TargetPane, request.TargetPane)
185		})
186	}
187}

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