refactor: restructure project into muxplex/ subdir with brand integration
- Move coordinator/, frontend/, Caddyfile, pyproject.toml, requirements.txt, docs/ into muxplex/ subdir in prep for packaging/sharing - Add brand assets to frontend/: favicon.ico, pwa-192/512.png, apple-touch-icon.png, wordmark-on-dark.svg - Update app: title → muxplex, header → wordmark SVG, brand color tokens in style.css, manifest.json updated with muxplex name and brand icons - Add design system: assets/branding/tokens.css (101 CSS custom properties), tokens.json (127 tokens), DESIGN-SYSTEM.md (856-line spec) - Add assets/branding/: SVG sources, rendered PNGs (icons, favicons, PWA, OG) - Add scripts/render-brand-assets.py for reproducible PNG generation - Add muxplex/README.md
This commit is contained in:
@@ -0,0 +1,318 @@
|
||||
"""
|
||||
Tests for coordinator/bells.py — bell flag polling and unseen_count tracking.
|
||||
All 17 acceptance-criteria tests are defined here.
|
||||
"""
|
||||
|
||||
import time
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from coordinator.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
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# autouse fixture — clear _bell_seen before/after each test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def reset_bell_seen():
|
||||
"""Clear _bell_seen before and after each test for isolation."""
|
||||
_bell_seen.clear()
|
||||
yield
|
||||
_bell_seen.clear()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# poll_bell_flag tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
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")):
|
||||
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")):
|
||||
result = await poll_bell_flag("my-session")
|
||||
assert result is False
|
||||
|
||||
|
||||
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",
|
||||
new=AsyncMock(side_effect=RuntimeError("session not found")),
|
||||
):
|
||||
result = await poll_bell_flag("my-session")
|
||||
assert result is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# process_bell_flags tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def test_process_bell_flags_increments_unseen_count_on_new_bell():
|
||||
"""process_bell_flags increments unseen_count on a 0→1 transition."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
|
||||
with patch("coordinator.bells.poll_bell_flag", new=AsyncMock(return_value=True)):
|
||||
changed = await process_bell_flags(["session-a"], state)
|
||||
|
||||
assert changed is True
|
||||
assert state["sessions"]["session-a"]["bell"]["unseen_count"] == 1
|
||||
assert state["sessions"]["session-a"]["bell"]["last_fired_at"] is not None
|
||||
|
||||
|
||||
async def test_process_bell_flags_does_not_double_count_persistent_flag():
|
||||
"""process_bell_flags does not increment unseen_count if flag stays at 1."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
|
||||
with patch("coordinator.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
|
||||
changed = await process_bell_flags(["session-a"], state)
|
||||
|
||||
assert changed is False
|
||||
assert state["sessions"]["session-a"]["bell"]["unseen_count"] == 1
|
||||
|
||||
|
||||
async def test_process_bell_flags_resets_tracking_when_flag_clears():
|
||||
"""1→0→1 sequence counts as two separate bells."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
|
||||
# side_effect drives three sequential calls: 0→1, 1→0, 0→1
|
||||
with patch(
|
||||
"coordinator.bells.poll_bell_flag",
|
||||
new=AsyncMock(side_effect=[True, False, True]),
|
||||
):
|
||||
for _ in range(3):
|
||||
await process_bell_flags(["session-a"], state)
|
||||
|
||||
assert state["sessions"]["session-a"]["bell"]["unseen_count"] == 2
|
||||
|
||||
|
||||
async def test_process_bell_flags_no_change_returns_false():
|
||||
"""process_bell_flags returns False when no bell state changed."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
|
||||
with patch("coordinator.bells.poll_bell_flag", new=AsyncMock(return_value=False)):
|
||||
changed = await process_bell_flags(["session-a"], state)
|
||||
|
||||
assert changed is False
|
||||
assert state["sessions"]["session-a"]["bell"]["unseen_count"] == 0
|
||||
|
||||
|
||||
async def test_process_bell_flags_creates_bell_entry_if_missing():
|
||||
"""process_bell_flags creates the bell sub-dict if session has no bell key."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {} # no 'bell' key
|
||||
|
||||
with patch("coordinator.bells.poll_bell_flag", new=AsyncMock(return_value=False)):
|
||||
await process_bell_flags(["session-a"], state)
|
||||
|
||||
assert "bell" in state["sessions"]["session-a"]
|
||||
assert state["sessions"]["session-a"]["bell"]["unseen_count"] == 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# should_clear_bell tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_should_clear_bell_returns_true_for_fullscreen_recent_interaction():
|
||||
"""should_clear_bell returns True when a device is fullscreen and interacted recently."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-a",
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 10.0, # 10 seconds ago
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
assert should_clear_bell("session-a", state) is True
|
||||
|
||||
|
||||
def test_should_clear_bell_returns_false_for_grid_mode():
|
||||
"""should_clear_bell returns False when device is in grid mode."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-a",
|
||||
"view_mode": "grid",
|
||||
"last_interaction_at": time.time() - 10.0, # recent interaction
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
assert should_clear_bell("session-a", state) is False
|
||||
|
||||
|
||||
def test_should_clear_bell_returns_false_when_interaction_too_old():
|
||||
"""should_clear_bell returns False when last interaction was more than 60s ago."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-a",
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 90.0, # 90 seconds ago (> 60s window)
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
assert should_clear_bell("session-a", state) is False
|
||||
|
||||
|
||||
def test_should_clear_bell_returns_false_when_device_viewing_different_session():
|
||||
"""should_clear_bell returns False when device is viewing a different session."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-b", # different session
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 10.0,
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
assert should_clear_bell("session-a", state) is False
|
||||
|
||||
|
||||
def test_should_clear_bell_returns_false_when_no_devices():
|
||||
"""should_clear_bell returns False when there are no connected devices."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {"bell": empty_bell()}
|
||||
# No devices in state["devices"]
|
||||
|
||||
assert should_clear_bell("session-a", state) is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# apply_bell_clear_rule tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_apply_bell_clear_rule_clears_matching_sessions():
|
||||
"""apply_bell_clear_rule resets unseen_count to 0 and sets seen_at for qualifying sessions."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {
|
||||
"bell": {
|
||||
"unseen_count": 3,
|
||||
"last_fired_at": time.time() - 30.0,
|
||||
"seen_at": None,
|
||||
}
|
||||
}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-a",
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 10.0,
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
before = time.time()
|
||||
apply_bell_clear_rule(state)
|
||||
after = time.time()
|
||||
|
||||
bell = state["sessions"]["session-a"]["bell"]
|
||||
assert bell["unseen_count"] == 0
|
||||
assert bell["seen_at"] is not None
|
||||
assert before <= bell["seen_at"] <= after
|
||||
|
||||
|
||||
def test_apply_bell_clear_rule_skips_sessions_with_zero_unseen():
|
||||
"""apply_bell_clear_rule does not modify sessions that already have unseen_count == 0."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {
|
||||
"bell": {
|
||||
"unseen_count": 0,
|
||||
"last_fired_at": None,
|
||||
"seen_at": None,
|
||||
}
|
||||
}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-a",
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 10.0,
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
result = apply_bell_clear_rule(state)
|
||||
|
||||
assert result == []
|
||||
assert state["sessions"]["session-a"]["bell"]["seen_at"] is None
|
||||
|
||||
|
||||
def test_apply_bell_clear_rule_returns_list_of_cleared_session_names():
|
||||
"""apply_bell_clear_rule returns the names of sessions that were cleared."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {
|
||||
"bell": {"unseen_count": 2, "last_fired_at": time.time() - 5.0, "seen_at": None}
|
||||
}
|
||||
state["sessions"]["session-b"] = {
|
||||
"bell": {"unseen_count": 1, "last_fired_at": time.time() - 5.0, "seen_at": None}
|
||||
}
|
||||
state["sessions"]["session-c"] = {
|
||||
"bell": {"unseen_count": 0, "last_fired_at": None, "seen_at": None}
|
||||
}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-a",
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 10.0,
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
state["devices"]["device-2"] = {
|
||||
"label": "Device 2",
|
||||
"viewing_session": "session-b",
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 10.0,
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
result = apply_bell_clear_rule(state)
|
||||
|
||||
assert sorted(result) == ["session-a", "session-b"]
|
||||
|
||||
|
||||
def test_apply_bell_clear_rule_resets_bell_seen_tracking():
|
||||
"""apply_bell_clear_rule resets _bell_seen[name] = False for cleared sessions."""
|
||||
state = empty_state()
|
||||
state["sessions"]["session-a"] = {
|
||||
"bell": {"unseen_count": 1, "last_fired_at": time.time() - 5.0, "seen_at": None}
|
||||
}
|
||||
state["devices"]["device-1"] = {
|
||||
"label": "Device 1",
|
||||
"viewing_session": "session-a",
|
||||
"view_mode": "fullscreen",
|
||||
"last_interaction_at": time.time() - 10.0,
|
||||
"last_heartbeat_at": time.time(),
|
||||
}
|
||||
|
||||
# Pre-seed _bell_seen as if the bell was previously seen
|
||||
_bell_seen["session-a"] = True
|
||||
|
||||
apply_bell_clear_rule(state)
|
||||
|
||||
assert _bell_seen.get("session-a") is False
|
||||
Reference in New Issue
Block a user