From 74b63033d7226e3796c357f52675e75b76a810f1 Mon Sep 17 00:00:00 2001 From: Brian Krabach Date: Sat, 28 Mar 2026 02:02:50 -0700 Subject: [PATCH] =?UTF-8?q?refactor:=20rename=20coordinator=20=E2=86=92=20?= =?UTF-8?q?muxplex=20package,=20move=20frontend=20inside=20as=20package=20?= =?UTF-8?q?data?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- {coordinator => muxplex}/__init__.py | 0 {coordinator => muxplex}/bells.py | 6 +- {frontend => muxplex/frontend}/app.js | 0 .../frontend}/apple-touch-icon.png | Bin {frontend => muxplex/frontend}/favicon-32.png | Bin {frontend => muxplex/frontend}/favicon.ico | Bin {frontend => muxplex/frontend}/index.html | 0 {frontend => muxplex/frontend}/manifest.json | 0 {frontend => muxplex/frontend}/pwa-192.png | Bin {frontend => muxplex/frontend}/pwa-512.png | Bin {frontend => muxplex/frontend}/style.css | 0 {frontend => muxplex/frontend}/terminal.js | 0 .../frontend}/tests/test_app.mjs | 0 .../frontend}/tests/test_terminal.mjs | 0 .../frontend}/wordmark-on-dark.svg | 0 {coordinator => muxplex}/main.py | 10 +-- {coordinator => muxplex}/sessions.py | 2 +- {coordinator => muxplex}/state.py | 2 +- {coordinator => muxplex}/tests/__init__.py | 0 {coordinator => muxplex}/tests/test_api.py | 84 +++++++++--------- {coordinator => muxplex}/tests/test_bells.py | 20 ++--- .../tests/test_frontend_css.py | 2 +- .../tests/test_frontend_html.py | 2 +- .../tests/test_integration.py | 30 +++---- .../tests/test_sessions.py | 10 +-- {coordinator => muxplex}/tests/test_state.py | 10 +-- {coordinator => muxplex}/tests/test_ttyd.py | 8 +- {coordinator => muxplex}/ttyd.py | 2 +- pyproject.toml | 2 +- {coordinator => scripts}/spike_bell_flag.py | 0 30 files changed, 95 insertions(+), 95 deletions(-) rename {coordinator => muxplex}/__init__.py (100%) rename {coordinator => muxplex}/bells.py (97%) rename {frontend => muxplex/frontend}/app.js (100%) rename {frontend => muxplex/frontend}/apple-touch-icon.png (100%) rename {frontend => muxplex/frontend}/favicon-32.png (100%) rename {frontend => muxplex/frontend}/favicon.ico (100%) rename {frontend => muxplex/frontend}/index.html (100%) rename {frontend => muxplex/frontend}/manifest.json (100%) rename {frontend => muxplex/frontend}/pwa-192.png (100%) rename {frontend => muxplex/frontend}/pwa-512.png (100%) rename {frontend => muxplex/frontend}/style.css (100%) rename {frontend => muxplex/frontend}/terminal.js (100%) rename {frontend => muxplex/frontend}/tests/test_app.mjs (100%) rename {frontend => muxplex/frontend}/tests/test_terminal.mjs (100%) rename {frontend => muxplex/frontend}/wordmark-on-dark.svg (100%) rename {coordinator => muxplex}/main.py (97%) rename {coordinator => muxplex}/sessions.py (98%) rename {coordinator => muxplex}/state.py (98%) rename {coordinator => muxplex}/tests/__init__.py (100%) rename {coordinator => muxplex}/tests/test_api.py (87%) rename {coordinator => muxplex}/tests/test_bells.py (94%) rename {coordinator => muxplex}/tests/test_frontend_css.py (99%) rename {coordinator => muxplex}/tests/test_frontend_html.py (99%) rename {coordinator => muxplex}/tests/test_integration.py (86%) rename {coordinator => muxplex}/tests/test_sessions.py (96%) rename {coordinator => muxplex}/tests/test_state.py (97%) rename {coordinator => muxplex}/tests/test_ttyd.py (96%) rename {coordinator => muxplex}/ttyd.py (98%) rename {coordinator => scripts}/spike_bell_flag.py (100%) diff --git a/coordinator/__init__.py b/muxplex/__init__.py similarity index 100% rename from coordinator/__init__.py rename to muxplex/__init__.py diff --git a/coordinator/bells.py b/muxplex/bells.py similarity index 97% rename from coordinator/bells.py rename to muxplex/bells.py index 723ede4..36098b2 100644 --- a/coordinator/bells.py +++ b/muxplex/bells.py @@ -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?) diff --git a/frontend/app.js b/muxplex/frontend/app.js similarity index 100% rename from frontend/app.js rename to muxplex/frontend/app.js diff --git a/frontend/apple-touch-icon.png b/muxplex/frontend/apple-touch-icon.png similarity index 100% rename from frontend/apple-touch-icon.png rename to muxplex/frontend/apple-touch-icon.png diff --git a/frontend/favicon-32.png b/muxplex/frontend/favicon-32.png similarity index 100% rename from frontend/favicon-32.png rename to muxplex/frontend/favicon-32.png diff --git a/frontend/favicon.ico b/muxplex/frontend/favicon.ico similarity index 100% rename from frontend/favicon.ico rename to muxplex/frontend/favicon.ico diff --git a/frontend/index.html b/muxplex/frontend/index.html similarity index 100% rename from frontend/index.html rename to muxplex/frontend/index.html diff --git a/frontend/manifest.json b/muxplex/frontend/manifest.json similarity index 100% rename from frontend/manifest.json rename to muxplex/frontend/manifest.json diff --git a/frontend/pwa-192.png b/muxplex/frontend/pwa-192.png similarity index 100% rename from frontend/pwa-192.png rename to muxplex/frontend/pwa-192.png diff --git a/frontend/pwa-512.png b/muxplex/frontend/pwa-512.png similarity index 100% rename from frontend/pwa-512.png rename to muxplex/frontend/pwa-512.png diff --git a/frontend/style.css b/muxplex/frontend/style.css similarity index 100% rename from frontend/style.css rename to muxplex/frontend/style.css diff --git a/frontend/terminal.js b/muxplex/frontend/terminal.js similarity index 100% rename from frontend/terminal.js rename to muxplex/frontend/terminal.js diff --git a/frontend/tests/test_app.mjs b/muxplex/frontend/tests/test_app.mjs similarity index 100% rename from frontend/tests/test_app.mjs rename to muxplex/frontend/tests/test_app.mjs diff --git a/frontend/tests/test_terminal.mjs b/muxplex/frontend/tests/test_terminal.mjs similarity index 100% rename from frontend/tests/test_terminal.mjs rename to muxplex/frontend/tests/test_terminal.mjs diff --git a/frontend/wordmark-on-dark.svg b/muxplex/frontend/wordmark-on-dark.svg similarity index 100% rename from frontend/wordmark-on-dark.svg rename to muxplex/frontend/wordmark-on-dark.svg diff --git a/coordinator/main.py b/muxplex/main.py similarity index 97% rename from coordinator/main.py rename to muxplex/main.py index bd9825a..f10eb94 100644 --- a/coordinator/main.py +++ b/muxplex/main.py @@ -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") diff --git a/coordinator/sessions.py b/muxplex/sessions.py similarity index 98% rename from coordinator/sessions.py rename to muxplex/sessions.py index 40d73a8..3df4f2d 100644 --- a/coordinator/sessions.py +++ b/muxplex/sessions.py @@ -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. diff --git a/coordinator/state.py b/muxplex/state.py similarity index 98% rename from coordinator/state.py rename to muxplex/state.py index f93fea9..56ca97b 100644 --- a/coordinator/state.py +++ b/muxplex/state.py @@ -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): diff --git a/coordinator/tests/__init__.py b/muxplex/tests/__init__.py similarity index 100% rename from coordinator/tests/__init__.py rename to muxplex/tests/__init__.py diff --git a/coordinator/tests/test_api.py b/muxplex/tests/test_api.py similarity index 87% rename from coordinator/tests/test_api.py rename to muxplex/tests/test_api.py index c32399e..9bc5548 100644 --- a/coordinator/tests/test_api.py +++ b/muxplex/tests/test_api.py @@ -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 _: diff --git a/coordinator/tests/test_bells.py b/muxplex/tests/test_bells.py similarity index 94% rename from coordinator/tests/test_bells.py rename to muxplex/tests/test_bells.py index cfb6030..9d49c0d 100644 --- a/coordinator/tests/test_bells.py +++ b/muxplex/tests/test_bells.py @@ -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"] diff --git a/coordinator/tests/test_frontend_css.py b/muxplex/tests/test_frontend_css.py similarity index 99% rename from coordinator/tests/test_frontend_css.py rename to muxplex/tests/test_frontend_css.py index 40ed1f8..74702a7 100644 --- a/coordinator/tests/test_frontend_css.py +++ b/muxplex/tests/test_frontend_css.py @@ -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: diff --git a/coordinator/tests/test_frontend_html.py b/muxplex/tests/test_frontend_html.py similarity index 99% rename from coordinator/tests/test_frontend_html.py rename to muxplex/tests/test_frontend_html.py index 73f9739..bc458cc 100644 --- a/coordinator/tests/test_frontend_html.py +++ b/muxplex/tests/test_frontend_html.py @@ -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") diff --git a/coordinator/tests/test_integration.py b/muxplex/tests/test_integration.py similarity index 86% rename from coordinator/tests/test_integration.py rename to muxplex/tests/test_integration.py index d9bb5b5..b9e391c 100644 --- a/coordinator/tests/test_integration.py +++ b/muxplex/tests/test_integration.py @@ -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() diff --git a/coordinator/tests/test_sessions.py b/muxplex/tests/test_sessions.py similarity index 96% rename from coordinator/tests/test_sessions.py rename to muxplex/tests/test_sessions.py index 3a6936a..a52e82a 100644 --- a/coordinator/tests/test_sessions.py +++ b/muxplex/tests/test_sessions.py @@ -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 == { diff --git a/coordinator/tests/test_state.py b/muxplex/tests/test_state.py similarity index 97% rename from coordinator/tests/test_state.py rename to muxplex/tests/test_state.py index d23697f..e5c502a 100644 --- a/coordinator/tests/test_state.py +++ b/muxplex/tests/test_state.py @@ -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") diff --git a/coordinator/tests/test_ttyd.py b/muxplex/tests/test_ttyd.py similarity index 96% rename from coordinator/tests/test_ttyd.py rename to muxplex/tests/test_ttyd.py index 64f3de4..deeeb03 100644 --- a/coordinator/tests/test_ttyd.py +++ b/muxplex/tests/test_ttyd.py @@ -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) # --------------------------------------------------------------------------- diff --git a/coordinator/ttyd.py b/muxplex/ttyd.py similarity index 98% rename from coordinator/ttyd.py rename to muxplex/ttyd.py index 0a372da..41b4ec2 100644 --- a/coordinator/ttyd.py +++ b/muxplex/ttyd.py @@ -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/) diff --git a/pyproject.toml b/pyproject.toml index 231248a..1dacc53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 = [ diff --git a/coordinator/spike_bell_flag.py b/scripts/spike_bell_flag.py similarity index 100% rename from coordinator/spike_bell_flag.py rename to scripts/spike_bell_flag.py