remote_hosts_test.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"errors"
6	"testing"
7
8	"github.com/stretchr/testify/mock"
9	"github.com/stretchr/testify/require"
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)
15
16func TestParseHostArgs(t *testing.T) {
17	tests := []struct {
18		name          string
19		args          []string
20		wantAction    string
21		wantTarget    string
22		wantTransport string
23		wantErr       string
24		nonUsageErr   bool
25	}{
26		{name: "add defaults to quic", args: []string{"add", "arch"}, wantAction: "add", wantTarget: "arch", wantTransport: "quic"},
27		{name: "add explicit quic", args: []string{"add", "--transport", "quic", "arch"}, wantAction: "add", wantTarget: "arch", wantTransport: "quic"},
28		{name: "add ssh", args: []string{"add", "--transport", "ssh", "user@arch"}, wantAction: "add", wantTarget: "user@arch", wantTransport: "ssh"},
29		{name: "add ssh equals form", args: []string{"add", "--transport=ssh", "arch"}, wantAction: "add", wantTarget: "arch", wantTransport: "ssh"},
30		{name: "add flag after target", args: []string{"add", "arch", "--transport", "ssh"}, wantAction: "add", wantTarget: "arch", wantTransport: "ssh"},
31		{name: "add invalid value", args: []string{"add", "--transport", "stdio", "arch"}, wantErr: `want "quic" or "ssh"`},
32		{name: "add invalid equals value", args: []string{"add", "--transport=udp", "arch"}, wantErr: `want "quic" or "ssh"`},
33		{name: "add empty equals value", args: []string{"add", "--transport=", "arch"}, wantErr: `want "quic" or "ssh"`},
34		{name: "add missing value", args: []string{"add", "arch", "--transport"}, wantErr: "requires a value"},
35		{name: "add repeated flag", args: []string{"add", "--transport", "ssh", "--transport", "quic", "arch"}, wantErr: "once"},
36		{name: "add unknown flag", args: []string{"add", "--bogus", "arch"}, wantErr: "unknown flag"},
37		{name: "add missing target", args: []string{"add"}, wantErr: "requires a host target"},
38		{name: "add flag without target", args: []string{"add", "--transport", "ssh"}, wantErr: "requires a host target"},
39		{name: "add extra target", args: []string{"add", "--transport", "ssh", "arch", "extra"}, wantErr: "exactly one host target"},
40		{name: "add invalid target", args: []string{"add", "--transport", "ssh", " bad"}, wantErr: "host", nonUsageErr: true},
41		{name: "rm", args: []string{"rm", "arch"}, wantAction: "rm", wantTarget: "arch"},
42		{name: "rm rejects transport", args: []string{"rm", "--transport", "ssh", "arch"}, wantErr: "unknown flag"},
43		{name: "rm rejects transport after target", args: []string{"rm", "arch", "--transport=ssh"}, wantErr: "exactly one host target"},
44		{name: "rm missing target", args: []string{"rm"}, wantErr: "requires a host target"},
45		{name: "list", args: []string{"list"}, wantAction: "list"},
46		{name: "list rejects transport", args: []string{"list", "--transport", "ssh"}, wantErr: "unknown flag"},
47		{name: "list extra", args: []string{"list", "arch"}, wantErr: "accepts no arguments"},
48	}
49	for _, tt := range tests {
50		t.Run(tt.name, func(t *testing.T) {
51			got, err := parseHostArgs(tt.args)
52			if tt.wantErr != "" {
53				require.Error(t, err)
54				require.Contains(t, err.Error(), tt.wantErr)
55				_, usage := errors.AsType[*usageError](err)
56				require.Equal(t, !tt.nonUsageErr, usage, "error type %T", err)
57				return
58			}
59			require.NoError(t, err)
60			require.Equal(t, kindHost, got.kind)
61			require.Equal(t, tt.wantAction, got.hostAction)
62			require.Equal(t, tt.wantTarget, got.hostTarget)
63			require.Equal(t, tt.wantTransport, got.hostTransport)
64		})
65	}
66}
67
68func TestHostAddPolicyFollowsTransport(t *testing.T) {
69	tests := []struct {
70		name       string
71		args       []string
72		wantPolicy ports.BrokerPolicy
73	}{
74		{name: "default", args: []string{"add", "arch"}, wantPolicy: remoteBrokerPolicy(hostTransportQUIC)},
75		{name: "quic", args: []string{"add", "--transport", "quic", "arch"}, wantPolicy: remoteBrokerPolicy(hostTransportQUIC)},
76		{name: "ssh", args: []string{"add", "--transport", "ssh", "arch"}, wantPolicy: remoteBrokerPolicy(hostTransportSSH)},
77	}
78	for _, tt := range tests {
79		t.Run(tt.name, func(t *testing.T) {
80			cmd, err := parseHostArgs(tt.args)
81			require.NoError(t, err)
82			service := portsmocks.NewMockBrokerService(t)
83			service.EXPECT().AddHost(mock.Anything, "arch", tt.wantPolicy).Return(domain.RemoteRegistration{}, nil).Once()
84			service.EXPECT().Close().Return(nil).Once()
85			deps := remoteHostDeps{connect: func(context.Context) (ports.BrokerService, error) { return service, nil }}
86			require.NoError(t, runHostCommand(context.Background(), cmd, deps))
87		})
88	}
89}
90
91func TestHostAddReportsTransportConflict(t *testing.T) {
92	// ipcConflict is the shape a conflict has after crossing broker IPC: only
93	// the code survives, not the sentinel.
94	ipcConflict := ports.BrokerError{Code: ports.BrokerErrorHostConflict, Text: "host registration conflict"}
95	storedSSH := ports.BrokerSnapshot{Daemons: []ports.BrokerDaemonObservation{
96		{Endpoint: "arch", Policy: remoteBrokerPolicy(hostTransportSSH)},
97	}}
98	storedLegacy := ports.BrokerSnapshot{Daemons: []ports.BrokerDaemonObservation{
99		{Endpoint: "arch", Policy: ports.BrokerPolicy{Transport: "stdio"}},
100	}}
101	tests := []struct {
102		name     string
103		addErr   error
104		snapshot ports.BrokerSnapshot
105		wantHint bool
106	}{
107		{name: "in-process sentinel with other transport", addErr: ports.ErrBrokerHostConflict, snapshot: storedSSH, wantHint: true},
108		{name: "IPC code with other transport", addErr: ipcConflict, snapshot: storedSSH, wantHint: true},
109		{name: "same transport conflicts elsewhere", addErr: ipcConflict, snapshot: ports.BrokerSnapshot{Daemons: []ports.BrokerDaemonObservation{
110			{Endpoint: "arch", Policy: remoteBrokerPolicy(hostTransportQUIC)},
111		}}},
112		{name: "host not in snapshot", addErr: ipcConflict, snapshot: ports.BrokerSnapshot{}},
113		{name: "unrecognised stored transport", addErr: ipcConflict, snapshot: storedLegacy},
114	}
115	for _, tt := range tests {
116		t.Run(tt.name, func(t *testing.T) {
117			service := portsmocks.NewMockBrokerService(t)
118			service.EXPECT().AddHost(mock.Anything, "arch", remoteBrokerPolicy(hostTransportQUIC)).Return(domain.RemoteRegistration{}, tt.addErr).Once()
119			service.EXPECT().Snapshot().Return(tt.snapshot).Once()
120			service.EXPECT().Close().Return(nil).Once()
121			deps := remoteHostDeps{connect: func(context.Context) (ports.BrokerService, error) { return service, nil }}
122			err := runHostCommand(context.Background(), command{kind: kindHost, hostAction: hostActionAdd, hostTarget: "arch", hostTransport: hostTransportQUIC}, deps)
123			require.ErrorIs(t, err, tt.addErr)
124			if !tt.wantHint {
125				require.NotContains(t, err.Error(), "vev host rm")
126				return
127			}
128			require.Contains(t, err.Error(), `transport "ssh"`)
129			require.Contains(t, err.Error(), "vev host rm arch")
130		})
131	}
132}

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