diff --git a/tests/dashboard/test_tower.py b/tests/dashboard/test_tower.py new file mode 100644 index 0000000..b9cc7e3 --- /dev/null +++ b/tests/dashboard/test_tower.py @@ -0,0 +1,187 @@ +"""Tests for Tower dashboard route (/tower).""" + +from unittest.mock import MagicMock, patch + + +def _mock_spark_engine(): + """Return a mock spark_engine with realistic return values.""" + engine = MagicMock() + + engine.status.return_value = { + "enabled": True, + "events_captured": 5, + "memories_stored": 3, + "predictions": {"total": 2, "avg_accuracy": 0.85}, + "event_types": { + "task_posted": 2, + "bid_submitted": 1, + "task_assigned": 1, + "task_completed": 1, + "task_failed": 0, + "agent_joined": 0, + "tool_executed": 0, + "creative_step": 0, + }, + } + + event = MagicMock() + event.event_type = "task_completed" + event.description = "Task finished" + event.importance = 0.8 + event.created_at = "2026-01-01T00:00:00" + event.agent_id = "agent-1234-abcd" + event.task_id = "task-5678-efgh" + event.data = '{"result": "ok"}' + engine.get_timeline.return_value = [event] + + pred = MagicMock() + pred.task_id = "task-5678-efgh" + pred.accuracy = 0.9 + pred.evaluated_at = "2026-01-01T01:00:00" + pred.created_at = "2026-01-01T00:30:00" + pred.predicted_value = '{"outcome": "success"}' + engine.get_predictions.return_value = [pred] + + advisory = MagicMock() + advisory.category = "performance" + advisory.priority = "high" + advisory.title = "Slow tasks" + advisory.detail = "Tasks taking longer than expected" + advisory.suggested_action = "Scale up workers" + engine.get_advisories.return_value = [advisory] + + return engine + + +class TestTowerUI: + """Tests for GET /tower endpoint.""" + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + def test_tower_returns_200(self, mock_engine, client): + response = client.get("/tower") + assert response.status_code == 200 + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + def test_tower_returns_html(self, mock_engine, client): + response = client.get("/tower") + assert "text/html" in response.headers["content-type"] + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + def test_tower_contains_dashboard_content(self, mock_engine, client): + response = client.get("/tower") + body = response.text + assert "tower" in body.lower() or "spark" in body.lower() + + +class TestSparkSnapshot: + """Tests for _spark_snapshot helper.""" + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + def test_snapshot_structure(self, mock_engine): + from dashboard.routes.tower import _spark_snapshot + + snap = _spark_snapshot() + assert snap["type"] == "spark_state" + assert "status" in snap + assert "events" in snap + assert "predictions" in snap + assert "advisories" in snap + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + def test_snapshot_events_parsed(self, mock_engine): + from dashboard.routes.tower import _spark_snapshot + + snap = _spark_snapshot() + ev = snap["events"][0] + assert ev["event_type"] == "task_completed" + assert ev["importance"] == 0.8 + assert ev["agent_id"] == "agent-12" + assert ev["task_id"] == "task-567" + assert ev["data"] == {"result": "ok"} + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + def test_snapshot_predictions_parsed(self, mock_engine): + from dashboard.routes.tower import _spark_snapshot + + snap = _spark_snapshot() + pred = snap["predictions"][0] + assert pred["task_id"] == "task-567" + assert pred["accuracy"] == 0.9 + assert pred["evaluated"] is True + assert pred["predicted"] == {"outcome": "success"} + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + def test_snapshot_advisories_parsed(self, mock_engine): + from dashboard.routes.tower import _spark_snapshot + + snap = _spark_snapshot() + adv = snap["advisories"][0] + assert adv["category"] == "performance" + assert adv["priority"] == "high" + assert adv["title"] == "Slow tasks" + assert adv["suggested_action"] == "Scale up workers" + + @patch("dashboard.routes.tower.spark_engine") + def test_snapshot_handles_empty_state(self, mock_engine): + mock_engine.status.return_value = {"enabled": False} + mock_engine.get_timeline.return_value = [] + mock_engine.get_predictions.return_value = [] + mock_engine.get_advisories.return_value = [] + + from dashboard.routes.tower import _spark_snapshot + + snap = _spark_snapshot() + assert snap["events"] == [] + assert snap["predictions"] == [] + assert snap["advisories"] == [] + + @patch("dashboard.routes.tower.spark_engine") + def test_snapshot_handles_invalid_json_data(self, mock_engine): + mock_engine.status.return_value = {"enabled": True} + + event = MagicMock() + event.event_type = "test" + event.description = "bad data" + event.importance = 0.5 + event.created_at = "2026-01-01T00:00:00" + event.agent_id = None + event.task_id = None + event.data = "not-json{" + mock_engine.get_timeline.return_value = [event] + + pred = MagicMock() + pred.task_id = None + pred.accuracy = None + pred.evaluated_at = None + pred.created_at = "2026-01-01T00:00:00" + pred.predicted_value = None + mock_engine.get_predictions.return_value = [pred] + + mock_engine.get_advisories.return_value = [] + + from dashboard.routes.tower import _spark_snapshot + + snap = _spark_snapshot() + ev = snap["events"][0] + assert ev["data"] == {} + assert "agent_id" not in ev + assert "task_id" not in ev + + pred = snap["predictions"][0] + assert pred["task_id"] == "?" + assert pred["predicted"] == {} + + +class TestTowerWebSocket: + """Tests for WS /tower/ws endpoint.""" + + @patch("dashboard.routes.tower.spark_engine", new_callable=_mock_spark_engine) + @patch("dashboard.routes.tower._PUSH_INTERVAL", 0) + def test_ws_sends_initial_snapshot(self, mock_engine, client): + import json + + with client.websocket_connect("/tower/ws") as ws: + data = json.loads(ws.receive_text()) + assert data["type"] == "spark_state" + assert "status" in data + assert "events" in data