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