diff --git a/tests/test_admin_observability_routes.py b/tests/test_admin_observability_routes.py index 2c7e66c..f2f968d 100644 --- a/tests/test_admin_observability_routes.py +++ b/tests/test_admin_observability_routes.py @@ -201,22 +201,95 @@ def test_host_health_200(client, monkeypatch): def test_anon_get_redirects_to_login(anon_client): """未登入打 GET 路由 → @login_required 必 302 redirect 到 /login。""" for path in [ + '/observability/', + '/observability/overview', + '/observability/rag_queries', + '/observability/business_intel', + '/observability/agent_orchestration', '/observability/ai_calls', '/observability/promotion_review', '/observability/quality_trend', '/observability/host_health', '/observability/budget', '/observability/ppt_audit_history', + '/observability/api/health_indicator', ]: r = anon_client.get(path) - assert r.status_code == 302, f'{path} 未強制 login (got {r.status_code})' + # 308(permanent redirect for trailing slash)或 302(login redirect)皆視為阻擋 + assert r.status_code in (302, 308), f'{path} 未強制 login (got {r.status_code})' def test_anon_post_blocked(anon_client): - """未登入 POST mutation 端點 → 必 302 redirect(不可執行 promote/budget update)。""" - r = anon_client.post('/observability/promotion_review/approve/1') - assert r.status_code == 302 - r = anon_client.post('/observability/promotion_review/reject/1') - assert r.status_code == 302 - r = anon_client.post('/observability/budget/update/1', json={'budget_usd': 99, 'alert_pct': 80}) - assert r.status_code == 302 + """未登入 POST mutation 端點 → 必 302 redirect(防 anon 執行任何 mutation)。""" + posts = [ + ('/observability/promotion_review/approve/1', None), + ('/observability/promotion_review/reject/1', None), + ('/observability/budget/update/1', {'budget_usd': 99, 'alert_pct': 80}), + ('/observability/ai_calls/trigger_code_review', None), + ('/observability/ppt_audit/trigger_aider_heal', None), + ('/observability/playbooks/toggle/1', None), + ('/observability/host_health/trigger_autoheal', None), + ('/observability/budget/force_throttle', None), + ] + for path, body in posts: + r = anon_client.post(path, json=body) if body else anon_client.post(path) + assert r.status_code in (302, 308), f'{path} POST 未強制 login (got {r.status_code})' + + +# ────────────────────────────────────────────────────────────────────────── +# Phase 38+ 新增 GET 路由 smoke +# ────────────────────────────────────────────────────────────────────────── + +def test_overview_index_200(client, monkeypatch): + """/observability/ (root index) — 觀測台總覽。""" + from routes import admin_observability_routes as mod + monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([])) + r = client.get('/observability/') + assert r.status_code in (200, 308) + + +def test_overview_dashboard_200(client, monkeypatch): + """/observability/overview — Phase 45 總覽頁。""" + from routes import admin_observability_routes as mod + monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([])) + # mock requests for 三主機 sparkline + import requests as _r + def fake_get(*_a, **_kw): + raise _r.exceptions.ConnectionError('mocked') + monkeypatch.setattr(_r, 'get', fake_get) + r = client.get('/observability/overview') + assert r.status_code == 200 + + +def test_rag_queries_200(client, monkeypatch): + """/observability/rag_queries — Phase 51 RAG 召回詳情頁。""" + from routes import admin_observability_routes as mod + monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([])) + r = client.get('/observability/rag_queries') + assert r.status_code == 200 + + +def test_business_intel_200(client, monkeypatch): + """/observability/business_intel — Phase 48 商業面 × AI 編排。""" + from routes import admin_observability_routes as mod + monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([])) + r = client.get('/observability/business_intel') + assert r.status_code == 200 + + +def test_agent_orchestration_200(client, monkeypatch): + """/observability/agent_orchestration — Phase 46 Agent 編排矩陣。""" + from routes import admin_observability_routes as mod + monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([])) + r = client.get('/observability/agent_orchestration') + assert r.status_code == 200 + + +def test_health_indicator_api_returns_json(client, monkeypatch): + """/observability/api/health_indicator — Phase 52 topbar 健康指示燈 JSON API。""" + from routes import admin_observability_routes as mod + monkeypatch.setattr(mod, 'get_session', lambda: _fake_session([])) + r = client.get('/observability/api/health_indicator') + assert r.status_code == 200 + assert r.content_type.startswith('application/json'), \ + f'expected JSON, got {r.content_type}'