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 "