fix: remove remaining ttyd references from Python codebase

This commit is contained in:
Ken
2026-05-28 06:48:15 +00:00
parent 266430d1c0
commit 61d2d442c0
5 changed files with 145 additions and 91 deletions
+1 -21
View File
@@ -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)
+36 -58
View File
@@ -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
+1 -6
View File
@@ -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
+9 -6
View File
@@ -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():
+98
View File
@@ -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"
)