"""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