refactor: remove install-service entirely — use muxplex service install

No backward compatibility alias. The command is simply gone.
Tests, README, and CLI all cleaned up. Historical plan docs
left as-is for reference.
This commit is contained in:
Brian Krabach
2026-04-01 02:02:57 -07:00
parent e24a5c3834
commit 14b8a33943
4 changed files with 1 additions and 181 deletions
+1 -2
View File
@@ -104,7 +104,6 @@ All serve options read from `~/.config/muxplex/settings.json` by default. CLI fl
| `muxplex upgrade` | Upgrade to latest version and restart service | | `muxplex upgrade` | Upgrade to latest version and restart service |
| `muxplex show-password` | Show the current muxplex password | | `muxplex show-password` | Show the current muxplex password |
| `muxplex reset-secret` | Regenerate signing secret (invalidates sessions) | | `muxplex reset-secret` | Regenerate signing secret (invalidates sessions) |
| `muxplex install-service` | *(deprecated — use `muxplex service install`)* |
### Service management ### Service management
@@ -196,7 +195,7 @@ muxplex/
└── muxplex/ # Python package └── muxplex/ # Python package
├── __init__.py ├── __init__.py
├── __main__.py # `python -m muxplex` entry point ├── __main__.py # `python -m muxplex` entry point
├── cli.py # CLI argument parsing and `install-service` subcommand ├── cli.py # CLI entry point and subcommand dispatch
├── main.py # FastAPI app: session API, bell hooks, WebSocket proxy to ttyd, static frontend ├── main.py # FastAPI app: session API, bell hooks, WebSocket proxy to ttyd, static frontend
├── sessions.py # tmux session discovery and snapshot capture ├── sessions.py # tmux session discovery and snapshot capture
├── bells.py # Bell/activity notification tracking ├── bells.py # Bell/activity notification tracking
-12
View File
@@ -579,18 +579,6 @@ def main() -> None:
help="Force reinstall even if already up to date", help="Force reinstall even if already up to date",
) )
# Intercept deprecated 'install-service' before argparse sees it
if len(sys.argv) > 1 and sys.argv[1] == "install-service":
print(
"\u26a0 'muxplex install-service' is deprecated."
" Use 'muxplex service install' instead.",
file=sys.stderr,
)
from muxplex.service import service_install # noqa: PLC0415
service_install()
return
args = parser.parse_args() args = parser.parse_args()
if args.command == "show-password": if args.command == "show-password":
-147
View File
@@ -7,12 +7,10 @@ import stat
from pathlib import Path from pathlib import Path
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
def test_cli_module_importable(): def test_cli_module_importable():
"""muxplex.cli must be importable.""" """muxplex.cli must be importable."""
from muxplex.cli import main # noqa: F401 from muxplex.cli import main # noqa: F401
def test_main_calls_serve_by_default(): def test_main_calls_serve_by_default():
"""Calling main() with no args must invoke serve() with None defaults (settings layer resolves).""" """Calling main() with no args must invoke serve() with None defaults (settings layer resolves)."""
from muxplex.cli import main from muxplex.cli import main
@@ -24,7 +22,6 @@ def test_main_calls_serve_by_default():
host=None, port=None, auth=None, session_ttl=None 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(); unset flags are None.""" """main() with --host/--port must forward them to serve(); unset flags are None."""
from muxplex.cli import main from muxplex.cli import main
@@ -36,7 +33,6 @@ def test_main_passes_custom_host_and_port():
host="192.168.1.1", port=9000, auth=None, session_ttl=None 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 None (settings layer resolves to 127.0.0.1).""" """Default --host must be None (settings layer resolves to 127.0.0.1)."""
from muxplex.cli import main from muxplex.cli import main
@@ -47,7 +43,6 @@ def test_main_default_host_is_localhost():
_, kwargs = mock_serve.call_args _, kwargs = mock_serve.call_args
assert kwargs["host"] is None 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'; unset flags are None.""" """main() with --auth password must forward auth='password'; unset flags are None."""
from muxplex.cli import main from muxplex.cli import main
@@ -59,7 +54,6 @@ def test_main_passes_auth_flag():
host=None, port=None, auth="password", session_ttl=None 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; unset flags are None.""" """main() with --session-ttl 3600 must forward session_ttl=3600; unset flags are None."""
from muxplex.cli import main from muxplex.cli import main
@@ -71,27 +65,6 @@ def test_main_passes_session_ttl_flag():
host=None, port=None, auth=None, session_ttl=3600 host=None, port=None, auth=None, session_ttl=3600
) )
def test_main_install_service_subcommand():
"""main() with 'install-service' must invoke service_install() (new module)."""
from muxplex.cli import main
with patch("muxplex.service.service_install") as mock_install:
with patch("sys.argv", ["muxplex", "install-service"]):
main()
mock_install.assert_called_once()
def test_main_install_service_system_flag():
"""main() with 'install-service --system' still calls service_install() (--system ignored; new module auto-detects)."""
from muxplex.cli import main
with patch("muxplex.service.service_install") as mock_install:
with patch("sys.argv", ["muxplex", "install-service", "--system"]):
main()
mock_install.assert_called_once()
def test_show_password_prints_password_from_file(tmp_path, monkeypatch, capsys): def test_show_password_prints_password_from_file(tmp_path, monkeypatch, capsys):
"""show_password() prints the password when MUXPLEX_AUTH=password and file exists.""" """show_password() prints the password when MUXPLEX_AUTH=password and file exists."""
from muxplex.cli import show_password from muxplex.cli import show_password
@@ -111,7 +84,6 @@ def test_show_password_prints_password_from_file(tmp_path, monkeypatch, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "my-test-password" in captured.out assert "my-test-password" in captured.out
def test_show_password_no_file(tmp_path, monkeypatch, capsys): def test_show_password_no_file(tmp_path, monkeypatch, capsys):
"""show_password() tells user no file found when in password mode with no file.""" """show_password() tells user no file found when in password mode with no file."""
from muxplex.cli import show_password from muxplex.cli import show_password
@@ -129,7 +101,6 @@ def test_show_password_no_file(tmp_path, monkeypatch, capsys):
output_lower = captured.out.lower() output_lower = captured.out.lower()
assert "no password" in output_lower or "not found" in output_lower assert "no password" in output_lower or "not found" in output_lower
def test_show_password_pam_mode(monkeypatch, capsys): def test_show_password_pam_mode(monkeypatch, capsys):
"""show_password() reports PAM mode when pam_available() is True and not password mode.""" """show_password() reports PAM mode when pam_available() is True and not password mode."""
from muxplex.cli import show_password from muxplex.cli import show_password
@@ -142,7 +113,6 @@ def test_show_password_pam_mode(monkeypatch, capsys):
captured = capsys.readouterr() captured = capsys.readouterr()
assert "pam" in captured.out.lower() assert "pam" in captured.out.lower()
def test_reset_secret_writes_new_secret(tmp_path, monkeypatch): def test_reset_secret_writes_new_secret(tmp_path, monkeypatch):
"""reset_secret() writes a new secret file with content longer than 20 chars.""" """reset_secret() writes a new secret file with content longer than 20 chars."""
from muxplex.cli import reset_secret from muxplex.cli import reset_secret
@@ -158,7 +128,6 @@ def test_reset_secret_writes_new_secret(tmp_path, monkeypatch):
content = secret_path.read_text().strip() content = secret_path.read_text().strip()
assert len(content) > 20, f"Secret must be longer than 20 chars, got {len(content)}" assert len(content) > 20, f"Secret must be longer than 20 chars, got {len(content)}"
def test_reset_secret_sets_0600_permissions(tmp_path, monkeypatch): def test_reset_secret_sets_0600_permissions(tmp_path, monkeypatch):
"""reset_secret() sets file permissions to 0o600.""" """reset_secret() sets file permissions to 0o600."""
from muxplex.cli import reset_secret from muxplex.cli import reset_secret
@@ -173,7 +142,6 @@ def test_reset_secret_sets_0600_permissions(tmp_path, monkeypatch):
file_mode = stat.S_IMODE(secret_path.stat().st_mode) file_mode = stat.S_IMODE(secret_path.stat().st_mode)
assert file_mode == 0o600, f"Expected 0o600, got {oct(file_mode)}" assert file_mode == 0o600, f"Expected 0o600, got {oct(file_mode)}"
def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys): def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys):
"""reset_secret() prints a warning that sessions are now invalid.""" """reset_secret() prints a warning that sessions are now invalid."""
from muxplex.cli import reset_secret from muxplex.cli import reset_secret
@@ -190,23 +158,6 @@ def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys):
f"Expected 'invalid' or 'warning' in output, got: {captured.out!r}" f"Expected 'invalid' or 'warning' in output, got: {captured.out!r}"
) )
def test_install_service_deprecated_alias_prints_warning(capsys):
"""install-service must print deprecation warning and forward to service install."""
from muxplex.cli import main
with (
patch("sys.argv", ["muxplex", "install-service"]),
patch("muxplex.service.service_install") as mock_install,
):
main()
captured = capsys.readouterr()
assert "deprecated" in captured.err.lower()
assert "service install" in captured.err.lower()
mock_install.assert_called_once()
def test_check_dependencies_exits_when_ttyd_missing(monkeypatch): def test_check_dependencies_exits_when_ttyd_missing(monkeypatch):
"""_check_dependencies() must sys.exit(1) when ttyd is not in PATH.""" """_check_dependencies() must sys.exit(1) when ttyd is not in PATH."""
import shutil import shutil
@@ -226,7 +177,6 @@ def test_check_dependencies_exits_when_ttyd_missing(monkeypatch):
_check_dependencies() _check_dependencies()
assert exc_info.value.code == 1 assert exc_info.value.code == 1
def test_check_dependencies_exits_when_tmux_missing(monkeypatch): def test_check_dependencies_exits_when_tmux_missing(monkeypatch):
"""_check_dependencies() must sys.exit(1) when tmux is not in PATH.""" """_check_dependencies() must sys.exit(1) when tmux is not in PATH."""
import shutil import shutil
@@ -246,7 +196,6 @@ def test_check_dependencies_exits_when_tmux_missing(monkeypatch):
_check_dependencies() _check_dependencies()
assert exc_info.value.code == 1 assert exc_info.value.code == 1
def test_check_dependencies_passes_when_all_present(monkeypatch): def test_check_dependencies_passes_when_all_present(monkeypatch):
"""_check_dependencies() must not raise when both tmux and ttyd are found.""" """_check_dependencies() must not raise when both tmux and ttyd are found."""
import shutil import shutil
@@ -257,7 +206,6 @@ def test_check_dependencies_passes_when_all_present(monkeypatch):
# Should not raise # Should not raise
_check_dependencies() _check_dependencies()
def test_main_check_dependencies_called_for_serve(monkeypatch): def test_main_check_dependencies_called_for_serve(monkeypatch):
"""main() must call _check_dependencies() when subcommand is serve.""" """main() must call _check_dependencies() when subcommand is serve."""
from muxplex.cli import main from muxplex.cli import main
@@ -271,21 +219,6 @@ def test_main_check_dependencies_called_for_serve(monkeypatch):
assert len(calls) == 1, "_check_dependencies must be called once for serve" assert len(calls) == 1, "_check_dependencies must be called once for serve"
def test_main_check_dependencies_not_called_for_install_service(monkeypatch):
"""main() must NOT call _check_dependencies() for install-service subcommand."""
from muxplex.cli import main
calls = []
monkeypatch.setattr("muxplex.cli._check_dependencies", lambda: calls.append(True))
with patch("muxplex.service.service_install"):
with patch("sys.argv", ["muxplex", "install-service"]):
main()
assert len(calls) == 0, "_check_dependencies must NOT be called for install-service"
def test_dunder_main_calls_main(): def test_dunder_main_calls_main():
"""python -m muxplex must call cli.main().""" """python -m muxplex must call cli.main()."""
import importlib.util import importlib.util
@@ -298,12 +231,10 @@ def test_dunder_main_calls_main():
exec(Path(spec.origin).read_text()) # noqa: S102 exec(Path(spec.origin).read_text()) # noqa: S102
mock_main.assert_called_once() mock_main.assert_called_once()
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# doctor() tests # doctor() tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_doctor_shows_python_version(capsys): def test_doctor_shows_python_version(capsys):
"""doctor must show Python version.""" """doctor must show Python version."""
from muxplex.cli import doctor from muxplex.cli import doctor
@@ -312,7 +243,6 @@ def test_doctor_shows_python_version(capsys):
out = capsys.readouterr().out out = capsys.readouterr().out
assert "Python" in out assert "Python" in out
def test_doctor_checks_tmux(capsys, monkeypatch): def test_doctor_checks_tmux(capsys, monkeypatch):
"""doctor must check for tmux.""" """doctor must check for tmux."""
import subprocess import subprocess
@@ -333,7 +263,6 @@ def test_doctor_checks_tmux(capsys, monkeypatch):
out = capsys.readouterr().out out = capsys.readouterr().out
assert "tmux" in out assert "tmux" in out
def test_doctor_reports_missing_ttyd(capsys, monkeypatch): def test_doctor_reports_missing_ttyd(capsys, monkeypatch):
"""doctor must report when ttyd is missing.""" """doctor must report when ttyd is missing."""
from muxplex.cli import doctor from muxplex.cli import doctor
@@ -351,7 +280,6 @@ def test_doctor_reports_missing_ttyd(capsys, monkeypatch):
assert "ttyd" in out assert "ttyd" in out
assert "not found" in out assert "not found" in out
def test_doctor_shows_platform(capsys): def test_doctor_shows_platform(capsys):
"""doctor must show platform info.""" """doctor must show platform info."""
from muxplex.cli import doctor from muxplex.cli import doctor
@@ -360,7 +288,6 @@ def test_doctor_shows_platform(capsys):
out = capsys.readouterr().out out = capsys.readouterr().out
assert "Platform" in out assert "Platform" in out
def test_doctor_subcommand_registered(): def test_doctor_subcommand_registered():
"""doctor must be a valid subcommand in main() argparse.""" """doctor must be a valid subcommand in main() argparse."""
import io import io
@@ -378,7 +305,6 @@ def test_doctor_subcommand_registered():
help_text = buf.getvalue().lower() help_text = buf.getvalue().lower()
assert "doctor" in help_text assert "doctor" in help_text
def test_main_dispatches_to_doctor(monkeypatch): def test_main_dispatches_to_doctor(monkeypatch):
"""main() with 'doctor' subcommand must invoke doctor().""" """main() with 'doctor' subcommand must invoke doctor()."""
from muxplex.cli import main from muxplex.cli import main
@@ -393,12 +319,10 @@ def test_main_dispatches_to_doctor(monkeypatch):
"doctor() must be called once when 'doctor' subcommand is used" "doctor() must be called once when 'doctor' subcommand is used"
) )
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# upgrade / update subcommand tests # upgrade / update subcommand tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_upgrade_subcommand_registered(): def test_upgrade_subcommand_registered():
"""upgrade must be a valid subcommand.""" """upgrade must be a valid subcommand."""
import io import io
@@ -416,7 +340,6 @@ def test_upgrade_subcommand_registered():
help_text = buf.getvalue().lower() help_text = buf.getvalue().lower()
assert "upgrade" in help_text assert "upgrade" in help_text
def test_update_alias_registered(): def test_update_alias_registered():
"""update must be a valid subcommand (alias for upgrade).""" """update must be a valid subcommand (alias for upgrade)."""
import io import io
@@ -434,7 +357,6 @@ def test_update_alias_registered():
help_text = buf.getvalue().lower() help_text = buf.getvalue().lower()
assert "update" in help_text assert "update" in help_text
def test_upgrade_calls_uv_tool_install(monkeypatch, capsys): def test_upgrade_calls_uv_tool_install(monkeypatch, capsys):
"""upgrade must attempt uv tool install when update is available.""" """upgrade must attempt uv tool install when update is available."""
import subprocess import subprocess
@@ -464,7 +386,6 @@ def test_upgrade_calls_uv_tool_install(monkeypatch, capsys):
uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)] uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)]
assert len(uv_calls) > 0, "upgrade must call uv tool install" assert len(uv_calls) > 0, "upgrade must call uv tool install"
def test_main_dispatches_to_upgrade(monkeypatch): def test_main_dispatches_to_upgrade(monkeypatch):
"""main() with 'upgrade' subcommand must invoke upgrade().""" """main() with 'upgrade' subcommand must invoke upgrade()."""
from muxplex.cli import main from muxplex.cli import main
@@ -477,7 +398,6 @@ def test_main_dispatches_to_upgrade(monkeypatch):
assert len(calls) == 1, "upgrade() must be called once for 'upgrade' subcommand" assert len(calls) == 1, "upgrade() must be called once for 'upgrade' subcommand"
def test_main_dispatches_update_to_upgrade(monkeypatch): def test_main_dispatches_update_to_upgrade(monkeypatch):
"""main() with 'update' subcommand must also invoke upgrade().""" """main() with 'update' subcommand must also invoke upgrade()."""
from muxplex.cli import main from muxplex.cli import main
@@ -490,12 +410,10 @@ def test_main_dispatches_update_to_upgrade(monkeypatch):
assert len(calls) == 1, "upgrade() must be called once for 'update' subcommand" assert len(calls) == 1, "upgrade() must be called once for 'update' subcommand"
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Smart version-check tests (_get_install_info / _check_for_update) # Smart version-check tests (_get_install_info / _check_for_update)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_get_install_info_returns_dict(): def test_get_install_info_returns_dict():
"""_get_install_info must return a dict with all required keys.""" """_get_install_info must return a dict with all required keys."""
from muxplex.cli import _get_install_info from muxplex.cli import _get_install_info
@@ -507,7 +425,6 @@ def test_get_install_info_returns_dict():
assert "url" in info assert "url" in info
assert info["source"] in ("git", "editable", "pypi", "unknown") assert info["source"] in ("git", "editable", "pypi", "unknown")
def test_check_for_update_editable_returns_false(): def test_check_for_update_editable_returns_false():
"""Editable installs must never suggest an update.""" """Editable installs must never suggest an update."""
from muxplex.cli import _check_for_update from muxplex.cli import _check_for_update
@@ -517,7 +434,6 @@ def test_check_for_update_editable_returns_false():
assert available is False assert available is False
assert "editable" in msg assert "editable" in msg
def test_upgrade_force_skips_version_check(monkeypatch, capsys): def test_upgrade_force_skips_version_check(monkeypatch, capsys):
"""upgrade(force=True) must skip the version check and proceed to install.""" """upgrade(force=True) must skip the version check and proceed to install."""
import subprocess import subprocess
@@ -550,7 +466,6 @@ def test_upgrade_force_skips_version_check(monkeypatch, capsys):
uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)] uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)]
assert len(uv_calls) > 0, "upgrade(force=True) must still call uv tool install" assert len(uv_calls) > 0, "upgrade(force=True) must still call uv tool install"
def test_upgrade_already_up_to_date_skips_install(monkeypatch, capsys): def test_upgrade_already_up_to_date_skips_install(monkeypatch, capsys):
"""upgrade() must print 'up to date' and NOT call uv when version check says current.""" """upgrade() must print 'up to date' and NOT call uv when version check says current."""
import subprocess import subprocess
@@ -581,7 +496,6 @@ def test_upgrade_already_up_to_date_skips_install(monkeypatch, capsys):
uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)] uv_calls = [c for c in calls if isinstance(c, list) and "uv" in str(c)]
assert len(uv_calls) == 0, "uv must NOT be called when already up to date" assert len(uv_calls) == 0, "uv must NOT be called when already up to date"
def test_upgrade_force_flag_registered(): def test_upgrade_force_flag_registered():
"""upgrade --force must be accepted by argparse without error.""" """upgrade --force must be accepted by argparse without error."""
import io import io
@@ -599,12 +513,10 @@ def test_upgrade_force_flag_registered():
help_text = buf.getvalue() help_text = buf.getvalue()
assert "--force" in help_text assert "--force" in help_text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# serve() settings.json integration tests # serve() settings.json integration tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_serve_reads_host_from_settings(tmp_path, monkeypatch): def test_serve_reads_host_from_settings(tmp_path, monkeypatch):
"""serve(host=None) must use host from settings.json.""" """serve(host=None) must use host from settings.json."""
settings_file = tmp_path / "settings.json" settings_file = tmp_path / "settings.json"
@@ -626,7 +538,6 @@ def test_serve_reads_host_from_settings(tmp_path, monkeypatch):
assert len(calls) == 1 assert len(calls) == 1
assert calls[0]["host"] == "192.168.0.1" assert calls[0]["host"] == "192.168.0.1"
def test_serve_cli_flag_overrides_settings(tmp_path, monkeypatch): def test_serve_cli_flag_overrides_settings(tmp_path, monkeypatch):
"""serve(host='10.0.0.1') must override settings.json host.""" """serve(host='10.0.0.1') must override settings.json host."""
settings_file = tmp_path / "settings.json" settings_file = tmp_path / "settings.json"
@@ -648,7 +559,6 @@ def test_serve_cli_flag_overrides_settings(tmp_path, monkeypatch):
assert len(calls) == 1 assert len(calls) == 1
assert calls[0]["host"] == "10.0.0.1" assert calls[0]["host"] == "10.0.0.1"
def test_serve_falls_back_to_default_when_no_settings_file(tmp_path, monkeypatch): def test_serve_falls_back_to_default_when_no_settings_file(tmp_path, monkeypatch):
"""serve() with no settings file and no CLI flags uses hardcoded defaults.""" """serve() with no settings file and no CLI flags uses hardcoded defaults."""
settings_file = tmp_path / "nonexistent_settings.json" settings_file = tmp_path / "nonexistent_settings.json"
@@ -671,7 +581,6 @@ def test_serve_falls_back_to_default_when_no_settings_file(tmp_path, monkeypatch
assert calls[0]["host"] == "127.0.0.1" assert calls[0]["host"] == "127.0.0.1"
assert calls[0]["port"] == 8088 assert calls[0]["port"] == 8088
def test_serve_port_from_settings(tmp_path, monkeypatch): def test_serve_port_from_settings(tmp_path, monkeypatch):
"""serve(port=None) must use port from settings.json.""" """serve(port=None) must use port from settings.json."""
settings_file = tmp_path / "settings.json" settings_file = tmp_path / "settings.json"
@@ -693,7 +602,6 @@ def test_serve_port_from_settings(tmp_path, monkeypatch):
assert len(calls) == 1 assert len(calls) == 1
assert calls[0]["port"] == 9999 assert calls[0]["port"] == 9999
def test_serve_session_ttl_from_settings(tmp_path, monkeypatch): def test_serve_session_ttl_from_settings(tmp_path, monkeypatch):
"""serve(session_ttl=None) must use session_ttl from settings.json.""" """serve(session_ttl=None) must use session_ttl from settings.json."""
settings_file = tmp_path / "settings.json" settings_file = tmp_path / "settings.json"
@@ -710,7 +618,6 @@ def test_serve_session_ttl_from_settings(tmp_path, monkeypatch):
assert os.environ.get("MUXPLEX_SESSION_TTL") == "3600" assert os.environ.get("MUXPLEX_SESSION_TTL") == "3600"
def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch): def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
"""serve(session_ttl=0) must work — 0 means browser session, a valid value.""" """serve(session_ttl=0) must work — 0 means browser session, a valid value."""
settings_file = tmp_path / "settings.json" settings_file = tmp_path / "settings.json"
@@ -727,13 +634,11 @@ def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
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, # argparse refactoring tests — None defaults, serve flags on both parsers,
# upgrade alias, install-service deprecation warning # upgrade alias, install-service deprecation warning
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_main_passes_none_for_unset_flags(): def test_main_passes_none_for_unset_flags():
"""main() with no flags passes None for host/port/auth/session_ttl to serve().""" """main() with no flags passes None for host/port/auth/session_ttl to serve()."""
from muxplex.cli import main from muxplex.cli import main
@@ -745,7 +650,6 @@ def test_main_passes_none_for_unset_flags():
host=None, port=None, auth=None, session_ttl=None host=None, port=None, auth=None, session_ttl=None
) )
def test_main_passes_explicit_host_only(): def test_main_passes_explicit_host_only():
"""main() with --host 10.0.0.1 passes host='10.0.0.1', others as None.""" """main() with --host 10.0.0.1 passes host='10.0.0.1', others as None."""
from muxplex.cli import main from muxplex.cli import main
@@ -757,7 +661,6 @@ def test_main_passes_explicit_host_only():
host="10.0.0.1", port=None, auth=None, session_ttl=None host="10.0.0.1", port=None, auth=None, session_ttl=None
) )
def test_main_serve_subcommand_accepts_flags(): def test_main_serve_subcommand_accepts_flags():
"""'muxplex serve --host 10.0.0.1 --port 9000' passes values to serve().""" """'muxplex serve --host 10.0.0.1 --port 9000' passes values to serve()."""
from muxplex.cli import main from muxplex.cli import main
@@ -771,20 +674,6 @@ def test_main_serve_subcommand_accepts_flags():
host="10.0.0.1", port=9000, auth=None, session_ttl=None host="10.0.0.1", port=9000, auth=None, session_ttl=None
) )
def test_install_service_subcommand_prints_deprecation_warning(capsys):
"""'muxplex install-service' must print a deprecation warning to stderr."""
from muxplex.cli import main
with patch("muxplex.service.service_install"):
with patch("sys.argv", ["muxplex", "install-service"]):
main()
captured = capsys.readouterr()
assert "deprecated" in captured.err.lower()
assert "muxplex service install" in captured.err
def test_help_shows_single_upgrade_line(): def test_help_shows_single_upgrade_line():
"""Help output shows 'upgrade (update)' alias notation, not two separate subcommand entries.""" """Help output shows 'upgrade (update)' alias notation, not two separate subcommand entries."""
import io import io
@@ -807,7 +696,6 @@ def test_help_shows_single_upgrade_line():
f"Got help text:\n{help_text}" f"Got help text:\n{help_text}"
) )
def test_doctor_shows_serve_config(tmp_path, monkeypatch, capsys): def test_doctor_shows_serve_config(tmp_path, monkeypatch, capsys):
"""doctor() must show the current serve config (host, port, auth).""" """doctor() must show the current serve config (host, port, auth)."""
import json import json
@@ -829,12 +717,10 @@ def test_doctor_shows_serve_config(tmp_path, monkeypatch, capsys):
assert "9999" in out assert "9999" in out
assert "password" in out assert "password" in out
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# service subcommand dispatch tests # service subcommand dispatch tests
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_service_install_dispatches(): def test_service_install_dispatches():
"""muxplex service install must call service_install().""" """muxplex service install must call service_install()."""
from muxplex.cli import main from muxplex.cli import main
@@ -844,7 +730,6 @@ def test_service_install_dispatches():
main() main()
mock_fn.assert_called_once() mock_fn.assert_called_once()
def test_service_uninstall_dispatches(): def test_service_uninstall_dispatches():
"""muxplex service uninstall must call service_uninstall().""" """muxplex service uninstall must call service_uninstall()."""
from muxplex.cli import main from muxplex.cli import main
@@ -854,7 +739,6 @@ def test_service_uninstall_dispatches():
main() main()
mock_fn.assert_called_once() mock_fn.assert_called_once()
def test_service_start_dispatches(): def test_service_start_dispatches():
"""muxplex service start must call service_start().""" """muxplex service start must call service_start()."""
from muxplex.cli import main from muxplex.cli import main
@@ -864,7 +748,6 @@ def test_service_start_dispatches():
main() main()
mock_fn.assert_called_once() mock_fn.assert_called_once()
def test_service_stop_dispatches(): def test_service_stop_dispatches():
"""muxplex service stop must call service_stop().""" """muxplex service stop must call service_stop()."""
from muxplex.cli import main from muxplex.cli import main
@@ -874,7 +757,6 @@ def test_service_stop_dispatches():
main() main()
mock_fn.assert_called_once() mock_fn.assert_called_once()
def test_service_restart_dispatches(): def test_service_restart_dispatches():
"""muxplex service restart must call service_restart().""" """muxplex service restart must call service_restart()."""
from muxplex.cli import main from muxplex.cli import main
@@ -884,7 +766,6 @@ def test_service_restart_dispatches():
main() main()
mock_fn.assert_called_once() mock_fn.assert_called_once()
def test_service_status_dispatches(): def test_service_status_dispatches():
"""muxplex service status must call service_status().""" """muxplex service status must call service_status()."""
from muxplex.cli import main from muxplex.cli import main
@@ -894,7 +775,6 @@ def test_service_status_dispatches():
main() main()
mock_fn.assert_called_once() mock_fn.assert_called_once()
def test_service_logs_dispatches(): def test_service_logs_dispatches():
"""muxplex service logs must call service_logs().""" """muxplex service logs must call service_logs()."""
from muxplex.cli import main from muxplex.cli import main
@@ -904,20 +784,6 @@ def test_service_logs_dispatches():
main() main()
mock_fn.assert_called_once() mock_fn.assert_called_once()
def test_install_service_deprecated_calls_service_install(capsys):
"""'muxplex install-service' deprecated alias must call service_install() and print deprecation warning."""
from muxplex.cli import main
with patch("muxplex.service.service_install") as mock_service_install:
with patch("sys.argv", ["muxplex", "install-service"]):
main()
mock_service_install.assert_called_once()
captured = capsys.readouterr()
assert "deprecated" in captured.err.lower()
def test_service_subcommand_in_help(): def test_service_subcommand_in_help():
"""'service' must appear in muxplex --help output.""" """'service' must appear in muxplex --help output."""
import io import io
@@ -935,21 +801,10 @@ def test_service_subcommand_in_help():
help_text = buf.getvalue().lower() help_text = buf.getvalue().lower()
assert "service" in help_text assert "service" in help_text
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# task-6: Verify old install_service/launchd/systemd removed from cli.py # task-6: Verify old install_service/launchd/systemd removed from cli.py
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_old_install_service_removed_from_cli():
"""install_service must no longer exist in muxplex.cli (moved to muxplex.service)."""
import muxplex.cli as cli_mod
assert not hasattr(cli_mod, "install_service"), (
"install_service should be removed from cli.py; use muxplex.service.service_install"
)
def test_old_install_launchd_removed_from_cli(): def test_old_install_launchd_removed_from_cli():
"""_install_launchd must no longer exist in muxplex.cli (moved to muxplex.service).""" """_install_launchd must no longer exist in muxplex.cli (moved to muxplex.service)."""
import muxplex.cli as cli_mod import muxplex.cli as cli_mod
@@ -958,7 +813,6 @@ def test_old_install_launchd_removed_from_cli():
"_install_launchd should be removed from cli.py; functionality is in muxplex.service" "_install_launchd should be removed from cli.py; functionality is in muxplex.service"
) )
def test_old_install_systemd_removed_from_cli(): def test_old_install_systemd_removed_from_cli():
"""_install_systemd must no longer exist in muxplex.cli (moved to muxplex.service).""" """_install_systemd must no longer exist in muxplex.cli (moved to muxplex.service)."""
import muxplex.cli as cli_mod import muxplex.cli as cli_mod
@@ -967,7 +821,6 @@ def test_old_install_systemd_removed_from_cli():
"_install_systemd should be removed from cli.py; functionality is in muxplex.service" "_install_systemd should be removed from cli.py; functionality is in muxplex.service"
) )
def test_upgrade_uses_service_module_install(monkeypatch, capsys): def test_upgrade_uses_service_module_install(monkeypatch, capsys):
"""upgrade() must call muxplex.service.service_install instead of cli.install_service.""" """upgrade() must call muxplex.service.service_install instead of cli.install_service."""
import subprocess import subprocess
-20
View File
@@ -43,23 +43,3 @@ def test_readme_shows_restart_workflow():
assert "muxplex service restart" in README, ( assert "muxplex service restart" in README, (
"README must include 'muxplex service restart' in the example" "README must include 'muxplex service restart' in the example"
) )
def test_readme_no_plain_install_service_in_install_sections():
"""README install/setup sections must not use plain 'muxplex install-service' as active command."""
lines = README.splitlines()
# Find lines that are in bash code blocks and contain install-service without being
# marked deprecated or commented out
in_code_block = False
for i, line in enumerate(lines):
stripped = line.strip()
if stripped.startswith("```"):
in_code_block = not in_code_block
if in_code_block and "install-service" in stripped:
# Allow lines that are comments (# deprecated) or explicitly deprecated
if not stripped.startswith("#"):
# This is an active command — it should NOT be 'muxplex install-service'
assert "muxplex install-service" not in stripped, (
f"Line {i + 1} has active 'muxplex install-service' command in code block: {line!r}. "
"Update to use 'muxplex service install' instead."
)