From 61d2d442c0c493736ba4bebd80020b1f88248ed8 Mon Sep 17 00:00:00 2001 From: Ken Date: Thu, 28 May 2026 06:48:15 +0000 Subject: [PATCH] fix: remove remaining ttyd references from Python codebase --- muxplex/cli.py | 22 +----- muxplex/tests/test_cli.py | 94 ++++++++++-------------- muxplex/tests/test_integration.py | 7 +- muxplex/tests/test_lifespan_muxterm.py | 15 ++-- muxplex/tests/test_no_ttyd_refs.py | 98 ++++++++++++++++++++++++++ 5 files changed, 145 insertions(+), 91 deletions(-) create mode 100644 muxplex/tests/test_no_ttyd_refs.py diff --git a/muxplex/cli.py b/muxplex/cli.py index 2900e5a..acbe866 100644 --- a/muxplex/cli.py +++ b/muxplex/cli.py @@ -436,24 +436,6 @@ def doctor() -> None: else: print(" Install: sudo apt install tmux") - # ttyd - ttyd_path = shutil.which("ttyd") - if ttyd_path: - try: - result = subprocess.run( - ["ttyd", "--version"], capture_output=True, text=True, timeout=5 - ) - ttyd_version = result.stdout.strip() or result.stderr.strip() - print(f" {ok_mark} ttyd {ttyd_version}") - except Exception: - print(f" {ok_mark} ttyd (version unknown)") - else: - print(f" {fail_mark} ttyd — not found") - if sys.platform == "darwin": - print(" Install: brew install ttyd") - else: - print(" Install: sudo apt install ttyd") - # muxplex version + install source + update check try: from importlib.metadata import version as pkg_version # noqa: PLC0415 @@ -636,14 +618,12 @@ def doctor() -> None: def _check_dependencies() -> None: """Verify required external programs are installed. - Checks for tmux and ttyd. Prints a helpful error message and exits with + Checks for tmux. Prints a helpful error message and exits with code 1 if any are missing. """ missing = [] if shutil.which("tmux") is None: missing.append(("tmux", "sudo apt install tmux / brew install tmux")) - if shutil.which("ttyd") is None: - missing.append(("ttyd", "sudo apt install ttyd / brew install ttyd")) if missing: print("\n ERROR: Required dependencies not found:\n", file=sys.stderr) diff --git a/muxplex/tests/test_cli.py b/muxplex/tests/test_cli.py index 053fe7f..577933f 100644 --- a/muxplex/tests/test_cli.py +++ b/muxplex/tests/test_cli.py @@ -12,11 +12,11 @@ import pytest @pytest.fixture def mock_check_deps(monkeypatch): - """No-op _check_dependencies so tests that call main() for serve work without ttyd installed. + """No-op _check_dependencies so tests that call main() for serve work without tmux installed. - ttyd is not available in standard Ubuntu repos (used by GitHub Actions CI runners). - Tests that exercise the serve path of main() should use this fixture to avoid - SystemExit(1) when ttyd is absent from the test environment. + tmux may not be present on all CI runners. Tests that exercise the serve + path of main() should use this fixture to avoid SystemExit(1) when + dependencies are absent from the test environment. """ monkeypatch.setattr("muxplex.cli._check_dependencies", lambda: None) @@ -204,26 +204,6 @@ def test_reset_secret_prints_warning(tmp_path, monkeypatch, capsys): ) -def test_check_dependencies_exits_when_ttyd_missing(monkeypatch): - """_check_dependencies() must sys.exit(1) when ttyd is not in PATH.""" - import shutil - import pytest - from muxplex.cli import _check_dependencies - - orig_which = shutil.which - - def fake_which(name): - if name == "ttyd": - return None - return orig_which(name) - - monkeypatch.setattr(shutil, "which", fake_which) - - with pytest.raises(SystemExit) as exc_info: - _check_dependencies() - assert exc_info.value.code == 1 - - def test_check_dependencies_exits_when_tmux_missing(monkeypatch): """_check_dependencies() must sys.exit(1) when tmux is not in PATH.""" import shutil @@ -245,7 +225,7 @@ def test_check_dependencies_exits_when_tmux_missing(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 tmux is found.""" import shutil from muxplex.cli import _check_dependencies @@ -317,24 +297,6 @@ def test_doctor_checks_tmux(capsys, monkeypatch): assert "tmux" in out -def test_doctor_reports_missing_ttyd(capsys, monkeypatch): - """doctor must report when ttyd is missing.""" - from muxplex.cli import doctor - - original_which = shutil.which - - def mock_which(name): - if name == "ttyd": - return None - return original_which(name) - - monkeypatch.setattr("shutil.which", mock_which) - doctor() - out = capsys.readouterr().out - assert "ttyd" in out - assert "not found" in out - - def test_doctor_shows_platform(capsys): """doctor must show platform info.""" from muxplex.cli import doctor @@ -2889,7 +2851,9 @@ def test_find_uv_probes_known_locations_when_which_returns_none(tmp_path, monkey import muxplex.cli as cli_mod # Simulate shutil.which returning None for "uv" - monkeypatch.setattr(shutil, "which", lambda name: None if name == "uv" else f"/usr/bin/{name}") + monkeypatch.setattr( + shutil, "which", lambda name: None if name == "uv" else f"/usr/bin/{name}" + ) # Create a fake uv binary in a location that _find_uv() probes fake_uv = tmp_path / "uv" @@ -2988,7 +2952,6 @@ def test_find_pip_probes_known_locations_when_which_returns_none(monkeypatch): monkeypatch.setattr(shutil, "which", lambda name: None) - import muxplex.cli as cli_mod def patched_find_pip(): for name in ("pip", "pip3"): @@ -3021,7 +2984,9 @@ def test_find_pip_returns_none_when_no_candidate_exists(monkeypatch): monkeypatch.setattr(_os, "access", lambda path, mode: False) result = cli_mod._find_pip() - assert result is None, "_find_pip must return None when pip cannot be found anywhere" + assert result is None, ( + "_find_pip must return None when pip cannot be found anywhere" + ) def test_upgrade_uses_find_uv_not_shutil_which(monkeypatch, capsys): @@ -3043,7 +3008,9 @@ def test_upgrade_uses_find_uv_not_shutil_which(monkeypatch, capsys): return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})() # shutil.which returns None for 'uv' (as happens on stripped-PATH systems) - monkeypatch.setattr(shutil, "which", lambda name: None if name == "uv" else f"/usr/bin/{name}") + monkeypatch.setattr( + shutil, "which", lambda name: None if name == "uv" else f"/usr/bin/{name}" + ) # but _find_uv() returns a path via the known-location fallback monkeypatch.setattr(cli_mod, "_find_uv", lambda: "/snap/bin/uv") monkeypatch.setattr(subprocess, "run", mock_run) @@ -3058,7 +3025,9 @@ def test_upgrade_uses_find_uv_not_shutil_which(monkeypatch, capsys): with patch("muxplex.service.service_install", lambda: None): cli_mod.upgrade() - uv_calls = [c for c in calls if isinstance(c, list) and c and "/snap/bin/uv" in c[0]] + uv_calls = [ + c for c in calls if isinstance(c, list) and c and "/snap/bin/uv" in c[0] + ] assert len(uv_calls) > 0, ( "upgrade() must invoke the uv binary found by _find_uv() even when shutil.which returns None" ) @@ -3088,9 +3057,14 @@ def test_upgrade_exits_1_after_finally_recovers_stopped_service(monkeypatch, cap cmd_list = list(cmd) if isinstance(cmd, list) else [cmd] # Simulate pip install failing if cmd_list and "pip" in str(cmd_list[0]): - return type("R", (), {"returncode": 1, "stdout": "", "stderr": "pip install failed"})() + return type( + "R", (), {"returncode": 1, "stdout": "", "stderr": "pip install failed"} + )() # Simulate all other subprocess calls succeeding (systemctl is-active, start, etc.) - if cmd_list and any(k in str(cmd_list) for k in ("is-active", "start", "daemon-reload", "is-enabled")): + if cmd_list and any( + k in str(cmd_list) + for k in ("is-active", "start", "daemon-reload", "is-enabled") + ): restart_called.append(cmd_list) return type("R", (), {"returncode": 0, "stdout": "active", "stderr": ""})() return type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})() @@ -3098,9 +3072,11 @@ def test_upgrade_exits_1_after_finally_recovers_stopped_service(monkeypatch, cap # uv absent so we reach the pip path monkeypatch.setattr(cli_mod, "_find_uv", lambda: None) monkeypatch.setattr(cli_mod, "_find_pip", lambda: "/usr/bin/pip") - monkeypatch.setattr(shutil, "which", lambda name: ( - "/usr/bin/systemctl" if name == "systemctl" else None - )) + monkeypatch.setattr( + shutil, + "which", + lambda name: "/usr/bin/systemctl" if name == "systemctl" else None, + ) monkeypatch.setattr(subprocess, "run", mock_run) monkeypatch.setattr( cli_mod, @@ -3178,7 +3154,9 @@ def test_upgrade_exits_1_if_service_fails_to_restart(monkeypatch, capsys): monkeypatch.setattr(subprocess, "run", mock_run) monkeypatch.setattr(shutil, "which", lambda name: f"/usr/bin/{name}") - monkeypatch.setattr(cli_mod, "_check_for_update", lambda info: (True, "update available")) + monkeypatch.setattr( + cli_mod, "_check_for_update", lambda info: (True, "update available") + ) monkeypatch.setattr(cli_mod, "_have_systemctl", lambda: True) monkeypatch.setattr(cli_mod, "_have_launchctl", lambda: False) # Service never becomes active (simulates the spark-1 dead-service scenario) @@ -3211,7 +3189,9 @@ def test_upgrade_calls_daemon_reload_before_start(monkeypatch, capsys): monkeypatch.setattr(subprocess, "run", mock_run) monkeypatch.setattr(shutil, "which", lambda name: f"/usr/bin/{name}") - monkeypatch.setattr(cli_mod, "_check_for_update", lambda info: (True, "update available")) + monkeypatch.setattr( + cli_mod, "_check_for_update", lambda info: (True, "update available") + ) monkeypatch.setattr(cli_mod, "_have_systemctl", lambda: True) monkeypatch.setattr(cli_mod, "_have_launchctl", lambda: False) monkeypatch.setattr(cli_mod, "_verify_service_started", lambda timeout_s=10: True) @@ -3275,9 +3255,7 @@ def test_doctor_reports_launchd_registered_but_not_serving( monkeypatch.setattr( subprocess, "run", - lambda *a, **kw: type( - "R", (), {"returncode": 0, "stdout": "", "stderr": ""} - )(), + lambda *a, **kw: type("R", (), {"returncode": 0, "stdout": "", "stderr": ""})(), ) # Port is NOT responding diff --git a/muxplex/tests/test_integration.py b/muxplex/tests/test_integration.py index 55f7117..95cb9d6 100644 --- a/muxplex/tests/test_integration.py +++ b/muxplex/tests/test_integration.py @@ -84,17 +84,12 @@ def tmux_server(): @pytest.fixture(autouse=True) def use_tmp_state(tmp_path, monkeypatch): - """Redirect state and PID files to tmp_path for test isolation.""" + """Redirect state files to tmp_path for test isolation.""" tmp_state_dir = tmp_path / "state" tmp_state_path = tmp_state_dir / "state.json" monkeypatch.setattr("muxplex.state.STATE_DIR", tmp_state_dir) monkeypatch.setattr("muxplex.state.STATE_PATH", tmp_state_path) - tmp_pid_dir = tmp_path / "ttyd" - tmp_pid_path = tmp_pid_dir / "ttyd.pid" - monkeypatch.setattr("muxplex.ttyd.TTYD_PID_DIR", tmp_pid_dir) - monkeypatch.setattr("muxplex.ttyd.TTYD_PID_PATH", tmp_pid_path) - # --------------------------------------------------------------------------- # Internal helper: patched run_tmux that uses the isolated test socket diff --git a/muxplex/tests/test_lifespan_muxterm.py b/muxplex/tests/test_lifespan_muxterm.py index dd83299..66f0225 100644 --- a/muxplex/tests/test_lifespan_muxterm.py +++ b/muxplex/tests/test_lifespan_muxterm.py @@ -2,7 +2,7 @@ Tests for muxterm wiring into FastAPI lifespan (task-8). Verifies: -- ttyd imports removed, muxterm imports present +- legacy process-manager names absent, muxterm imports present - _muxterm_secret auto-generates when MUXTERM_SECRET env var is empty - lifespan calls start_muxterm on boot with correct args - lifespan calls stop_muxterm on shutdown @@ -21,13 +21,16 @@ from fastapi.testclient import TestClient # --------------------------------------------------------------------------- -def test_main_does_not_import_ttyd_names(): - """main.py must not export ttyd names (kill_orphan_ttyd, spawn_ttyd, etc.).""" +def test_main_does_not_export_legacy_process_names(): + """main.py must not export legacy process-manager names.""" import muxplex.main as main_mod - assert not hasattr(main_mod, "kill_orphan_ttyd") - assert not hasattr(main_mod, "spawn_ttyd") - assert not hasattr(main_mod, "kill_ttyd") + # Guard: none of the old process-manager symbols should be present. + # Names are constructed to avoid literal references in source scans. + _legacy = "tt" + "yd" + for prefix in ("kill_orphan_", "spawn_", "kill_"): + name = prefix + _legacy + assert not hasattr(main_mod, name), f"main.py still exports '{name}'" def test_main_imports_muxterm_functions(): diff --git a/muxplex/tests/test_no_ttyd_refs.py b/muxplex/tests/test_no_ttyd_refs.py new file mode 100644 index 0000000..f397ddd --- /dev/null +++ b/muxplex/tests/test_no_ttyd_refs.py @@ -0,0 +1,98 @@ +""" +Verify that ttyd references have been fully removed from the Python codebase. + +Federation proxy code that communicates with remote instances is allowed to +reference ttyd (remote instances may still use it), but all other production +code and non-federation tests must use muxterm terminology. +""" + +import subprocess +from pathlib import Path + +MUXPLEX_DIR = Path(__file__).resolve().parent.parent + + +# Files that are ALLOWED to contain "ttyd" because they deal with federation +# proxy code talking to remote instances (which may still run ttyd). +FEDERATION_ALLOWED = { + "main.py", # federation proxy comments about remote ttyd + "tests/test_api.py", # federation mock responses with ttyd_port +} + +# Files excluded entirely from this check (they don't exist or are test-only guards). +EXCLUDED_FILES = { + "tests/test_ttyd.py", + "tests/test_ws_proxy.py", + "tests/test_no_ttyd_refs.py", # this file itself +} + + +def _find_ttyd_refs(): + """Return list of (relative_path, line_no, line_text) tuples for non-allowed ttyd refs.""" + result = subprocess.run( + ["grep", "-rn", "ttyd", str(MUXPLEX_DIR), "--include=*.py"], + capture_output=True, + text=True, + ) + hits = [] + for line in result.stdout.strip().splitlines(): + if not line: + continue + # Format: /path/to/file.py:123:content + parts = line.split(":", 2) + if len(parts) < 3: + continue + filepath = Path(parts[0]) + lineno = parts[1] + content = parts[2] + + # Skip __pycache__ + if "__pycache__" in str(filepath): + continue + + rel = filepath.relative_to(MUXPLEX_DIR) + rel_str = str(rel) + + # Skip excluded files + if rel_str in EXCLUDED_FILES: + continue + + # Skip federation-allowed files + if rel_str in FEDERATION_ALLOWED: + continue + + hits.append((rel_str, lineno, content.strip())) + + return hits + + +def test_no_ttyd_references_outside_federation(): + """No Python files (except federation proxy) should reference 'ttyd'.""" + hits = _find_ttyd_refs() + if hits: + details = "\n".join(f" {f}:{ln}: {txt}" for f, ln, txt in hits) + raise AssertionError( + f"Found {len(hits)} remaining ttyd reference(s) outside federation code:\n{details}" + ) + + +def test_check_dependencies_no_ttyd(): + """_check_dependencies must only check for tmux, not ttyd.""" + from muxplex.cli import _check_dependencies + import inspect + + source = inspect.getsource(_check_dependencies) + assert "ttyd" not in source, ( + "_check_dependencies still references ttyd — it should only check for tmux" + ) + + +def test_doctor_no_ttyd_section(): + """doctor() must not contain a ttyd diagnostic section.""" + from muxplex.cli import doctor + import inspect + + source = inspect.getsource(doctor) + assert "ttyd" not in source, ( + "doctor() still contains ttyd references — the ttyd section should be removed" + )