feat: refactor serve() to read settings.json with CLI override sentinel (task-2)

- Change serve() signature: all params now default to None (sentinel for 'not passed by CLI')
- Add load_settings() call: resolution order is CLI flag > settings.json > hardcoded default
- Switch from os.environ.setdefault() to os.environ[] hard assignment so settings.json values take effect
- Add 6 new tests: host/port/session_ttl from settings, CLI override, fallback to defaults, session_ttl=0 as valid value
This commit is contained in:
Brian Krabach
2026-03-31 14:17:02 -07:00
parent a4c2a8e3b8
commit c057ad6325
2 changed files with 160 additions and 16 deletions
+21 -8
View File
@@ -150,17 +150,30 @@ def show_password() -> None:
def serve( def serve(
host: str = "127.0.0.1", host: str | None = None,
port: int = 8088, port: int | None = None,
auth: str = "pam", auth: str | None = None,
session_ttl: int = 604800, session_ttl: int | None = None,
) -> None: ) -> None:
"""Start the muxplex server.""" """Start the muxplex server.
Resolution order: CLI flag (if not None) > settings.json > hardcoded default.
"""
import uvicorn # noqa: PLC0415 import uvicorn # noqa: PLC0415
os.environ.setdefault("MUXPLEX_PORT", str(port)) from muxplex.settings import load_settings # noqa: PLC0415
os.environ.setdefault("MUXPLEX_AUTH", auth)
os.environ.setdefault("MUXPLEX_SESSION_TTL", str(session_ttl)) settings = load_settings()
host = host if host is not None else settings.get("host", "127.0.0.1")
port = port if port is not None else settings.get("port", 8088)
auth = auth if auth is not None else settings.get("auth", "pam")
session_ttl = (
session_ttl if session_ttl is not None else settings.get("session_ttl", 604800)
)
os.environ["MUXPLEX_PORT"] = str(port)
os.environ["MUXPLEX_AUTH"] = auth
os.environ["MUXPLEX_SESSION_TTL"] = str(session_ttl)
from muxplex.main import app # noqa: PLC0415 from muxplex.main import app # noqa: PLC0415
+139 -8
View File
@@ -1,9 +1,11 @@
"""Tests for muxplex/cli.py — CLI entry point.""" """Tests for muxplex/cli.py — CLI entry point."""
import json
import os
import shutil import shutil
import stat import stat
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import MagicMock, patch
def test_cli_module_importable(): def test_cli_module_importable():
@@ -330,15 +332,16 @@ def test_install_service_uses_modern_launchctl_on_macos(tmp_path, monkeypatch, c
install_service(system=False) install_service(system=False)
launchctl_cmds = [c for c in calls if isinstance(c, list) and "launchctl" in str(c)] launchctl_cmds = [c for c in calls if isinstance(c, list) and "launchctl" in str(c)]
assert any( assert any("bootout" in str(c) for c in launchctl_cmds), (
"bootout" in str(c) for c in launchctl_cmds "must bootout old service before loading"
), "must bootout old service before loading" )
assert any( assert any("bootstrap" in str(c) for c in launchctl_cmds), (
"bootstrap" in str(c) for c in launchctl_cmds "must use launchctl bootstrap (modern API)"
), "must use launchctl bootstrap (modern API)" )
# Must NOT use deprecated load # Must NOT use deprecated load
assert not any( assert not any(
c == ["launchctl", "load"] or (isinstance(c, list) and c[:2] == ["launchctl", "load"]) c == ["launchctl", "load"]
or (isinstance(c, list) and c[:2] == ["launchctl", "load"])
for c in launchctl_cmds for c in launchctl_cmds
), "must NOT use deprecated launchctl load" ), "must NOT use deprecated launchctl load"
@@ -792,3 +795,131 @@ 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
# ---------------------------------------------------------------------------
def test_serve_reads_host_from_settings(tmp_path, monkeypatch):
"""serve(host=None) must use host from settings.json."""
settings_file = tmp_path / "settings.json"
settings_file.write_text(json.dumps({"host": "192.168.0.1"}))
monkeypatch.setattr("muxplex.settings.SETTINGS_PATH", settings_file)
calls = []
def fake_run(*args, **kwargs):
calls.append(kwargs)
with patch("uvicorn.run", fake_run):
with patch.dict("sys.modules", {"muxplex.main": MagicMock()}):
from muxplex.cli import serve
serve(host=None)
assert len(calls) == 1
assert calls[0]["host"] == "192.168.0.1"
def test_serve_cli_flag_overrides_settings(tmp_path, monkeypatch):
"""serve(host='10.0.0.1') must override settings.json host."""
settings_file = tmp_path / "settings.json"
settings_file.write_text(json.dumps({"host": "192.168.0.1"}))
monkeypatch.setattr("muxplex.settings.SETTINGS_PATH", settings_file)
calls = []
def fake_run(*args, **kwargs):
calls.append(kwargs)
with patch("uvicorn.run", fake_run):
with patch.dict("sys.modules", {"muxplex.main": MagicMock()}):
from muxplex.cli import serve
serve(host="10.0.0.1")
assert len(calls) == 1
assert calls[0]["host"] == "10.0.0.1"
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."""
settings_file = tmp_path / "nonexistent_settings.json"
# Deliberately not written — file does not exist
monkeypatch.setattr("muxplex.settings.SETTINGS_PATH", settings_file)
calls = []
def fake_run(*args, **kwargs):
calls.append(kwargs)
with patch("uvicorn.run", fake_run):
with patch.dict("sys.modules", {"muxplex.main": MagicMock()}):
from muxplex.cli import serve
serve()
assert len(calls) == 1
assert calls[0]["host"] == "127.0.0.1"
assert calls[0]["port"] == 8088
def test_serve_port_from_settings(tmp_path, monkeypatch):
"""serve(port=None) must use port from settings.json."""
settings_file = tmp_path / "settings.json"
settings_file.write_text(json.dumps({"port": 9999}))
monkeypatch.setattr("muxplex.settings.SETTINGS_PATH", settings_file)
calls = []
def fake_run(*args, **kwargs):
calls.append(kwargs)
with patch("uvicorn.run", fake_run):
with patch.dict("sys.modules", {"muxplex.main": MagicMock()}):
from muxplex.cli import serve
serve(port=None)
assert len(calls) == 1
assert calls[0]["port"] == 9999
def test_serve_session_ttl_from_settings(tmp_path, monkeypatch):
"""serve(session_ttl=None) must use session_ttl from settings.json."""
settings_file = tmp_path / "settings.json"
settings_file.write_text(json.dumps({"session_ttl": 3600}))
monkeypatch.setattr("muxplex.settings.SETTINGS_PATH", settings_file)
monkeypatch.delenv("MUXPLEX_SESSION_TTL", raising=False)
with patch("uvicorn.run"):
with patch.dict("sys.modules", {"muxplex.main": MagicMock()}):
from muxplex.cli import serve
serve(session_ttl=None)
assert os.environ.get("MUXPLEX_SESSION_TTL") == "3600"
def test_serve_session_ttl_zero_is_valid(tmp_path, monkeypatch):
"""serve(session_ttl=0) must work — 0 means browser session, a valid value."""
settings_file = tmp_path / "settings.json"
settings_file.write_text(json.dumps({"session_ttl": 3600}))
monkeypatch.setattr("muxplex.settings.SETTINGS_PATH", settings_file)
monkeypatch.delenv("MUXPLEX_SESSION_TTL", raising=False)
with patch("uvicorn.run"):
with patch.dict("sys.modules", {"muxplex.main": MagicMock()}):
from muxplex.cli import serve
serve(session_ttl=0)
assert os.environ.get("MUXPLEX_SESSION_TTL") == "0"