refactor: argparse None defaults, serve flags on both parsers, upgrade alias

- Add _add_serve_flags() helper to set --host/--port/--auth/--session-ttl
  all with default=None so serve() can distinguish 'not passed' from
  'passed the default value'
- Apply _add_serve_flags() to both root parser and 'serve' subparser so
  'muxplex serve --host X --port Y' is accepted
- Consolidate upgrade/update via aliases=['update'] instead of two
  separate subparsers — help now shows 'upgrade (update)'
- Print deprecation warning to stderr when 'install-service' is used
- Update 5 existing tests to expect None instead of hardcoded defaults
- Add 4 new tests: test_main_passes_none_for_unset_flags,
  test_main_passes_explicit_host_only,
  test_main_serve_subcommand_accepts_flags,
  test_help_shows_single_upgrade_line

All 55 tests pass.
This commit is contained in:
Brian Krabach
2026-03-31 14:30:02 -07:00
parent c057ad6325
commit 03e9dc66de
2 changed files with 122 additions and 35 deletions
+45 -25
View File
@@ -645,32 +645,50 @@ def upgrade(*, force: bool = False) -> None:
doctor() doctor()
def _add_serve_flags(parser: argparse.ArgumentParser) -> None:
"""Add --host, --port, --auth, --session-ttl flags to a parser.
All default to None so serve() can distinguish 'not passed' from
'passed the default value'.
"""
parser.add_argument(
"--host",
default=None,
help="Bind host (default: from settings.json, then 127.0.0.1)",
)
parser.add_argument(
"--port",
type=int,
default=None,
help="Port (default: from settings.json, then 8088)",
)
parser.add_argument(
"--auth",
choices=["pam", "password"],
default=None,
help="Auth method: pam or password (default: from settings.json, then pam)",
)
parser.add_argument(
"--session-ttl",
type=int,
default=None,
dest="session_ttl",
help="Session TTL in seconds (default: from settings.json, then 604800; 0 = browser session)",
)
def main() -> None: def main() -> None:
"""CLI entry point.""" """CLI entry point."""
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
prog="muxplex", prog="muxplex",
description="muxplex — web-based tmux session dashboard", description="muxplex — web-based tmux session dashboard",
) )
parser.add_argument( _add_serve_flags(parser)
"--host", default="127.0.0.1", help="Bind host (default: 127.0.0.1)"
)
parser.add_argument("--port", type=int, default=8088, help="Port (default: 8088)")
parser.add_argument(
"--auth",
choices=["pam", "password"],
default="pam",
help="Authentication method: pam or password (default: pam)",
)
parser.add_argument(
"--session-ttl",
type=int,
default=604800,
dest="session_ttl",
help="Session TTL in seconds (default: 604800 = 7 days; 0 = browser session)",
)
sub = parser.add_subparsers(dest="command") sub = parser.add_subparsers(dest="command")
sub.add_parser("serve", help="Start the server (default)")
serve_parser = sub.add_parser("serve", help="Start the server (default)")
_add_serve_flags(serve_parser)
svc = sub.add_parser( svc = sub.add_parser(
"install-service", "install-service",
@@ -689,23 +707,25 @@ def main() -> None:
sub.add_parser("doctor", help="Check dependencies and system status") sub.add_parser("doctor", help="Check dependencies and system status")
upgrade_parser = sub.add_parser( upgrade_parser = sub.add_parser(
"upgrade", help="Upgrade muxplex to latest version and restart service" "upgrade",
aliases=["update"],
help="Upgrade muxplex to latest version and restart service",
) )
upgrade_parser.add_argument( upgrade_parser.add_argument(
"--force", "--force",
action="store_true", action="store_true",
help="Force reinstall even if already up to date", help="Force reinstall even if already up to date",
) )
update_parser = sub.add_parser("update", help="Alias for upgrade")
update_parser.add_argument(
"--force",
action="store_true",
help="Force reinstall even if already up to date",
)
args = parser.parse_args() args = parser.parse_args()
if args.command == "install-service": if args.command == "install-service":
import sys
print(
"Warning: 'install-service' is deprecated and will be removed in a future version.",
file=sys.stderr,
)
install_service(system=args.system) install_service(system=args.system)
elif args.command == "show-password": elif args.command == "show-password":
show_password() show_password()
+77 -10
View File
@@ -14,61 +14,61 @@ def test_cli_module_importable():
def test_main_calls_serve_by_default(): def test_main_calls_serve_by_default():
"""Calling main() with no args must invoke serve() with new defaults.""" """Calling main() with no args must invoke serve() with None defaults (settings layer resolves)."""
from muxplex.cli import main from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve: with patch("muxplex.cli.serve") as mock_serve:
with patch("sys.argv", ["muxplex"]): with patch("sys.argv", ["muxplex"]):
main() main()
mock_serve.assert_called_once_with( mock_serve.assert_called_once_with(
host="127.0.0.1", port=8088, auth="pam", session_ttl=604800 host=None, port=None, auth=None, session_ttl=None
) )
def test_main_passes_custom_host_and_port(): def test_main_passes_custom_host_and_port():
"""main() with --host/--port must forward them to serve().""" """main() with --host/--port must forward them to serve(); unset flags are None."""
from muxplex.cli import main from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve: with patch("muxplex.cli.serve") as mock_serve:
with patch("sys.argv", ["muxplex", "--host", "192.168.1.1", "--port", "9000"]): with patch("sys.argv", ["muxplex", "--host", "192.168.1.1", "--port", "9000"]):
main() main()
mock_serve.assert_called_once_with( mock_serve.assert_called_once_with(
host="192.168.1.1", port=9000, auth="pam", session_ttl=604800 host="192.168.1.1", port=9000, auth=None, session_ttl=None
) )
def test_main_default_host_is_localhost(): def test_main_default_host_is_localhost():
"""Default --host must be 127.0.0.1 (not 0.0.0.0).""" """Default --host must be None (settings layer resolves to 127.0.0.1)."""
from muxplex.cli import main from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve: with patch("muxplex.cli.serve") as mock_serve:
with patch("sys.argv", ["muxplex"]): with patch("sys.argv", ["muxplex"]):
main() main()
_, kwargs = mock_serve.call_args _, kwargs = mock_serve.call_args
assert kwargs["host"] == "127.0.0.1" assert kwargs["host"] is None
def test_main_passes_auth_flag(): def test_main_passes_auth_flag():
"""main() with --auth password must forward auth='password' to serve().""" """main() with --auth password must forward auth='password'; unset flags are None."""
from muxplex.cli import main from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve: with patch("muxplex.cli.serve") as mock_serve:
with patch("sys.argv", ["muxplex", "--auth", "password"]): with patch("sys.argv", ["muxplex", "--auth", "password"]):
main() main()
mock_serve.assert_called_once_with( mock_serve.assert_called_once_with(
host="127.0.0.1", port=8088, auth="password", session_ttl=604800 host=None, port=None, auth="password", session_ttl=None
) )
def test_main_passes_session_ttl_flag(): def test_main_passes_session_ttl_flag():
"""main() with --session-ttl 3600 must forward session_ttl=3600 to serve().""" """main() with --session-ttl 3600 must forward session_ttl=3600; unset flags are None."""
from muxplex.cli import main from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve: with patch("muxplex.cli.serve") as mock_serve:
with patch("sys.argv", ["muxplex", "--session-ttl", "3600"]): with patch("sys.argv", ["muxplex", "--session-ttl", "3600"]):
main() main()
mock_serve.assert_called_once_with( mock_serve.assert_called_once_with(
host="127.0.0.1", port=8088, auth="pam", session_ttl=3600 host=None, port=None, auth=None, session_ttl=3600
) )
@@ -923,3 +923,70 @@ def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
serve(session_ttl=0) serve(session_ttl=0)
assert os.environ.get("MUXPLEX_SESSION_TTL") == "0" assert os.environ.get("MUXPLEX_SESSION_TTL") == "0"
# ---------------------------------------------------------------------------
# argparse refactoring tests — None defaults, serve flags on both parsers,
# upgrade alias, install-service deprecation warning
# ---------------------------------------------------------------------------
def test_main_passes_none_for_unset_flags():
"""main() with no flags passes None for host/port/auth/session_ttl to serve()."""
from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve:
with patch("sys.argv", ["muxplex"]):
main()
mock_serve.assert_called_once_with(
host=None, port=None, auth=None, session_ttl=None
)
def test_main_passes_explicit_host_only():
"""main() with --host 10.0.0.1 passes host='10.0.0.1', others as None."""
from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve:
with patch("sys.argv", ["muxplex", "--host", "10.0.0.1"]):
main()
mock_serve.assert_called_once_with(
host="10.0.0.1", port=None, auth=None, session_ttl=None
)
def test_main_serve_subcommand_accepts_flags():
"""'muxplex serve --host 10.0.0.1 --port 9000' passes values to serve()."""
from muxplex.cli import main
with patch("muxplex.cli.serve") as mock_serve:
with patch(
"sys.argv", ["muxplex", "serve", "--host", "10.0.0.1", "--port", "9000"]
):
main()
mock_serve.assert_called_once_with(
host="10.0.0.1", port=9000, auth=None, session_ttl=None
)
def test_help_shows_single_upgrade_line():
"""Help output shows 'upgrade (update)' alias notation, not two separate subcommand entries."""
import io
from muxplex.cli import main
buf = io.StringIO()
with patch("sys.argv", ["muxplex", "--help"]):
try:
with patch("sys.stdout", buf):
main()
except SystemExit:
pass
help_text = buf.getvalue()
# With aliases=['update'], argparse renders: 'upgrade (update) description'
# With separate parsers, 'upgrade' and 'update' each have their own help lines
assert "upgrade (update)" in help_text, (
"upgrade and update must appear as alias notation 'upgrade (update)', not two separate entries. "
f"Got help text:\n{help_text}"
)