forked from Rockachopa/Timmy-time-dashboard
* polish: streamline nav, extract inline styles, improve tablet UX - Restructure desktop nav from 8+ flat links + overflow dropdown into 5 grouped dropdowns (Core, Agents, Intel, System, More) matching the mobile menu structure to reduce decision fatigue - Extract all inline styles from mission_control.html and base.html notification elements into mission-control.css with semantic classes - Replace JS-built innerHTML with secure DOM construction in notification loader and chat history - Add CONNECTING state to connection indicator (amber) instead of showing OFFLINE before WebSocket connects - Add tablet breakpoint (1024px) with larger touch targets for Apple Pencil / stylus use and safe-area padding for iPad toolbar - Add active-link highlighting in desktop dropdown menus - Rename "Mission Control" page title to "System Overview" to disambiguate from the chat home page - Add "Home — Timmy Time" page title to index.html https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * fix(security): move auth-gate credentials to environment variables Hardcoded username, password, and HMAC secret in auth-gate.py replaced with os.environ lookups. Startup now refuses to run if any variable is unset. Added AUTH_GATE_SECRET/USER/PASS to .env.example. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h * refactor(tooling): migrate from black+isort+bandit to ruff Replace three separate linting/formatting tools with a single ruff invocation. Updates tox.ini (lint, format, pre-push, pre-commit envs), .pre-commit-config.yaml, and CI workflow. Fixes all ruff errors including unused imports, missing raise-from, and undefined names. Ruff config maps existing bandit skips to equivalent S-rules. https://claude.ai/code/session_015uPUoKyYa8M2UAcyk5Gt6h --------- Co-authored-by: Claude <noreply@anthropic.com>
192 lines
6.2 KiB
Python
192 lines
6.2 KiB
Python
"""Tests for the event broadcaster (infrastructure.events.broadcaster)."""
|
|
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
from infrastructure.events.broadcaster import (
|
|
EVENT_ICONS,
|
|
EVENT_LABELS,
|
|
EventBroadcaster,
|
|
format_event_for_display,
|
|
get_event_icon,
|
|
get_event_label,
|
|
)
|
|
|
|
# ── Fake EventLogEntry for testing ──────────────────────────────────────────
|
|
|
|
|
|
class FakeEventType(Enum):
|
|
TASK_CREATED = "task.created"
|
|
TASK_ASSIGNED = "task.assigned"
|
|
BID_SUBMITTED = "bid.submitted"
|
|
AGENT_JOINED = "agent.joined"
|
|
SYSTEM_INFO = "system.info"
|
|
|
|
|
|
@dataclass
|
|
class FakeEventLogEntry:
|
|
id: str = "evt-abc123"
|
|
event_type: FakeEventType = FakeEventType.TASK_CREATED
|
|
source: str = "test"
|
|
task_id: str = "task-1"
|
|
agent_id: str = "agent-1"
|
|
timestamp: str = "2026-03-06T12:00:00Z"
|
|
data: dict = None
|
|
|
|
def __post_init__(self):
|
|
if self.data is None:
|
|
self.data = {}
|
|
|
|
|
|
class TestEventBroadcaster:
|
|
"""Test EventBroadcaster class."""
|
|
|
|
def test_init(self):
|
|
b = EventBroadcaster()
|
|
assert b._ws_manager is None
|
|
|
|
async def test_broadcast_no_ws_manager(self):
|
|
b = EventBroadcaster()
|
|
# _get_ws_manager returns None => returns 0
|
|
count = await b.broadcast(FakeEventLogEntry())
|
|
assert count == 0
|
|
|
|
async def test_broadcast_with_ws_manager(self):
|
|
b = EventBroadcaster()
|
|
mock_ws = MagicMock()
|
|
mock_ws.broadcast_json = AsyncMock(return_value=3)
|
|
b._ws_manager = mock_ws
|
|
|
|
event = FakeEventLogEntry()
|
|
count = await b.broadcast(event)
|
|
assert count == 3
|
|
mock_ws.broadcast_json.assert_awaited_once()
|
|
|
|
# Verify payload structure
|
|
payload = mock_ws.broadcast_json.call_args[0][0]
|
|
assert payload["type"] == "event"
|
|
assert payload["payload"]["id"] == "evt-abc123"
|
|
assert payload["payload"]["event_type"] == "task.created"
|
|
|
|
async def test_broadcast_ws_error_returns_zero(self):
|
|
b = EventBroadcaster()
|
|
mock_ws = MagicMock()
|
|
mock_ws.broadcast_json = AsyncMock(side_effect=RuntimeError("ws down"))
|
|
b._ws_manager = mock_ws
|
|
|
|
count = await b.broadcast(FakeEventLogEntry())
|
|
assert count == 0
|
|
|
|
def test_broadcast_sync_no_loop(self):
|
|
"""broadcast_sync should not crash when no event loop is running."""
|
|
b = EventBroadcaster()
|
|
# This should silently pass (no event loop)
|
|
b.broadcast_sync(FakeEventLogEntry())
|
|
|
|
|
|
class TestEventIcons:
|
|
"""Test icon/label lookup functions."""
|
|
|
|
def test_known_icon(self):
|
|
assert get_event_icon("task.created") == "📝"
|
|
assert get_event_icon("agent.joined") == "🟢"
|
|
|
|
def test_unknown_icon_returns_bullet(self):
|
|
assert get_event_icon("nonexistent") == "•"
|
|
|
|
def test_known_label(self):
|
|
assert get_event_label("task.created") == "New task"
|
|
assert get_event_label("task.failed") == "Task failed"
|
|
|
|
def test_unknown_label_returns_type(self):
|
|
assert get_event_label("custom.event") == "custom.event"
|
|
|
|
def test_all_icons_have_labels(self):
|
|
"""Every icon key should also have a label."""
|
|
for key in EVENT_ICONS:
|
|
assert key in EVENT_LABELS, f"Missing label for icon key: {key}"
|
|
|
|
|
|
class TestFormatEventForDisplay:
|
|
"""Test format_event_for_display helper."""
|
|
|
|
def test_task_created_truncates_description(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.TASK_CREATED,
|
|
data={"description": "A" * 100},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert result["description"].endswith("...")
|
|
assert len(result["description"]) <= 63
|
|
|
|
def test_task_created_short_description(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.TASK_CREATED,
|
|
data={"description": "Short task"},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert result["description"] == "Short task"
|
|
|
|
def test_task_assigned(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.TASK_ASSIGNED,
|
|
agent_id="agent-12345678-long",
|
|
data={"bid_sats": 500},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert "agent-12" in result["description"]
|
|
assert "500 sats" in result["description"]
|
|
|
|
def test_bid_submitted(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.BID_SUBMITTED,
|
|
data={"bid_sats": 250},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert "250 sats" in result["description"]
|
|
|
|
def test_agent_joined_with_persona(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.AGENT_JOINED,
|
|
data={"persona_id": "forge"},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert "forge" in result["description"]
|
|
|
|
def test_agent_joined_no_persona(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.AGENT_JOINED,
|
|
data={},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert result["description"] == "New agent"
|
|
|
|
def test_generic_event_with_message(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.SYSTEM_INFO,
|
|
data={"message": "All systems go"},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert result["description"] == "All systems go"
|
|
|
|
def test_generic_event_no_data(self):
|
|
event = FakeEventLogEntry(
|
|
event_type=FakeEventType.SYSTEM_INFO,
|
|
data={},
|
|
)
|
|
result = format_event_for_display(event)
|
|
assert result["description"] == ""
|
|
|
|
def test_output_structure(self):
|
|
event = FakeEventLogEntry()
|
|
result = format_event_for_display(event)
|
|
assert "id" in result
|
|
assert "icon" in result
|
|
assert "label" in result
|
|
assert "type" in result
|
|
assert "source" in result
|
|
assert "timestamp" in result
|
|
assert "time_short" in result
|
|
assert result["time_short"] == "12:00:00"
|