ui_driver_test.go RawHistoryFolder

1package app
2
3import (
4	"testing"
5
6	"github.com/stretchr/testify/require"
7)
8
9func TestParseUIDriverArgs(t *testing.T) {
10	tests := []struct {
11		name       string
12		args       []string
13		want       uiDriverOptions
14		wantErr    bool
15		wantSocket bool
16	}{
17		{name: "defaults", want: uiDriverOptions{cols: uiDriverDefaultColumns, rows: uiDriverDefaultRows}},
18		{name: "headless", args: []string{"--session", "work", "--cols", "100", "--rows", "40", "--remote", "user@example.com"}, want: uiDriverOptions{session: "work", cols: 100, rows: 40, remote: "user@example.com"}},
19		{name: "picker", args: []string{"--picker", "--cols", "100", "--rows", "40"}, want: uiDriverOptions{picker: true, cols: 100, rows: 40}},
20		{name: "socket", args: []string{"--socket", "/tmp/ui.sock"}, want: uiDriverOptions{socket: "/tmp/ui.sock", cols: uiDriverDefaultColumns, rows: uiDriverDefaultRows}, wantSocket: true},
21		{name: "socket conflicts", args: []string{"--socket", "/tmp/ui.sock", "--rows", "20"}, wantErr: true},
22		{name: "socket conflicts with picker", args: []string{"--socket", "/tmp/ui.sock", "--picker"}, wantErr: true},
23		{name: "picker conflicts with session", args: []string{"--picker", "--session", "work"}, wantErr: true},
24		{name: "picker conflicts with remote", args: []string{"--picker", "--remote", "user@example.com"}, wantErr: true},
25		{name: "duplicate picker", args: []string{"--picker", "--picker"}, wantErr: true},
26		{name: "relative socket", args: []string{"--socket", "ui.sock"}, wantErr: true},
27		{name: "duplicate socket", args: []string{"--socket", "/tmp/a.sock", "--socket", "/tmp/b.sock"}, wantErr: true},
28		{name: "duplicate session", args: []string{"--session", "one", "--session", "two"}, wantErr: true},
29		{name: "duplicate columns", args: []string{"--cols", "80", "--cols", "100"}, wantErr: true},
30		{name: "duplicate rows", args: []string{"--rows", "24", "--rows", "30"}, wantErr: true},
31		{name: "duplicate remote", args: []string{"--remote", "one.example", "--remote", "two.example"}, wantErr: true},
32		{name: "missing socket", args: []string{"--socket"}, wantErr: true},
33		{name: "missing session", args: []string{"--session"}, wantErr: true},
34		{name: "missing columns", args: []string{"--cols"}, wantErr: true},
35		{name: "missing rows", args: []string{"--rows"}, wantErr: true},
36		{name: "missing remote", args: []string{"--remote"}, wantErr: true},
37		// --launch-config is a clean break: the option no longer exists and is
38		// refused as unknown, before any terminal, broker, or filesystem access.
39		{name: "removed launch config", args: []string{"--launch-config", "/tmp/config.json"}, wantErr: true},
40		{name: "removed launch config without value", args: []string{"--launch-config"}, wantErr: true},
41		{name: "removed launch config after session", args: []string{"--session", "work", "--launch-config", "/tmp/config.json"}, wantErr: true},
42		{name: "unknown", args: []string{"--nope"}, wantErr: true},
43		{name: "positional", args: []string{"work"}, wantErr: true},
44		{name: "bad geometry", args: []string{"--cols", "0"}, wantErr: true},
45		{name: "bad remote", args: []string{"--remote", "bad host"}, wantErr: true},
46	}
47	for _, tt := range tests {
48		t.Run(tt.name, func(t *testing.T) {
49			got, err := parseUIDriverArgs(tt.args)
50			if tt.wantErr {
51				require.Error(t, err)
52				return
53			}
54			require.NoError(t, err)
55			require.Equal(t, tt.want, got)
56			if tt.wantSocket {
57				require.NotEmpty(t, got.socket)
58			}
59		})
60	}
61}
62
63// TestLaunchConfigOptionIsAnUnknownFlagWithUsageExit pins the clean break: the
64// removed option is refused by argument parsing alone, with the usage exit code,
65// before any terminal, broker, or filesystem interaction.
66func TestLaunchConfigOptionIsAnUnknownFlagWithUsageExit(t *testing.T) {
67	command, err := parseArgs([]string{"--ui-driver", "--launch-config", "/tmp/config.json"})
68	require.Error(t, err)
69	require.Zero(t, command.kind)
70	require.Equal(t, 2, ExitCode(err), "an unknown option is a usage error")
71	require.ErrorContains(t, err, "unknown `--ui-driver` option")
72	require.ErrorContains(t, err, "--launch-config")
73}
74
75// TestUIDriverOptionsHelpAndUsage pins the usage text for the closed option set:
76// the removed option never appears, and the new one does.
77func TestUIDriverOptionsHelpAndUsage(t *testing.T) {
78	_, err := parseUIDriverArgs([]string{"--help"})
79	require.Error(t, err)
80	require.Contains(t, err.Error(), "--picker")
81	require.NotContains(t, err.Error(), "launch-config")
82}
83
84func TestParseArgsUIOptionsAreAttachOnly(t *testing.T) {
85	command, err := parseArgs([]string{"--ui-control", "new", "work"})
86	require.NoError(t, err)
87	require.Equal(t, kindAttach, command.kind)
88	require.True(t, command.ui.control)
89	require.False(t, command.ui.observe)
90	driver, err := parseArgs([]string{"--ui-driver", "--session", "work"})
91	require.NoError(t, err)
92	require.Equal(t, kindUIDriver, driver.kind)
93	require.Equal(t, "work", driver.uiDriver.session)
94	picker, err := parseArgs([]string{"--ui-driver", "--picker"})
95	require.NoError(t, err)
96	require.Equal(t, kindUIDriver, picker.kind)
97	require.True(t, picker.uiDriver.picker)
98	_, err = parseArgs([]string{"ui-driver"})
99	require.Error(t, err)
100	_, err = parseArgs([]string{"--ui-driver", "--ui-observe"})
101	require.Error(t, err)
102	trailing, err := parseArgs([]string{"attach", "work", "--ui-observe", "--ui-socket", "/tmp/ui.sock"})
103	require.NoError(t, err)
104	require.True(t, trailing.ui.observe)
105	require.Equal(t, "/tmp/ui.sock", trailing.ui.socket)
106	_, err = parseArgs([]string{"--ui-observe", "ls"})
107	require.Error(t, err)
108	_, err = parseArgs([]string{"--ui-socket", "/tmp/ui.sock"})
109	require.Error(t, err)
110}
111
112// TestInteractiveObservedCompositionUsesTheBrokerConnector pins that the
113// interactive observed path and the headless driver keep no daemon dialer, no
114// remote factory, and no launch configuration: both compose the shared broker
115// client over the process' production connector.
116func TestInteractiveObservedCompositionUsesTheBrokerConnector(t *testing.T) {
117	sources := loadAppSources(t, false)
118	body := sources["ui_driver.go"] + sources["attach.go"]
119	require.Contains(t, body, "newProductionBrokerConnector()")
120	for _, forbidden := range []string{
121		"runAttachWithDeps", "localDaemonDialer", "remoteDialerFactory", "launchConfig",
122		"configuredRemoteLaunches", "cleanupHeadlessEndpoint", "remoteLaunchAllowlistFromEnv",
123		"runAttachDeps", "launchEndpoint", "uiRemoteCleanupCommand",
124	} {
125		require.NotContains(t, body, forbidden, "the UI-driver composition must not keep %q", forbidden)
126	}
127}

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