From 181574586e719d7293cf8a8d9f10e80ed9975e6c Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Fri, 27 Feb 2026 17:38:33 -0800 Subject: [PATCH] Fix --help flag executing commands instead of showing help (#657) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Fix --help flag executing commands instead of showing help (#650) The --help check fell through to command dispatch when subcommandUsage returned nil for a command. Now --help always returns before dispatch, printing a generic fallback when no specific help text exists. Also adds missing subcommandUsage entries for: new-window, list-panes, list-pane-surfaces, surface-health, trigger-flash, list-panels, focus-panel, set-app-focus, ping, capabilities, identify, list-windows, current-window, refresh-surfaces, current-workspace, list-notifications, clear-notifications. Closes #650 * Document all flags and options in CLI --help output Every subcommand's help text now shows all accepted flags, options, environment variable fallbacks, and positional arguments matching the actual dispatch code. Also adds help entries for commands that were returning the generic fallback (list-workspaces, list-panes, list-pane-surfaces, surface-health, trigger-flash, list-panels, focus-panel, set-app-focus, etc.) * Show 'Unknown command' for invalid commands, add legacy alias help Unknown commands now show "Unknown command 'X'. Run 'cmux help' to see available commands." instead of the misleading "No detailed help available." Also adds help entries for legacy browser aliases (open-browser, navigate, etc.) pointing to 'cmux browser --help'. * Audit all 89 CLI commands for complete help coverage - Add missing `help` subcommandUsage entry - Expand id|ref → id|ref|index for move-workspace-to-window, close-workspace, select-workspace, rename-workspace - Document CMUX_WORKSPACE_ID/CMUX_TAB_ID/CMUX_SURFACE_ID env var defaults in tab-action, rename-workspace - Expand browser help: get (--selector, --attr, --property), find (--name, --exact, --index), network route (--abort, --body), open/open-split/new env var defaults - Remove duplicate rename-window help case (now handled by rename-workspace combined case) - Upgrade regression test to auto-extract dispatch+subcommandUsage switches and flag any commands missing help entries --- CLI/cmux.swift | 504 ++++++++++++++++-- tests/test_cli_subcommand_help_regressions.py | 147 +++++ 2 files changed, 603 insertions(+), 48 deletions(-) create mode 100644 tests/test_cli_subcommand_help_regressions.py diff --git a/CLI/cmux.swift b/CLI/cmux.swift index 75f9954d..09fd854a 100644 --- a/CLI/cmux.swift +++ b/CLI/cmux.swift @@ -696,6 +696,8 @@ struct CMUXCLI { if dispatchSubcommandHelp(command: command, commandArgs: commandArgs) { return } + print("Unknown command '\(command)'. Run 'cmux help' to see available commands.") + return } let client = SocketClient(path: socketPath) @@ -3394,10 +3396,59 @@ struct CMUXCLI { throw CLIError(message: "Unable to resolve surface ID") } - /// Return the help/usage text for a subcommand, or nil if the command has no - /// dedicated help (e.g. simple no-arg commands like `ping`). + /// Return the help/usage text for a subcommand, or nil if the command is unknown. private func subcommandUsage(_ command: String) -> String? { switch command { + case "ping": + return """ + Usage: cmux ping + + Check connectivity to the cmux socket server. + """ + case "capabilities": + return """ + Usage: cmux capabilities + + Print server capabilities as JSON. + """ + case "help": + return """ + Usage: cmux help + + Show top-level CLI usage and command list. + """ + case "identify": + return """ + Usage: cmux identify [--workspace ] [--surface ] [--no-caller] + + Print server identity and caller context details. + + Flags: + --workspace Caller workspace context (default: $CMUX_WORKSPACE_ID) + --surface Caller surface context (default: $CMUX_SURFACE_ID) + --no-caller Omit caller context from the request + """ + case "list-windows": + return """ + Usage: cmux list-windows + + List open windows. + """ + case "current-window": + return """ + Usage: cmux current-window + + Print the currently selected window ID. + """ + case "new-window": + return """ + Usage: cmux new-window + + Create a new window. + + Example: + cmux new-window + """ case "focus-window": return """ Usage: cmux focus-window --window @@ -3426,47 +3477,56 @@ struct CMUXCLI { """ case "move-workspace-to-window": return """ - Usage: cmux move-workspace-to-window --workspace --window + Usage: cmux move-workspace-to-window --workspace --window Move a workspace to a different window. Flags: - --workspace Workspace to move (required) - --window Target window (required) + --workspace Workspace to move (required) + --window Target window (required) Example: cmux move-workspace-to-window --workspace workspace:2 --window window:1 """ case "move-surface": return """ - Usage: cmux move-surface --surface [flags] + Usage: cmux move-surface [--surface | ] [flags] Move a surface to a different pane, workspace, or window. Flags: - --surface Surface to move (required) + --surface Surface to move (required unless passed positionally) --pane Target pane --workspace Target workspace --window Target window --before Place before this surface + --before-surface + Alias for --before --after Place after this surface + --after-surface + Alias for --after --index Place at this index --focus Focus the surface after moving Example: cmux move-surface --surface surface:1 --workspace workspace:2 - cmux move-surface --surface 0 --pane pane:2 --index 0 + cmux move-surface surface:1 --pane pane:2 --index 0 """ case "reorder-surface": return """ - Usage: cmux reorder-surface --surface [flags] + Usage: cmux reorder-surface [--surface | ] [flags] Reorder a surface within its pane. Flags: - --surface Surface to reorder (required) + --surface Surface to reorder (required unless passed positionally) + --workspace Workspace context --before Place before this surface + --before-surface + Alias for --before --after Place after this surface + --after-surface + Alias for --after --index Place at this index Example: @@ -3475,15 +3535,19 @@ struct CMUXCLI { """ case "reorder-workspace": return """ - Usage: cmux reorder-workspace --workspace [flags] + Usage: cmux reorder-workspace [--workspace | ] [flags] Reorder a workspace within its window. Flags: - --workspace Workspace to reorder (required) + --workspace Workspace to reorder (required unless passed positionally) --index Place at this index --before Place before this workspace + --before-workspace + Alias for --before --after Place after this workspace + --after-workspace + Alias for --after --window Window context Example: @@ -3506,7 +3570,7 @@ struct CMUXCLI { Flags: --action Action name (required if not positional) --workspace Target workspace (default: current/$CMUX_WORKSPACE_ID) - --title Title for rename + --title Title for rename (or pass trailing title text) Example: cmux workspace-action --workspace workspace:2 --action pin @@ -3529,10 +3593,10 @@ struct CMUXCLI { Flags: --action Action name (required if not positional) - --tab Target tab (accepts tab: or surface:; alias: --surface) + --tab Target tab (accepts tab: or surface:; default: $CMUX_TAB_ID, then $CMUX_SURFACE_ID, then focused tab) --surface Alias for --tab (backward compatibility) --workspace Workspace context (default: current/$CMUX_WORKSPACE_ID) - --title Title for rename + --title Title for rename (or pass trailing title text) --url Optional URL for new-browser-right Example: @@ -3562,12 +3626,25 @@ struct CMUXCLI { """ case "new-workspace": return """ - Usage: cmux new-workspace + Usage: cmux new-workspace [--command ] Create a new workspace in the current window. + Flags: + --command Send text+Enter to the new workspace after creation + Example: cmux new-workspace + cmux new-workspace --command "npm test" + """ + case "list-workspaces": + return """ + Usage: cmux list-workspaces + + List workspaces in the current window. + + Example: + cmux list-workspaces """ case "new-split": return """ @@ -3584,6 +3661,33 @@ struct CMUXCLI { cmux new-split right cmux new-split down --workspace workspace:1 """ + case "list-panes": + return """ + Usage: cmux list-panes [--workspace ] + + List panes in a workspace. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux list-panes + cmux list-panes --workspace workspace:2 + """ + case "list-pane-surfaces": + return """ + Usage: cmux list-pane-surfaces [--workspace ] [--pane ] + + List surfaces in a pane. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + --pane Restrict to a specific pane (default: focused pane) + + Example: + cmux list-pane-surfaces + cmux list-pane-surfaces --workspace workspace:2 --pane pane:1 + """ case "tree": return """ Usage: cmux tree [flags] @@ -3612,16 +3716,17 @@ struct CMUXCLI { """ case "focus-pane": return """ - Usage: cmux focus-pane --pane [flags] + Usage: cmux focus-pane [--pane | ] [flags] Focus the specified pane. Flags: - --pane Pane to focus (required) + --pane Pane to focus (required unless passed positionally) --workspace Workspace context (default: $CMUX_WORKSPACE_ID) Example: cmux focus-pane --pane pane:2 + cmux focus-pane pane:1 cmux focus-pane --pane pane:1 --workspace workspace:2 """ case "new-pane": @@ -3685,26 +3790,87 @@ struct CMUXCLI { cmux drag-surface-to-split --surface surface:1 right cmux drag-surface-to-split --panel surface:2 down """ + case "refresh-surfaces": + return """ + Usage: cmux refresh-surfaces + + Refresh surface snapshots for the focused workspace. + """ + case "surface-health": + return """ + Usage: cmux surface-health [--workspace ] + + List health details for surfaces in a workspace. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux surface-health + cmux surface-health --workspace workspace:2 + """ + case "trigger-flash": + return """ + Usage: cmux trigger-flash [--workspace ] [--surface ] [--panel ] + + Trigger the unread flash indicator for a surface. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + --surface Target surface (default: $CMUX_SURFACE_ID) + --panel Alias for --surface + + Example: + cmux trigger-flash + cmux trigger-flash --workspace workspace:2 --surface surface:3 + """ + case "list-panels": + return """ + Usage: cmux list-panels [--workspace ] + + List surfaces (panels) in a workspace. + + Flags: + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux list-panels + cmux list-panels --workspace workspace:2 + """ + case "focus-panel": + return """ + Usage: cmux focus-panel --panel [--workspace ] + + Focus a specific panel (surface). + + Flags: + --panel Panel/surface to focus (required) + --workspace Workspace context (default: $CMUX_WORKSPACE_ID) + + Example: + cmux focus-panel --panel surface:2 + cmux focus-panel --panel surface:5 --workspace workspace:2 + """ case "close-workspace": return """ - Usage: cmux close-workspace --workspace + Usage: cmux close-workspace --workspace Close the specified workspace. Flags: - --workspace Workspace to close (required) + --workspace Workspace to close (required) Example: cmux close-workspace --workspace workspace:2 """ case "select-workspace": return """ - Usage: cmux select-workspace --workspace + Usage: cmux select-workspace --workspace Select (switch to) the specified workspace. Flags: - --workspace Workspace to select (required) + --workspace Workspace to select (required) Example: cmux select-workspace --workspace workspace:2 @@ -3712,51 +3878,210 @@ struct CMUXCLI { """ case "rename-workspace", "rename-window": return """ - Usage: cmux rename-workspace [--workspace ] [--] + Usage: cmux rename-workspace [--workspace <id|ref|index>] [--] <title> Rename a workspace. Defaults to the current workspace. tmux-compatible alias: rename-window Flags: - --workspace <id|ref> Workspace to rename (default: current workspace) + --workspace <id|ref|index> Workspace to rename (default: current/$CMUX_WORKSPACE_ID) Example: cmux rename-workspace "backend logs" cmux rename-window --workspace workspace:2 "agent run" """ + case "current-workspace": + return """ + Usage: cmux current-workspace + + Print the currently selected workspace ID. + """ case "capture-pane": return """ Usage: cmux capture-pane [--workspace <id|ref>] [--surface <id|ref>] [--scrollback] [--lines <n>] tmux-compatible alias for reading terminal text from a pane. + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: $CMUX_SURFACE_ID) + --scrollback Include scrollback + --lines <n> Return only the last N lines (implies --scrollback) + Example: cmux capture-pane --workspace workspace:2 --surface surface:1 --scrollback --lines 200 """ case "resize-pane": return """ - Usage: cmux resize-pane --pane <id|ref> [--workspace <id|ref>] (-L|-R|-U|-D) [--amount <n>] + Usage: cmux resize-pane [--pane <id|ref>] [--workspace <id|ref>] [-L|-R|-U|-D] [--amount <n>] tmux-compatible pane resize command. - Note: currently returns not_supported until programmable divider resize is implemented. + + Flags: + --pane <id|ref> Pane to resize (default: focused pane) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + -L|-R|-U|-D Direction (default: -R) + --amount <n> Resize amount (default: 1) """ case "pipe-pane": return """ - Usage: cmux pipe-pane --command <shell-command> [--workspace <id|ref>] [--surface <id|ref>] + Usage: cmux pipe-pane [--workspace <id|ref>] [--surface <id|ref>] [--command <shell-command> | <shell-command>] Capture pane text and pipe it to a shell command via stdin. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + --command <command> Shell command to run (or pass as trailing text) """ case "wait-for": return """ Usage: cmux wait-for [-S|--signal] <name> [--timeout <seconds>] Wait for or signal a named synchronization token. - """ - case "swap-pane", "break-pane", "join-pane", "next-window", "previous-window", "last-window", "last-pane", "find-window", "clear-history", "set-hook", "popup", "bind-key", "unbind-key", "copy-mode", "set-buffer", "paste-buffer", "list-buffers", "respawn-pane", "display-message": - return """ - Usage: cmux \(command) --help - tmux compatibility command. See `cmux --help` for exact syntax. + Flags: + -S, --signal Signal the token instead of waiting + --timeout <seconds> Wait timeout (default: 30) + """ + case "swap-pane": + return """ + Usage: cmux swap-pane --pane <id|ref> --target-pane <id|ref> [--workspace <id|ref>] + + Swap two panes. + + Flags: + --pane <id|ref> Source pane (required) + --target-pane <id|ref> Target pane (required) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + """ + case "break-pane": + return """ + Usage: cmux break-pane [--workspace <id|ref>] [--pane <id|ref>] [--surface <id|ref>] [--no-focus] + + Move a pane/surface out into its own pane context. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --pane <id|ref> Source pane + --surface <id|ref> Source surface + --no-focus Do not focus the result + """ + case "join-pane": + return """ + Usage: cmux join-pane --target-pane <id|ref> [--workspace <id|ref>] [--pane <id|ref>] [--surface <id|ref>] [--no-focus] + + Join a pane/surface into another pane. + + Flags: + --target-pane <id|ref> Target pane (required) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --pane <id|ref> Source pane + --surface <id|ref> Source surface + --no-focus Do not focus the result + """ + case "next-window", "previous-window", "last-window": + return """ + Usage: cmux \(command) + + Switch workspace selection (next/previous/last) in the current window. + """ + case "last-pane": + return """ + Usage: cmux last-pane [--workspace <id|ref>] + + Focus the previously focused pane in a workspace. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + """ + case "find-window": + return """ + Usage: cmux find-window [--content] [--select] [query] + + Find workspaces by title (and optionally terminal content). + + Flags: + --content Search terminal content in addition to workspace titles + --select Select the first match + """ + case "clear-history": + return """ + Usage: cmux clear-history [--workspace <id|ref>] [--surface <id|ref>] + + Clear terminal scrollback history. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + """ + case "set-hook": + return """ + Usage: cmux set-hook [--list] [--unset <event>] | <event> <command> + + Manage tmux-compat hook definitions. + + Flags: + --list List configured hooks + --unset <event> Remove a hook by event name + """ + case "popup": + return """ + Usage: cmux popup + + tmux compatibility placeholder. This command is currently not supported. + """ + case "bind-key", "unbind-key", "copy-mode": + return """ + Usage: cmux \(command) + + tmux compatibility placeholder. This command is currently not supported. + """ + case "set-buffer": + return """ + Usage: cmux set-buffer [--name <name>] [--] <text> + + Save text into a named tmux-compat buffer. + + Flags: + --name <name> Buffer name (default: default) + """ + case "paste-buffer": + return """ + Usage: cmux paste-buffer [--name <name>] [--workspace <id|ref>] [--surface <id|ref>] + + Paste a named tmux-compat buffer into a surface. + + Flags: + --name <name> Buffer name (default: default) + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + """ + case "list-buffers": + return """ + Usage: cmux list-buffers + + List tmux-compat buffers. + """ + case "respawn-pane": + return """ + Usage: cmux respawn-pane [--workspace <id|ref>] [--surface <id|ref>] [--command <cmd> | <cmd>] + + Send a command (or default shell restart command) to a surface. + + Flags: + --workspace <id|ref> Workspace context (default: $CMUX_WORKSPACE_ID) + --surface <id|ref> Surface context (default: focused surface) + --command <cmd> Command text (or pass trailing command text) + """ + case "display-message": + return """ + Usage: cmux display-message [-p|--print] <text> + + Print text (or show it via notification bridge in parity mode). + + Flags: + -p, --print Print to stdout only """ case "read-screen": return """ @@ -3846,6 +4171,18 @@ struct CMUXCLI { cmux notify --title "Build done" --body "All tests passed" cmux notify --title "Error" --subtitle "test.swift" --body "Line 42: syntax error" """ + case "list-notifications": + return """ + Usage: cmux list-notifications + + List queued notifications. + """ + case "clear-notifications": + return """ + Usage: cmux clear-notifications + + Clear all queued notifications. + """ case "set-status": return """ Usage: cmux set-status <key> <value> [flags] @@ -3970,16 +4307,35 @@ struct CMUXCLI { cmux sidebar-state cmux sidebar-state --workspace workspace:2 """ + case "set-app-focus": + return """ + Usage: cmux set-app-focus <active|inactive|clear> + + Override app focus state for notification routing tests. + + Example: + cmux set-app-focus inactive + cmux set-app-focus clear + """ + case "simulate-app-active": + return """ + Usage: cmux simulate-app-active + + Trigger the app-active handler used by notification focus tests. + """ case "claude-hook": return """ - Usage: cmux claude-hook <session-start|stop|notification> [flags] + Usage: cmux claude-hook <session-start|active|stop|idle|notification|notify> [flags] Hook for Claude Code integration. Reads JSON from stdin. Subcommands: session-start Signal that a Claude session has started + active Alias for session-start stop Signal that a Claude session has stopped + idle Alias for stop notification Forward a Claude notification + notify Alias for notification Flags: --workspace <id|ref> Target workspace (default: $CMUX_WORKSPACE_ID) @@ -3994,29 +4350,81 @@ struct CMUXCLI { Usage: cmux browser [--surface <id|ref|index> | <surface>] <subcommand> [args] Browser automation commands. Most subcommands require a surface handle. + A surface can be passed as `--surface <handle>` or as the first positional token. + `open`/`open-split`/`new`/`identify` can run without an explicit surface. Subcommands: - open [url] Create browser split (or navigate if surface given) - open-split [url] Create browser in a new split - goto|navigate <url> Navigate to URL [--snapshot-after] - back|forward|reload History navigation [--snapshot-after] - url|get-url Get current URL - snapshot Get DOM snapshot [--interactive|-i] [--cursor] [--compact] [--max-depth <n>] [--selector <css>] - eval <script> Evaluate JavaScript - wait Wait for condition [--selector] [--text] [--url-contains] [--timeout-ms] - click|dblclick|hover <sel> Mouse actions [--snapshot-after] - type <selector> <text> Type text [--snapshot-after] - fill <selector> [text] Fill input [--snapshot-after] - press|keydown|keyup <key> Keyboard actions [--snapshot-after] - get <property> [selector] Get page properties (url|title|text|html|value|attr|count|box|styles) - find <strategy> <query> Find elements (role|text|label|placeholder|testid|first|last|nth) - identify Identify browser surface + open|open-split|new [url] [--workspace <id|ref|index>] [--window <id|ref|index>] + open/open-split/new default to $CMUX_WORKSPACE_ID when --workspace is omitted and --window is not set + goto|navigate <url> [--snapshot-after] + back|forward|reload [--snapshot-after] + url|get-url + focus-webview | is-webview-focused + snapshot [--interactive|-i] [--cursor] [--compact] [--max-depth <n>] [--selector <css>] + eval [--script <js> | <js>] + wait [--selector <css>] [--text <text>] [--url-contains <text>|--url <text>] [--load-state <interactive|complete>] [--function <js>] [--timeout-ms <ms>|--timeout <seconds>] + click|dblclick|hover|focus|check|uncheck|scroll-into-view [--selector <css> | <css>] [--snapshot-after] + type|fill [--selector <css> | <css>] [--text <text> | <text>] [--snapshot-after] + press|key|keydown|keyup [--key <key> | <key>] [--snapshot-after] + select [--selector <css> | <css>] [--value <value> | <value>] [--snapshot-after] + scroll [--selector <css>] [--dx <n>] [--dy <n>] [--snapshot-after] + screenshot [--out <path>] + get <url|title|text|html|value|attr|count|box|styles> [...] + text|html|value|count|box|styles|attr: [--selector <css> | <css>] + attr: [--attr <name> | <name>] + styles: [--property <name>] + is <visible|enabled|checked> [--selector <css> | <css>] + find <role|text|label|placeholder|alt|title|testid|first|last|nth> [...] + role: [--name <text>] [--exact] <role> + text|label|placeholder|alt|title|testid: [--exact] <text> + first|last: [--selector <css> | <css>] + nth: [--index <n> | <n>] [--selector <css> | <css>] + frame <main|selector> [--selector <css>] + dialog <accept|dismiss> [text] + download [wait] [--path <path>] [--timeout-ms <ms>|--timeout <seconds>] + cookies <get|set|clear> [--name <name>] [--value <value>] [--url <url>] [--domain <domain>] [--path <path>] [--expires <unix>] [--secure] [--all] + storage <local|session> <get|set|clear> [...] + tab <new|list|switch|close|<index>> [...] + console <list|clear> + errors <list|clear> + highlight [--selector <css> | <css>] + state <save|load> <path> + addinitscript|addscript [--script <js> | <js>] + addstyle [--css <css> | <css>] + viewport <width> <height> + geolocation|geo <latitude> <longitude> + offline <true|false> + trace <start|stop> [path] + network <route|unroute|requests> ... + route <pattern> [--abort] [--body <text>] + unroute <pattern> + screencast <start|stop> + input <mouse|keyboard|touch> [args...] + input_mouse | input_keyboard | input_touch + identify [--surface <id|ref|index>] Example: cmux browser open https://example.com cmux browser surface:1 navigate https://google.com cmux browser --surface surface:1 snapshot --interactive """ + // Legacy browser aliases — point users to `cmux browser --help` + case "open-browser": + return "Legacy alias for 'cmux browser open'. Run 'cmux browser --help' for details." + case "navigate": + return "Legacy alias for 'cmux browser navigate'. Run 'cmux browser --help' for details." + case "browser-back": + return "Legacy alias for 'cmux browser back'. Run 'cmux browser --help' for details." + case "browser-forward": + return "Legacy alias for 'cmux browser forward'. Run 'cmux browser --help' for details." + case "browser-reload": + return "Legacy alias for 'cmux browser reload'. Run 'cmux browser --help' for details." + case "get-url": + return "Legacy alias for 'cmux browser get-url'. Run 'cmux browser --help' for details." + case "focus-webview": + return "Legacy alias for 'cmux browser focus-webview'. Run 'cmux browser --help' for details." + case "is-webview-focused": + return "Legacy alias for 'cmux browser is-webview-focused'. Run 'cmux browser --help' for details." default: return nil } diff --git a/tests/test_cli_subcommand_help_regressions.py b/tests/test_cli_subcommand_help_regressions.py new file mode 100644 index 00000000..1d2b031c --- /dev/null +++ b/tests/test_cli_subcommand_help_regressions.py @@ -0,0 +1,147 @@ +#!/usr/bin/env python3 +"""Regression tests for CLI subcommand help coverage and accuracy.""" + +from __future__ import annotations + +import re +import subprocess +from pathlib import Path + + +def get_repo_root() -> Path: + result = subprocess.run( + ["git", "rev-parse", "--show-toplevel"], + capture_output=True, + text=True, + check=False, + ) + if result.returncode == 0: + return Path(result.stdout.strip()) + return Path.cwd() + + +def require(content: str, needle: str, message: str, failures: list[str]) -> None: + if needle not in content: + failures.append(message) + + +def extract_switch_commands(content: str, start_index: int = 0) -> tuple[set[str], int]: + marker = "switch command {" + marker_index = content.find(marker, start_index) + if marker_index == -1: + return set(), -1 + + open_brace = content.find("{", marker_index) + if open_brace == -1: + return set(), -1 + + depth = 1 + cursor = open_brace + 1 + while cursor < len(content) and depth > 0: + char = content[cursor] + if char == "{": + depth += 1 + elif char == "}": + depth -= 1 + cursor += 1 + + block = content[open_brace + 1:cursor - 1] + commands: set[str] = set() + collecting_case = False + case_lines: list[str] = [] + + for line in block.splitlines(): + stripped = line.strip() + if stripped.startswith("case "): + collecting_case = True + case_lines = [line] + elif collecting_case: + case_lines.append(line) + + if collecting_case and ":" in line: + case_text = "\n".join(case_lines) + commands.update(re.findall(r'"([^"]+)"', case_text)) + collecting_case = False + case_lines = [] + + return commands, cursor + + +def main() -> int: + repo_root = get_repo_root() + cli_path = repo_root / "CLI" / "cmux.swift" + if not cli_path.exists(): + print(f"FAIL: missing expected file: {cli_path}") + return 1 + + content = cli_path.read_text(encoding="utf-8") + failures: list[str] = [] + + require( + content, + 'if commandArgs.contains("--help") || commandArgs.contains("-h") {', + "Subcommand help pre-dispatch gate is missing", + failures, + ) + require( + content, + 'if dispatchSubcommandHelp(command: command, commandArgs: commandArgs) {', + "Subcommand help dispatch call is missing", + failures, + ) + require( + content, + "print(\"Unknown command '\\(command)'. Run 'cmux help' to see available commands.\")", + "Subcommand help fallback unknown-command line is missing", + failures, + ) + require( + content, + "print(\"Unknown command '\\(command)'. Run 'cmux help' to see available commands.\")\n return", + "Subcommand help fallback must return before command execution", + failures, + ) + + dispatch_commands, next_index = extract_switch_commands(content, 0) + subcommand_usage_commands, _ = extract_switch_commands(content, next_index if next_index != -1 else 0) + if not dispatch_commands: + failures.append("Failed to parse main dispatch switch command list") + if not subcommand_usage_commands: + failures.append("Failed to parse subcommandUsage switch command list") + + missing_help_entries = sorted(dispatch_commands - subcommand_usage_commands) + if missing_help_entries: + failures.append( + "Missing subcommandUsage entries for dispatch command(s): " + + ", ".join(missing_help_entries) + ) + + # Regression checks for concrete help text that previously drifted from dispatch logic. + for needle, message in [ + ('case "help":', "Missing subcommandUsage entry for help"), + ("Usage: cmux help", "help subcommand usage text is missing"), + ("Usage: cmux move-workspace-to-window --workspace <id|ref|index> --window <id|ref|index>", "move-workspace-to-window help must document index handles"), + ("--tab <id|ref|index> Target tab (accepts tab:<n> or surface:<n>; default: $CMUX_TAB_ID, then $CMUX_SURFACE_ID, then focused tab)", "tab-action help must document CMUX_TAB_ID/CMUX_SURFACE_ID fallback"), + ("--workspace <id|ref|index> Workspace to rename (default: current/$CMUX_WORKSPACE_ID)", "rename-workspace help must document CMUX_WORKSPACE_ID fallback"), + ("text|html|value|count|box|styles|attr: [--selector <css> | <css>]", "browser get help must document --selector"), + ("attr: [--attr <name> | <name>]", "browser get attr help must document --attr"), + ("styles: [--property <name>]", "browser get styles help must document --property"), + ("role: [--name <text>] [--exact] <role>", "browser find role help must document --name/--exact"), + ("text|label|placeholder|alt|title|testid: [--exact] <text>", "browser find text-like help must document --exact"), + ("nth: [--index <n> | <n>] [--selector <css> | <css>]", "browser find nth help must document --index/--selector"), + ("route <pattern> [--abort] [--body <text>]", "browser network route help must document --abort/--body"), + ]: + require(content, needle, message, failures) + + if failures: + print("FAIL: CLI subcommand help regression(s) detected") + for failure in failures: + print(f"- {failure}") + return 1 + + print("PASS: CLI subcommand help coverage and flag/env documentation are present") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())