v0.2.0 80c61e3

preview_test.go RawHistoryFolder

1package remote
2
3import (
4	"context"
5	"encoding/base64"
6	"errors"
7	"fmt"
8	"os/exec"
9	"strings"
10	"testing"
11	"time"
12
13	"github.com/stretchr/testify/require"
14
15	renderer "github.com/bnema/vev-vt"
16	"github.com/bnema/vev/internal/adapters/sshstdio"
17	"github.com/bnema/vev/internal/domain"
18	"github.com/bnema/vev/internal/ports"
19)
20
21func previewClientTargetForTest() domain.RemoteSessionTarget {
22	var lifecycle domain.SessionLifecycleID
23	lifecycle[0] = 1
24	return domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"}
25}
26
27func TestRemotePreviewClientBuildsExactSSHCommandAndDecodesResponse(t *testing.T) {
28	var lifecycle domain.SessionLifecycleID
29	lifecycle[0] = 9
30	target := domain.RemoteSessionTarget{
31		Endpoint: "build@mule", DisplayOrigin: "mule", LifecycleID: lifecycle,
32		SessionName: "work", LiveTabID: "tab-1",
33	}
34	want := ports.RemotePreview{
35		Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,
36		LifecycleID: lifecycle, TabID: target.LiveTabID, Revision: 7, Width: 1, Height: 1,
37		Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},
38	}
39	payload := ports.MarshalRemotePreview(want)
40	require.NotNil(t, payload)
41
42	var gotPath string
43	var gotArgs []string
44	client := &PreviewClient{command: func(ctx context.Context, name string, args ...string) *exec.Cmd {
45		gotPath = name
46		gotArgs = append([]string(nil), args...)
47		if len(args) != 3 {
48			t.Fatalf("ssh args = %#v, want target and one remote command", args)
49		}
50		words := strings.Split(args[2], " ")
51		if len(words) != 3 {
52			t.Fatalf("remote command = %q, want three shell words", args[2])
53		}
54		encoded := strings.Trim(words[2], "'")
55		requestPayload, err := base64.RawURLEncoding.DecodeString(encoded)
56		if err != nil {
57			t.Fatalf("decode request: %v", err)
58		}
59		request, err := ports.UnmarshalRemotePreviewRequest(requestPayload)
60		if err != nil {
61			t.Fatalf("decode request payload: %v", err)
62		}
63		if request.Target != target || request.Width != 1 || request.Height != 1 {
64			t.Fatalf("request = %#v, want target and 1x1 dimensions", request)
65		}
66		return stdoutCmd(ctx, string(payload))
67	}}
68
69	got, err := client.Preview(context.Background(), target, 1, 1)
70	require.NoError(t, err)
71	require.Equal(t, want, got)
72	wantCommand := sshstdio.BuildCommandForRemoteCommand(target.Endpoint, "vev", "_remote-preview", "request")
73	require.Equal(t, wantCommand.Path, gotPath)
74	require.Equal(t, target.Endpoint, gotArgs[1])
75	require.Contains(t, gotArgs[2], "'_remote-preview'")
76}
77
78func TestRemotePreviewClientRejectsMismatchedResponseIdentity(t *testing.T) {
79	target := previewClientTargetForTest()
80	var otherLifecycle domain.SessionLifecycleID
81	otherLifecycle[0] = 2
82	valid := ports.RemotePreview{
83		Version: ports.RemotePreviewSchemaVersion, Status: ports.RemotePreviewOK,
84		LifecycleID: target.LifecycleID, TabID: target.LiveTabID, Revision: 1, Width: 1, Height: 1,
85		Cells: []renderer.Cell{{Rune: 'x', Style: renderer.DefaultStyle()}},
86	}
87	tests := []struct {
88		name   string
89		mutate func(*ports.RemotePreview)
90	}{
91		{name: "lifecycle ID", mutate: func(preview *ports.RemotePreview) { preview.LifecycleID = otherLifecycle }},
92		{name: "tab ID", mutate: func(preview *ports.RemotePreview) { preview.TabID = "tab-2" }},
93	}
94	for _, tt := range tests {
95		t.Run(tt.name, func(t *testing.T) {
96			preview := valid
97			tt.mutate(&preview)
98			payload := ports.MarshalRemotePreview(preview)
99			require.NotNil(t, payload)
100
101			client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
102				return stdoutCmd(ctx, string(payload))
103			}}
104			_, err := client.Preview(context.Background(), target, 1, 1)
105			require.ErrorIs(t, err, errRemotePreviewIdentity)
106		})
107	}
108}
109
110func TestRemotePreviewClientRejectsMalformedResponse(t *testing.T) {
111	client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
112		return stdoutCmd(ctx, "not-a-preview")
113	}}
114	var lifecycle domain.SessionLifecycleID
115	lifecycle[0] = 1
116	target := domain.RemoteSessionTarget{Endpoint: "arch", DisplayOrigin: "arch", LifecycleID: lifecycle, SessionName: "work", LiveTabID: "tab-1"}
117	_, err := client.Preview(context.Background(), target, 1, 1)
118	require.Error(t, err)
119}
120
121func TestRemotePreviewClientRejectsOversizedOutput(t *testing.T) {
122	client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
123		return exec.CommandContext(ctx, "sh", "-c", fmt.Sprintf("head -c %d /dev/zero", ports.RemotePreviewMaxBytes+1))
124	}}
125	target := previewClientTargetForTest()
126	_, err := client.Preview(context.Background(), target, 1, 1)
127	require.ErrorIs(t, err, errRemotePreviewTooLarge)
128}
129
130func TestRemotePreviewClientReturnsStderrDiagnostic(t *testing.T) {
131	client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
132		return exec.CommandContext(ctx, "sh", "-c", "printf 'connection refused' >&2; exit 1")
133	}}
134	target := previewClientTargetForTest()
135	_, err := client.Preview(context.Background(), target, 1, 1)
136	require.ErrorIs(t, err, errRemotePreviewSSH)
137	require.Contains(t, err.Error(), "connection refused")
138}
139
140func TestRemotePreviewClientReturnsInternalTimeoutSentinel(t *testing.T) {
141	client := &PreviewClient{
142		timeout: 20 * time.Millisecond,
143		command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
144			return exec.CommandContext(ctx, "sleep", "10")
145		},
146	}
147	_, err := client.Preview(context.Background(), previewClientTargetForTest(), 1, 1)
148	require.ErrorIs(t, err, ports.ErrRemotePreviewTimeout)
149}
150
151func TestRemotePreviewClientReturnsCallerCancellation(t *testing.T) {
152	started := make(chan struct{})
153	client := &PreviewClient{command: func(ctx context.Context, _ string, _ ...string) *exec.Cmd {
154		close(started)
155		return exec.CommandContext(ctx, "sleep", "10")
156	}}
157	target := previewClientTargetForTest()
158	ctx, cancel := context.WithCancel(context.Background())
159	defer cancel()
160	errCh := make(chan error, 1)
161	go func() {
162		_, err := client.Preview(ctx, target, 1, 1)
163		errCh <- err
164	}()
165	<-started
166	cancel()
167	err := <-errCh
168	require.ErrorIs(t, err, context.Canceled)
169	require.False(t, errors.Is(err, errRemotePreviewSSH))
170}

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