Files
awoooi/apps/api/tests/test_terminal_service.py
OG T b94a7800ad
All checks were successful
E2E Health Check / e2e-health (push) Successful in 17s
fix(approval): 修復 Y/n 簽核按鈕無動作問題 (Phase 22 P1)
根本原因: 前端未傳送 CSRF Token,API 拒絕所有簽核請求

修復內容:
1. live-approval-panel.tsx: 整合 useCSRF hook
   - 簽核時帶上 csrfToken 參數
   - 拒絕時帶上 csrfToken 參數
   - 新增 CSRF 載入/錯誤狀態顯示

2. test_intent_classifier.py: 移除 Mock 違規 (P1)
   - 改用 @requires_ollama marker
   - 真實 Ollama 整合測試

3. test_terminal_service.py: 移除 Mock 違規 (P1)
   - 改用 @requires_database/@requires_k8s markers
   - 保留純函數單元測試

遵循規範:
- feedback_no_mock_testing.md: 禁止 MagicMock/AsyncMock
- Phase 20 CSRF Protection: Double Submit Cookie

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-31 16:16:16 +08:00

393 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Phase 19.6: Terminal Service Tests
==================================
Omni-Terminal SSE 架構測試
Phase 22 P1 修復: 移除 Mock使用真實服務
2026-03-31 Claude Code (首席架構師)
測試內容:
1. 意圖分類 (classify_intent) - 純函數
2. Model 驗證 - 純 Pydantic
3. Service 依賴注入
4. 整合測試 (需要 Redis/K8s)
遵循規範:
- feedback_no_mock_testing.md: 禁止 MagicMock/AsyncMock/patch
- ADR-031 Omni-Terminal SSE Architecture
"""
import pytest
from src.models.terminal import (
SpatialContext,
TerminalIntentRequest,
TerminalSessionStatus,
)
from src.services.terminal_service import (
IntentType,
TerminalService,
classify_intent,
get_terminal_service,
)
# =============================================================================
# Test Markers
# =============================================================================
requires_database = pytest.mark.skipif(
"not config.getoption('--run-integration', default=False)",
reason="Need --run-integration option to run database integration tests",
)
requires_k8s = pytest.mark.skipif(
"not config.getoption('--run-k8s', default=False)",
reason="Need --run-k8s option to run K8s integration tests",
)
# =============================================================================
# Intent Classification Tests - Pure Functions
# =============================================================================
INTENT_CLASSIFICATION_TEST_CASES = [
# 查詢類 - 狀態
("status", IntentType.QUERY_STATUS),
("狀態", IntentType.QUERY_STATUS),
("pod status", IntentType.QUERY_STATUS),
("健康狀態", IntentType.QUERY_STATUS),
("health check", IntentType.QUERY_STATUS),
# 查詢類 - 指標
("metrics", IntentType.QUERY_METRICS),
("指標", IntentType.QUERY_METRICS),
("cpu usage", IntentType.QUERY_METRICS),
("記憶體使用量", IntentType.QUERY_METRICS),
("memory utilization", IntentType.QUERY_METRICS),
# 查詢類 - 日誌
("logs", IntentType.QUERY_LOGS),
("日誌", IntentType.QUERY_LOGS),
("error logs", IntentType.QUERY_LOGS),
("錯誤訊息", IntentType.QUERY_LOGS),
("exception trace", IntentType.QUERY_LOGS),
# 操作類 - 簽核
("approve", IntentType.ACTION_APPROVAL),
("簽核", IntentType.ACTION_APPROVAL),
("授權", IntentType.ACTION_APPROVAL),
("批准這個操作", IntentType.ACTION_APPROVAL),
("show pending approval", IntentType.ACTION_APPROVAL),
# 操作類 - 重啟
("restart", IntentType.ACTION_RESTART),
("重啟", IntentType.ACTION_RESTART),
("rollout", IntentType.ACTION_RESTART),
("重新部署", IntentType.ACTION_RESTART),
# 操作類 - 擴容
("scale", IntentType.ACTION_SCALE),
("擴容", IntentType.ACTION_SCALE),
("增加副本", IntentType.ACTION_SCALE),
("replica count", IntentType.ACTION_SCALE),
# 分析類 - RCA
("rca", IntentType.ANALYZE_RCA),
("root cause", IntentType.ANALYZE_RCA),
("根因", IntentType.ANALYZE_RCA),
("分析問題", IntentType.ANALYZE_RCA),
("為什麼 API 變慢", IntentType.ANALYZE_RCA),
("why is it slow", IntentType.ANALYZE_RCA),
# 分析類 - Incident
("incident", IntentType.ANALYZE_INCIDENT),
("事件", IntentType.ANALYZE_INCIDENT),
("alert 告警", IntentType.ANALYZE_INCIDENT),
("告警詳情", IntentType.ANALYZE_INCIDENT),
# 一般
("hello", IntentType.GENERAL),
("你好", IntentType.GENERAL),
("help", IntentType.GENERAL),
("random text", IntentType.GENERAL),
]
@pytest.mark.parametrize("intent,expected_type", INTENT_CLASSIFICATION_TEST_CASES)
def test_classify_intent(intent: str, expected_type: IntentType):
"""測試意圖分類準確度 - 純函數,不需要外部依賴"""
result = classify_intent(intent)
assert result == expected_type, f"Intent '{intent}' should be classified as {expected_type}, got {result}"
def test_classify_intent_case_insensitive():
"""測試意圖分類不區分大小寫"""
assert classify_intent("STATUS") == IntentType.QUERY_STATUS
assert classify_intent("Metrics") == IntentType.QUERY_METRICS
assert classify_intent("RESTART") == IntentType.ACTION_RESTART
# =============================================================================
# Model Validation Tests - Pure Pydantic
# =============================================================================
def test_spatial_context_required_fields():
"""測試 SpatialContext 必填欄位"""
context = SpatialContext(current_page="/dashboard")
assert context.current_page == "/dashboard"
assert context.focused_entity_id is None # 預設為 None
def test_spatial_context_with_entity():
"""測試 SpatialContext 帶實體 ID"""
context = SpatialContext(
current_page="/incidents",
focused_entity_id="INC-001",
)
assert context.current_page == "/incidents"
assert context.focused_entity_id == "INC-001"
def test_terminal_intent_request_validation():
"""測試 TerminalIntentRequest 驗證"""
request = TerminalIntentRequest(
intent="check system status",
context=SpatialContext(current_page="/dashboard"),
)
assert request.intent == "check system status"
assert request.context.current_page == "/dashboard"
assert request.session_id is None # 預設為 None
def test_terminal_intent_request_with_session():
"""測試帶 session_id 的 TerminalIntentRequest"""
request = TerminalIntentRequest(
intent="continue analysis",
context=SpatialContext(current_page="/"),
session_id="sess-001",
)
assert request.session_id == "sess-001"
def test_terminal_session_status_enum():
"""測試 TerminalSessionStatus 枚舉值"""
assert TerminalSessionStatus.PENDING.value == "pending"
assert TerminalSessionStatus.PROCESSING.value == "processing"
assert TerminalSessionStatus.COMPLETED.value == "completed"
assert TerminalSessionStatus.ERROR.value == "error"
assert TerminalSessionStatus.ABORTED.value == "aborted"
# =============================================================================
# Dependency Injection Tests
# =============================================================================
@pytest.mark.asyncio
async def test_get_terminal_service():
"""測試 FastAPI 依賴注入函數"""
service = await get_terminal_service()
assert isinstance(service, TerminalService)
@pytest.mark.asyncio
async def test_get_terminal_service_creates_new_instance():
"""測試每次呼叫建立新實例 (非 Singleton)"""
service1 = await get_terminal_service()
service2 = await get_terminal_service()
assert service1 is not service2, "Should create new instance each time"
# =============================================================================
# Service Unit Tests (No External Dependencies)
# =============================================================================
@pytest.mark.asyncio
async def test_terminal_service_instantiation():
"""測試 TerminalService 實例化"""
service = TerminalService()
assert service._sessions == {}
assert service._tasks == {}
@pytest.mark.asyncio
async def test_get_session_not_found():
"""測試取得不存在的 Session"""
service = TerminalService()
session = await service.get_session("nonexistent-session")
assert session is None
@pytest.mark.asyncio
async def test_abort_session_not_found():
"""測試中斷不存在的 Session"""
service = TerminalService()
result = await service.abort_session("nonexistent-session")
assert result is False
# =============================================================================
# Intent Coverage Statistics
# =============================================================================
def test_intent_type_coverage():
"""確保所有 IntentType 都有測試案例"""
tested_types = {expected for _, expected in INTENT_CLASSIFICATION_TEST_CASES}
all_types = set(IntentType)
missing = all_types - tested_types
assert not missing, f"Missing test cases for IntentType: {missing}"
# =============================================================================
# Integration Tests - Require Real Services
# =============================================================================
# Phase 22 P1 修復: 移除 Mock使用真實服務
# 2026-03-31 Claude Code (首席架構師)
# =============================================================================
class TestHandleApprovalActionIntegration:
"""
_handle_approval_action 整合測試
需要:
- PostgreSQL (ApprovalService)
- Redis (SSE Publisher)
"""
@pytest.mark.asyncio
@requires_database
async def test_approval_action_returns_pending_list(self):
"""
測試查詢待簽核項目
使用真實 ApprovalService 查詢資料庫
"""
from src.services.approval_service import get_approval_service
service = TerminalService()
approval_service = get_approval_service()
# 查詢真實的待簽核項目
pending = await approval_service.get_pending_approvals()
# 驗證返回格式
assert isinstance(pending, list)
@pytest.mark.asyncio
@requires_database
async def test_approval_service_connection(self):
"""測試 ApprovalService 連線"""
from src.services.approval_service import get_approval_service
service = get_approval_service()
# 應該能成功取得服務實例
assert service is not None
class TestHandleStatusQueryIntegration:
"""
_handle_status_query 整合測試
需要:
- K8s 連線 (K8sRepository)
"""
@pytest.mark.asyncio
@requires_k8s
async def test_k8s_repository_available(self):
"""測試 K8sRepository 連線"""
from src.repositories.k8s_repository import get_k8s_repository
repo = get_k8s_repository()
is_available = await repo.is_available()
# 如果 K8s 可用,應該返回 True
# 如果在 Mock 模式,也應該返回 True
assert isinstance(is_available, bool)
@pytest.mark.asyncio
@requires_k8s
async def test_k8s_pod_status_summary(self):
"""測試 K8s Pod 狀態摘要"""
from src.repositories.k8s_repository import get_k8s_repository
repo = get_k8s_repository()
if not await repo.is_available():
pytest.skip("K8s not available")
summary = await repo.get_pod_status_summary()
# 驗證返回格式
assert "total" in summary
assert "running" in summary
assert "pending" in summary
assert "failed" in summary
@pytest.mark.asyncio
@requires_k8s
async def test_k8s_list_deployments(self):
"""測試 K8s Deployment 列表"""
from src.repositories.k8s_repository import get_k8s_repository
repo = get_k8s_repository()
if not await repo.is_available():
pytest.skip("K8s not available")
deployments = await repo.list_deployments()
# 驗證返回格式
assert isinstance(deployments, list)
class TestTerminalServiceIntegration:
"""
TerminalService 完整整合測試
需要:
- Redis (SSE Publisher)
- PostgreSQL (ApprovalService)
"""
@pytest.mark.asyncio
@requires_database
async def test_terminal_service_with_real_publisher(self):
"""
測試 TerminalService 搭配真實 SSE Publisher
驗證:
1. 能建立 Session
2. 能發送 SSE 事件
"""
from src.core.sse import get_sse_publisher
service = TerminalService()
publisher = get_sse_publisher()
# 驗證 publisher 實例
assert publisher is not None
@pytest.mark.asyncio
@requires_database
async def test_process_intent_with_real_services(self):
"""
測試意圖處理 (不含 SSE 輸出)
驗證:
1. 意圖分類正確
2. Session 狀態正確
"""
service = TerminalService()
# 測試意圖分類
intent_type = classify_intent("check status")
assert intent_type == IntentType.QUERY_STATUS
intent_type = classify_intent("show pending approvals")
assert intent_type == IntentType.ACTION_APPROVAL