fix: remove remaining ttyd references from Python codebase
This commit is contained in:
+1
-21
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user