refactor: rename coordinator → muxplex package, move frontend inside as package data

This commit is contained in:
Brian Krabach
2026-03-28 02:02:50 -07:00
parent be26d40a31
commit 74b63033d7
30 changed files with 95 additions and 95 deletions
+3 -3
View File
@@ -1,5 +1,5 @@
"""
Bell flag polling and unseen_count tracking for the tmux-web coordinator.
Bell flag polling and unseen_count tracking for the tmux-web muxplex.
Based on spike findings: reading the tmux window_bell_flag does NOT clear it.
The flag persists until the window is made active inside tmux.
@@ -17,8 +17,8 @@ Public API:
import time
from coordinator.sessions import run_tmux
from coordinator.state import empty_bell
from muxplex.sessions import run_tmux
from muxplex.state import empty_bell
# ---------------------------------------------------------------------------
# In-memory tracking: session_name → bool (was flag set on last poll?)

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Before

Width:  |  Height:  |  Size: 669 B

After

Width:  |  Height:  |  Size: 669 B

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 401 B

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

+5 -5
View File
@@ -19,8 +19,8 @@ from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from pydantic import BaseModel
from coordinator.bells import apply_bell_clear_rule, process_bell_flags
from coordinator.sessions import (
from muxplex.bells import apply_bell_clear_rule, process_bell_flags
from muxplex.sessions import (
enumerate_sessions,
get_session_list,
get_snapshots,
@@ -28,7 +28,7 @@ from coordinator.sessions import (
snapshot_all,
update_session_cache,
)
from coordinator.state import (
from muxplex.state import (
empty_bell,
load_state,
prune_devices,
@@ -37,7 +37,7 @@ from coordinator.state import (
save_state,
state_lock,
)
from coordinator.ttyd import kill_orphan_ttyd, kill_ttyd, spawn_ttyd, TTYD_PORT
from muxplex.ttyd import kill_orphan_ttyd, kill_ttyd, spawn_ttyd, TTYD_PORT
# ---------------------------------------------------------------------------
# Configuration
@@ -341,5 +341,5 @@ async def setup_hooks() -> dict:
# Static file serving — MUST come after all API routes (first-match-wins)
# ---------------------------------------------------------------------------
_FRONTEND_DIR = pathlib.Path(__file__).parent.parent / "frontend"
_FRONTEND_DIR = pathlib.Path(__file__).parent / "frontend"
app.mount("/", StaticFiles(directory=str(_FRONTEND_DIR), html=True), name="frontend")
@@ -1,5 +1,5 @@
"""
tmux session enumeration and snapshot helpers for the tmux-web coordinator.
tmux session enumeration and snapshot helpers for the tmux-web muxplex.
In-memory cache:
_session_list most-recently-enumerated list of session names.
+1 -1
View File
@@ -1,5 +1,5 @@
"""
State schema and factory functions for the tmux-web coordinator.
State schema and factory functions for the tmux-web muxplex.
State schema (all values are plain JSON-serialisable dicts):
@@ -5,7 +5,7 @@ Tests for coordinator/main.py — FastAPI skeleton, lifespan, /health endpoint.
import pytest
from fastapi.testclient import TestClient
from coordinator.main import app
from muxplex.main import app
# ---------------------------------------------------------------------------
@@ -19,26 +19,26 @@ def patch_startup_and_state(tmp_path, monkeypatch):
# Redirect state files
tmp_state_dir = tmp_path / "state"
tmp_state_path = tmp_state_dir / "state.json"
monkeypatch.setattr("coordinator.state.STATE_DIR", tmp_state_dir)
monkeypatch.setattr("coordinator.state.STATE_PATH", tmp_state_path)
monkeypatch.setattr("muxplex.state.STATE_DIR", tmp_state_dir)
monkeypatch.setattr("muxplex.state.STATE_PATH", tmp_state_path)
# Redirect PID files
tmp_pid_dir = tmp_path / "ttyd"
tmp_pid_path = tmp_pid_dir / "ttyd.pid"
monkeypatch.setattr("coordinator.ttyd.TTYD_PID_DIR", tmp_pid_dir)
monkeypatch.setattr("coordinator.ttyd.TTYD_PID_PATH", tmp_pid_path)
monkeypatch.setattr("muxplex.ttyd.TTYD_PID_DIR", tmp_pid_dir)
monkeypatch.setattr("muxplex.ttyd.TTYD_PID_PATH", tmp_pid_path)
# Mock kill_orphan_ttyd so startup doesn't touch real processes (must be async)
async def _mock_kill_orphan():
return False
monkeypatch.setattr("coordinator.main.kill_orphan_ttyd", _mock_kill_orphan)
monkeypatch.setattr("muxplex.main.kill_orphan_ttyd", _mock_kill_orphan)
# Replace _poll_loop with a no-op so tests don't spin up real poll cycles
async def noop_poll_loop() -> None:
pass
monkeypatch.setattr("coordinator.main._poll_loop", noop_poll_loop)
monkeypatch.setattr("muxplex.main._poll_loop", noop_poll_loop)
# ---------------------------------------------------------------------------
@@ -101,7 +101,7 @@ def test_get_state_active_session_is_none_initially(client):
def test_patch_state_updates_session_order(client):
"""PATCH /api/state updates session_order and persists the change."""
from coordinator.state import load_state, save_state
from muxplex.state import load_state, save_state
# Write initial state with a known session order
initial_state = {
@@ -148,9 +148,9 @@ def test_patch_state_ignores_unknown_fields(client):
def test_get_sessions_returns_list(client, monkeypatch):
"""GET /api/sessions must return a JSON list."""
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["alpha"])
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha"])
monkeypatch.setattr(
"coordinator.main.get_snapshots", lambda: {"alpha": "some text"}
"muxplex.main.get_snapshots", lambda: {"alpha": "some text"}
)
response = client.get("/api/sessions")
@@ -162,10 +162,10 @@ def test_get_sessions_returns_list(client, monkeypatch):
def test_get_sessions_each_item_has_required_fields(client, monkeypatch):
"""Each item in GET /api/sessions must have name, snapshot, and bell fields."""
from coordinator.state import save_state
from muxplex.state import save_state
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["beta"])
monkeypatch.setattr("coordinator.main.get_snapshots", lambda: {"beta": "output"})
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["beta"])
monkeypatch.setattr("muxplex.main.get_snapshots", lambda: {"beta": "output"})
save_state(
{
"active_session": None,
@@ -191,9 +191,9 @@ def test_get_sessions_each_item_has_required_fields(client, monkeypatch):
def test_get_sessions_includes_snapshot_text(client, monkeypatch):
"""GET /api/sessions snapshot field must contain the cached capture-pane text."""
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["gamma"])
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["gamma"])
monkeypatch.setattr(
"coordinator.main.get_snapshots",
"muxplex.main.get_snapshots",
lambda: {"gamma": "hello from tmux pane"},
)
@@ -207,11 +207,11 @@ def test_get_sessions_includes_snapshot_text(client, monkeypatch):
def test_get_sessions_includes_bell_state(client, monkeypatch):
"""GET /api/sessions bell field must include unseen_count from persistent state."""
from coordinator.state import save_state
from muxplex.state import save_state
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["delta"])
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["delta"])
monkeypatch.setattr(
"coordinator.main.get_snapshots", lambda: {"delta": "pane text"}
"muxplex.main.get_snapshots", lambda: {"delta": "pane text"}
)
save_state(
{
@@ -239,8 +239,8 @@ def test_get_sessions_includes_bell_state(client, monkeypatch):
def test_get_sessions_returns_empty_list_when_no_sessions(client, monkeypatch):
"""GET /api/sessions must return an empty list when there are no sessions."""
monkeypatch.setattr("coordinator.main.get_session_list", lambda: [])
monkeypatch.setattr("coordinator.main.get_snapshots", lambda: {})
monkeypatch.setattr("muxplex.main.get_session_list", lambda: [])
monkeypatch.setattr("muxplex.main.get_snapshots", lambda: {})
response = client.get("/api/sessions")
assert response.status_code == 200
@@ -254,17 +254,17 @@ def test_get_sessions_returns_empty_list_when_no_sessions(client, monkeypatch):
def test_connect_session_returns_200(client, monkeypatch):
"""POST /api/sessions/{name}/connect returns 200 and correct body when session exists."""
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["alpha"])
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha"])
async def mock_kill():
return True
monkeypatch.setattr("coordinator.main.kill_ttyd", mock_kill)
monkeypatch.setattr("muxplex.main.kill_ttyd", mock_kill)
async def mock_spawn(name):
pass
monkeypatch.setattr("coordinator.main.spawn_ttyd", mock_spawn)
monkeypatch.setattr("muxplex.main.spawn_ttyd", mock_spawn)
response = client.post("/api/sessions/alpha/connect")
assert response.status_code == 200
@@ -275,19 +275,19 @@ def test_connect_session_returns_200(client, monkeypatch):
def test_connect_session_sets_active_session(client, monkeypatch):
"""POST /api/sessions/{name}/connect persists active_session to state."""
from coordinator.state import load_state
from muxplex.state import load_state
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["alpha"])
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha"])
async def mock_kill():
return True
monkeypatch.setattr("coordinator.main.kill_ttyd", mock_kill)
monkeypatch.setattr("muxplex.main.kill_ttyd", mock_kill)
async def mock_spawn(name):
pass
monkeypatch.setattr("coordinator.main.spawn_ttyd", mock_spawn)
monkeypatch.setattr("muxplex.main.spawn_ttyd", mock_spawn)
client.post("/api/sessions/alpha/connect")
@@ -297,7 +297,7 @@ def test_connect_session_sets_active_session(client, monkeypatch):
def test_connect_session_kills_existing_ttyd(client, monkeypatch):
"""POST /api/sessions/{name}/connect calls kill_ttyd then spawn_ttyd."""
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["alpha"])
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha"])
call_order = []
@@ -308,8 +308,8 @@ def test_connect_session_kills_existing_ttyd(client, monkeypatch):
async def mock_spawn(name):
call_order.append(("spawn", name))
monkeypatch.setattr("coordinator.main.kill_ttyd", mock_kill)
monkeypatch.setattr("coordinator.main.spawn_ttyd", mock_spawn)
monkeypatch.setattr("muxplex.main.kill_ttyd", mock_kill)
monkeypatch.setattr("muxplex.main.spawn_ttyd", mock_spawn)
response = client.post("/api/sessions/alpha/connect")
assert response.status_code == 200
@@ -318,7 +318,7 @@ def test_connect_session_kills_existing_ttyd(client, monkeypatch):
def test_connect_nonexistent_session_returns_404(client, monkeypatch):
"""POST /api/sessions/{name}/connect returns 404 when session is not in list."""
monkeypatch.setattr("coordinator.main.get_session_list", lambda: ["alpha", "beta"])
monkeypatch.setattr("muxplex.main.get_session_list", lambda: ["alpha", "beta"])
response = client.post("/api/sessions/gamma/connect")
assert response.status_code == 404
@@ -331,7 +331,7 @@ def test_connect_nonexistent_session_returns_404(client, monkeypatch):
def test_delete_current_kills_ttyd_and_clears_active(client, monkeypatch):
"""DELETE /api/sessions/current kills ttyd and clears active_session."""
from coordinator.state import load_state, save_state
from muxplex.state import load_state, save_state
# Set up initial state with active session
save_state(
@@ -349,7 +349,7 @@ def test_delete_current_kills_ttyd_and_clears_active(client, monkeypatch):
kill_called.append(True)
return True
monkeypatch.setattr("coordinator.main.kill_ttyd", mock_kill)
monkeypatch.setattr("muxplex.main.kill_ttyd", mock_kill)
response = client.delete("/api/sessions/current")
assert response.status_code == 200
@@ -488,7 +488,7 @@ def test_receive_bell_returns_ok_and_session_name(client):
def test_receive_bell_increments_unseen_count(client):
"""POST /api/sessions/{name}/bell increments unseen_count in state."""
from coordinator.state import load_state
from muxplex.state import load_state
client.post("/api/sessions/my-session/bell")
@@ -499,7 +499,7 @@ def test_receive_bell_increments_unseen_count(client):
def test_receive_bell_creates_session_entry_if_absent(client):
"""POST /api/sessions/{name}/bell creates session/bell entries if missing."""
from coordinator.state import load_state
from muxplex.state import load_state
# Ensure session does not exist in state yet
client.post("/api/sessions/brand-new/bell")
@@ -511,7 +511,7 @@ def test_receive_bell_creates_session_entry_if_absent(client):
def test_receive_bell_multiple_calls_accumulate(client):
"""Three POST calls to the bell endpoint accumulate unseen_count to 3."""
from coordinator.state import load_state
from muxplex.state import load_state
for _ in range(3):
client.post("/api/sessions/multi-session/bell")
@@ -525,7 +525,7 @@ def test_receive_bell_sets_last_fired_at(client):
"""POST /api/sessions/{name}/bell sets last_fired_at to a recent timestamp."""
import time
from coordinator.state import load_state
from muxplex.state import load_state
before = time.time()
client.post("/api/sessions/timed-session/bell")
@@ -546,7 +546,7 @@ def test_setup_hooks_returns_ok(client, monkeypatch):
"""POST /api/internal/setup-hooks returns {"ok": True} when tmux hook registers."""
from unittest.mock import AsyncMock
monkeypatch.setattr("coordinator.main.run_tmux", AsyncMock(return_value=""))
monkeypatch.setattr("muxplex.main.run_tmux", AsyncMock(return_value=""))
response = client.post("/api/internal/setup-hooks")
assert response.status_code == 200
@@ -559,7 +559,7 @@ def test_setup_hooks_returns_ok_false_on_error(client, monkeypatch):
from unittest.mock import AsyncMock
monkeypatch.setattr(
"coordinator.main.run_tmux",
"muxplex.main.run_tmux",
AsyncMock(side_effect=RuntimeError("tmux not found")),
)
@@ -575,7 +575,7 @@ def test_setup_hooks_curl_discards_response_body(client, monkeypatch):
from unittest.mock import AsyncMock
mock_run_tmux = AsyncMock(return_value="")
monkeypatch.setattr("coordinator.main.run_tmux", mock_run_tmux)
monkeypatch.setattr("muxplex.main.run_tmux", mock_run_tmux)
response = client.post("/api/internal/setup-hooks")
assert response.status_code == 200
@@ -594,11 +594,11 @@ def test_lifespan_alert_bell_hook_discards_response(monkeypatch):
"""Lifespan startup registers alert-bell hook with curl -o /dev/null to discard response."""
from unittest.mock import AsyncMock
from fastapi.testclient import TestClient
from coordinator.main import app
from muxplex.main import app
# Mock run_tmux to capture the hook command
mock_run_tmux = AsyncMock(return_value="")
monkeypatch.setattr("coordinator.main.run_tmux", mock_run_tmux)
monkeypatch.setattr("muxplex.main.run_tmux", mock_run_tmux)
# Trigger lifespan by creating a TestClient
with TestClient(app) as _:
@@ -8,14 +8,14 @@ from unittest.mock import AsyncMock, patch
import pytest
from coordinator.bells import (
from muxplex.bells import (
_bell_seen,
apply_bell_clear_rule,
poll_bell_flag,
process_bell_flags,
should_clear_bell,
)
from coordinator.state import empty_bell, empty_state
from muxplex.state import empty_bell, empty_state
# ---------------------------------------------------------------------------
@@ -38,14 +38,14 @@ def reset_bell_seen():
async def test_poll_bell_flag_returns_true_when_flag_is_1():
"""poll_bell_flag returns True when tmux reports window_bell_flag=1."""
with patch("coordinator.bells.run_tmux", new=AsyncMock(return_value="1\n")):
with patch("muxplex.bells.run_tmux", new=AsyncMock(return_value="1\n")):
result = await poll_bell_flag("my-session")
assert result is True
async def test_poll_bell_flag_returns_false_when_flag_is_0():
"""poll_bell_flag returns False when tmux reports window_bell_flag=0."""
with patch("coordinator.bells.run_tmux", new=AsyncMock(return_value="0\n")):
with patch("muxplex.bells.run_tmux", new=AsyncMock(return_value="0\n")):
result = await poll_bell_flag("my-session")
assert result is False
@@ -53,7 +53,7 @@ async def test_poll_bell_flag_returns_false_when_flag_is_0():
async def test_poll_bell_flag_returns_false_on_error():
"""poll_bell_flag returns False when run_tmux raises RuntimeError."""
with patch(
"coordinator.bells.run_tmux",
"muxplex.bells.run_tmux",
new=AsyncMock(side_effect=RuntimeError("session not found")),
):
result = await poll_bell_flag("my-session")
@@ -70,7 +70,7 @@ async def test_process_bell_flags_increments_unseen_count_on_new_bell():
state = empty_state()
state["sessions"]["session-a"] = {"bell": empty_bell()}
with patch("coordinator.bells.poll_bell_flag", new=AsyncMock(return_value=True)):
with patch("muxplex.bells.poll_bell_flag", new=AsyncMock(return_value=True)):
changed = await process_bell_flags(["session-a"], state)
assert changed is True
@@ -83,7 +83,7 @@ async def test_process_bell_flags_does_not_double_count_persistent_flag():
state = empty_state()
state["sessions"]["session-a"] = {"bell": empty_bell()}
with patch("coordinator.bells.poll_bell_flag", new=AsyncMock(return_value=True)):
with patch("muxplex.bells.poll_bell_flag", new=AsyncMock(return_value=True)):
# First poll — 0→1 transition
await process_bell_flags(["session-a"], state)
# Second poll — 1→1 (persistent), should NOT increment again
@@ -100,7 +100,7 @@ async def test_process_bell_flags_resets_tracking_when_flag_clears():
# side_effect drives three sequential calls: 0→1, 1→0, 0→1
with patch(
"coordinator.bells.poll_bell_flag",
"muxplex.bells.poll_bell_flag",
new=AsyncMock(side_effect=[True, False, True]),
):
for _ in range(3):
@@ -114,7 +114,7 @@ async def test_process_bell_flags_no_change_returns_false():
state = empty_state()
state["sessions"]["session-a"] = {"bell": empty_bell()}
with patch("coordinator.bells.poll_bell_flag", new=AsyncMock(return_value=False)):
with patch("muxplex.bells.poll_bell_flag", new=AsyncMock(return_value=False)):
changed = await process_bell_flags(["session-a"], state)
assert changed is False
@@ -126,7 +126,7 @@ async def test_process_bell_flags_creates_bell_entry_if_missing():
state = empty_state()
state["sessions"]["session-a"] = {} # no 'bell' key
with patch("coordinator.bells.poll_bell_flag", new=AsyncMock(return_value=False)):
with patch("muxplex.bells.poll_bell_flag", new=AsyncMock(return_value=False)):
await process_bell_flags(["session-a"], state)
assert "bell" in state["sessions"]["session-a"]
@@ -2,7 +2,7 @@
import pathlib
CSS_PATH = pathlib.Path(__file__).parent.parent.parent / "frontend" / "style.css"
CSS_PATH = pathlib.Path(__file__).parent.parent / "frontend" / "style.css"
def read_css() -> str:
@@ -4,7 +4,7 @@ import pathlib
from bs4 import BeautifulSoup, Tag
HTML_PATH = pathlib.Path(__file__).parent.parent.parent / "frontend" / "index.html"
HTML_PATH = pathlib.Path(__file__).parent.parent / "frontend" / "index.html"
# Parse once per module — tests are read-only so sharing is safe.
_SOUP: BeautifulSoup = BeautifulSoup(HTML_PATH.read_text(), "html.parser")
@@ -1,5 +1,5 @@
"""
Integration tests for the tmux-web coordinator.
Integration tests for the tmux-web muxplex.
These tests require a real tmux installation and spin up an isolated tmux
server on socket 'test-server' for the duration of the module.
@@ -18,10 +18,10 @@ from unittest.mock import patch
import pytest
import coordinator.state as state_mod
from coordinator.bells import poll_bell_flag
from coordinator.main import _run_poll_cycle
from coordinator.sessions import enumerate_sessions, get_snapshots
import muxplex.state as state_mod
from muxplex.bells import poll_bell_flag
from muxplex.main import _run_poll_cycle
from muxplex.sessions import enumerate_sessions, get_snapshots
# ---------------------------------------------------------------------------
@@ -87,13 +87,13 @@ def use_tmp_state(tmp_path, monkeypatch):
"""Redirect state and PID files to tmp_path for test isolation."""
tmp_state_dir = tmp_path / "state"
tmp_state_path = tmp_state_dir / "state.json"
monkeypatch.setattr("coordinator.state.STATE_DIR", tmp_state_dir)
monkeypatch.setattr("coordinator.state.STATE_PATH", tmp_state_path)
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("coordinator.ttyd.TTYD_PID_DIR", tmp_pid_dir)
monkeypatch.setattr("coordinator.ttyd.TTYD_PID_PATH", tmp_pid_path)
monkeypatch.setattr("muxplex.ttyd.TTYD_PID_DIR", tmp_pid_dir)
monkeypatch.setattr("muxplex.ttyd.TTYD_PID_PATH", tmp_pid_path)
# ---------------------------------------------------------------------------
@@ -134,7 +134,7 @@ def make_run_tmux_for_socket(socket: str):
async def test_enumerate_sessions_finds_test_session(tmux_server):
"""enumerate_sessions discovers the 'test' session on the isolated tmux server."""
patched_run_tmux = make_run_tmux_for_socket(tmux_server)
with patch("coordinator.sessions.run_tmux", side_effect=patched_run_tmux):
with patch("muxplex.sessions.run_tmux", side_effect=patched_run_tmux):
sessions = await enumerate_sessions()
assert "test" in sessions
@@ -158,7 +158,7 @@ async def test_bell_flag_detected_after_printf_bell(tmux_server):
await asyncio.sleep(1.0)
patched_run_tmux = make_run_tmux_for_socket(tmux_server)
with patch("coordinator.bells.run_tmux", side_effect=patched_run_tmux):
with patch("muxplex.bells.run_tmux", side_effect=patched_run_tmux):
result = await poll_bell_flag("test")
assert result is True
@@ -169,8 +169,8 @@ async def test_full_poll_cycle_via_api(tmux_server):
and populates the in-memory snapshot cache with non-empty content."""
patched_run_tmux = make_run_tmux_for_socket(tmux_server)
with (
patch("coordinator.sessions.run_tmux", side_effect=patched_run_tmux),
patch("coordinator.bells.run_tmux", side_effect=patched_run_tmux),
patch("muxplex.sessions.run_tmux", side_effect=patched_run_tmux),
patch("muxplex.bells.run_tmux", side_effect=patched_run_tmux),
):
await _run_poll_cycle()
@@ -191,8 +191,8 @@ async def test_state_file_written_atomically_by_poll_cycle(tmux_server):
"""After _run_poll_cycle, state.json exists, no .tmp file remains, content is valid JSON."""
patched_run_tmux = make_run_tmux_for_socket(tmux_server)
with (
patch("coordinator.sessions.run_tmux", side_effect=patched_run_tmux),
patch("coordinator.bells.run_tmux", side_effect=patched_run_tmux),
patch("muxplex.sessions.run_tmux", side_effect=patched_run_tmux),
patch("muxplex.bells.run_tmux", side_effect=patched_run_tmux),
):
await _run_poll_cycle()
@@ -7,8 +7,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import coordinator.sessions as sessions_mod
from coordinator.sessions import (
import muxplex.sessions as sessions_mod
from muxplex.sessions import (
capture_pane,
enumerate_sessions,
get_snapshots,
@@ -169,7 +169,7 @@ async def test_snapshot_all_returns_dict_keyed_by_name():
async def mock_capture(name, lines=30):
return f"output-for-{name}"
with patch("coordinator.sessions.capture_pane", side_effect=mock_capture):
with patch("muxplex.sessions.capture_pane", side_effect=mock_capture):
result = await snapshot_all(["alpha", "beta", "gamma"])
assert result == {
@@ -181,7 +181,7 @@ async def test_snapshot_all_returns_dict_keyed_by_name():
async def test_snapshot_all_returns_empty_dict_for_empty_input():
"""snapshot_all([]) returns an empty dict without calling capture_pane."""
with patch("coordinator.sessions.capture_pane", new=AsyncMock()) as mock_capture:
with patch("muxplex.sessions.capture_pane", new=AsyncMock()) as mock_capture:
result = await snapshot_all([])
assert result == {}
@@ -196,7 +196,7 @@ async def test_snapshot_all_returns_empty_string_on_individual_failure():
raise RuntimeError("pane not found")
return f"output-for-{name}"
with patch("coordinator.sessions.capture_pane", side_effect=mock_capture):
with patch("muxplex.sessions.capture_pane", side_effect=mock_capture):
result = await snapshot_all(["session-a", "bad-session", "session-b"])
assert result == {
@@ -9,7 +9,7 @@ from pathlib import Path
import pytest
from coordinator.state import (
from muxplex.state import (
empty_bell,
empty_device,
empty_state,
@@ -31,8 +31,8 @@ def use_tmp_state_dir(tmp_path, monkeypatch):
"""Redirect state I/O to a fresh temp directory for every test."""
tmp_state_dir = tmp_path / "state"
tmp_state_path = tmp_state_dir / "state.json"
monkeypatch.setattr("coordinator.state.STATE_DIR", tmp_state_dir)
monkeypatch.setattr("coordinator.state.STATE_PATH", tmp_state_path)
monkeypatch.setattr("muxplex.state.STATE_DIR", tmp_state_dir)
monkeypatch.setattr("muxplex.state.STATE_PATH", tmp_state_path)
# ---------------------------------------------------------------------------
@@ -180,7 +180,7 @@ async def test_write_then_read_roundtrip():
async def test_write_creates_state_dir_if_missing():
"""save_state() must create STATE_DIR (and parents) if they do not exist."""
import coordinator.state as state_mod
import muxplex.state as state_mod
# The tmp "state" subdirectory should not exist yet.
assert not state_mod.STATE_DIR.exists()
@@ -190,7 +190,7 @@ async def test_write_creates_state_dir_if_missing():
async def test_write_is_atomic_no_tmp_file_left():
"""After write_state(), no .tmp file should remain on disk."""
import coordinator.state as state_mod
import muxplex.state as state_mod
await write_state(empty_state())
tmp_file = Path(str(state_mod.STATE_PATH) + ".tmp")
@@ -8,8 +8,8 @@ from unittest.mock import AsyncMock, MagicMock, patch
import pytest
import coordinator.ttyd as ttyd_mod
from coordinator.ttyd import kill_orphan_ttyd, kill_ttyd, spawn_ttyd
import muxplex.ttyd as ttyd_mod
from muxplex.ttyd import kill_orphan_ttyd, kill_ttyd, spawn_ttyd
# ---------------------------------------------------------------------------
@@ -22,8 +22,8 @@ def use_tmp_pid_dir(tmp_path, monkeypatch):
"""Redirect PID file I/O to a fresh temp directory for every test."""
tmp_pid_dir = tmp_path / "ttyd"
tmp_pid_path = tmp_pid_dir / "ttyd.pid"
monkeypatch.setattr("coordinator.ttyd.TTYD_PID_DIR", tmp_pid_dir)
monkeypatch.setattr("coordinator.ttyd.TTYD_PID_PATH", tmp_pid_path)
monkeypatch.setattr("muxplex.ttyd.TTYD_PID_DIR", tmp_pid_dir)
monkeypatch.setattr("muxplex.ttyd.TTYD_PID_PATH", tmp_pid_path)
# ---------------------------------------------------------------------------
+1 -1
View File
@@ -1,5 +1,5 @@
"""
ttyd process lifecycle management for the tmux-web coordinator.
ttyd process lifecycle management for the tmux-web muxplex.
Constants:
TTYD_PID_DIR directory for the PID file (default: ~/.local/share/tmux-web/)
+1 -1
View File
@@ -8,7 +8,7 @@ version = "0.1.0"
requires-python = ">=3.12"
[tool.pytest.ini_options]
testpaths = ["coordinator/tests"]
testpaths = ["muxplex/tests"]
asyncio_mode = "auto"
addopts = "--import-mode=importlib -m 'not integration'"
markers = [