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}