diff --git a/dream-server/extensions/services/dashboard-api/tests/test_routers.py b/dream-server/extensions/services/dashboard-api/tests/test_routers.py index 2253dabf..53e732db 100644 --- a/dream-server/extensions/services/dashboard-api/tests/test_routers.py +++ b/dream-server/extensions/services/dashboard-api/tests/test_routers.py @@ -49,6 +49,24 @@ def test_workflows_requires_auth(test_client): assert resp.status_code == 401 +def test_agents_metrics_requires_auth(test_client): + """GET /api/agents/metrics without auth header → 401.""" + resp = test_client.get("/api/agents/metrics") + assert resp.status_code == 401 + + +def test_agents_cluster_requires_auth(test_client): + """GET /api/agents/cluster without auth header → 401.""" + resp = test_client.get("/api/agents/cluster") + assert resp.status_code == 401 + + +def test_agents_throughput_requires_auth(test_client): + """GET /api/agents/throughput without auth header → 401.""" + resp = test_client.get("/api/agents/throughput") + assert resp.status_code == 401 + + # --------------------------------------------------------------------------- # Setup router # --------------------------------------------------------------------------- @@ -309,3 +327,136 @@ def test_api_service_tokens_authenticated(test_client): assert resp.status_code == 200 data = resp.json() assert isinstance(data, dict) + + +# --------------------------------------------------------------------------- +# Agents router +# --------------------------------------------------------------------------- + + +def test_agents_metrics_authenticated(test_client): + """GET /api/agents/metrics with auth → 200, returns agent metrics with seeded data.""" + from agent_monitor import agent_metrics, throughput + + # Seed non-default values to test actual aggregation + agent_metrics.session_count = 5 + agent_metrics.tokens_per_second = 123.45 + throughput.add_sample(100.0) + throughput.add_sample(150.0) + + resp = test_client.get("/api/agents/metrics", headers=test_client.auth_headers) + assert resp.status_code == 200 + data = resp.json() + assert "agent" in data + assert "cluster" in data + assert "throughput" in data + + # Verify seeded values are reflected in response + assert data["agent"]["session_count"] == 5 + assert data["agent"]["tokens_per_second"] == 123.45 + assert data["throughput"]["current"] == 150.0 + assert data["throughput"]["peak"] == 150.0 + + +def test_agents_cluster_authenticated(test_client): + """GET /api/agents/cluster with auth → 200, returns cluster status with mocked data.""" + + async def _fake_subprocess(*args, **kwargs): + """Mock subprocess that returns a 2-node cluster with 1 healthy node.""" + proc = MagicMock() + cluster_response = b'{"nodes": [{"id": "node1", "healthy": true}, {"id": "node2", "healthy": false}]}' + proc.communicate = AsyncMock(return_value=(cluster_response, b"")) + proc.returncode = 0 + return proc + + with patch("asyncio.create_subprocess_exec", side_effect=_fake_subprocess): + resp = test_client.get("/api/agents/cluster", headers=test_client.auth_headers) + + assert resp.status_code == 200 + data = resp.json() + assert "nodes" in data + assert "total_gpus" in data + assert "active_gpus" in data + assert "failover_ready" in data + + # Verify parsed cluster data + assert data["total_gpus"] == 2 + assert data["active_gpus"] == 1 + assert data["failover_ready"] is False # Only 1 healthy node, need >1 for failover + + +def test_agents_cluster_failover_ready(test_client): + """GET /api/agents/cluster with 2 healthy nodes → failover_ready is True.""" + + async def _fake_subprocess(*args, **kwargs): + """Mock subprocess that returns a 2-node cluster with both nodes healthy.""" + proc = MagicMock() + cluster_response = b'{"nodes": [{"id": "node1", "healthy": true}, {"id": "node2", "healthy": true}]}' + proc.communicate = AsyncMock(return_value=(cluster_response, b"")) + proc.returncode = 0 + return proc + + with patch("asyncio.create_subprocess_exec", side_effect=_fake_subprocess): + resp = test_client.get("/api/agents/cluster", headers=test_client.auth_headers) + + assert resp.status_code == 200 + data = resp.json() + assert data["total_gpus"] == 2 + assert data["active_gpus"] == 2 + assert data["failover_ready"] is True # 2 healthy nodes enables failover + + +def test_agents_metrics_html_xss_escaping(test_client): + """GET /api/agents/metrics.html escapes HTML special chars to prevent XSS.""" + from agent_monitor import agent_metrics, throughput + + # Inject XSS payload into agent metrics + agent_metrics.session_count = 999 + throughput.add_sample(42.0) + + # Mock cluster status with XSS payload in node data + async def _fake_subprocess(*args, **kwargs): + proc = MagicMock() + # Node ID contains script tag + cluster_response = b'{"nodes": [{"id": "", "healthy": true}]}' + proc.communicate = AsyncMock(return_value=(cluster_response, b"")) + proc.returncode = 0 + return proc + + with patch("asyncio.create_subprocess_exec", side_effect=_fake_subprocess): + resp = test_client.get("/api/agents/metrics.html", headers=test_client.auth_headers) + + assert resp.status_code == 200 + html_content = resp.text + + # Verify HTML special chars are escaped + assert "