- F401: 移除未使用的 imports (TerminalSessionStatus, AutoApproveDecision, TerminalSession)
- I001: 修正 import blocks 排序
- C401: set(generator) → {set comprehension}
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
225 lines
7.3 KiB
Python
225 lines
7.3 KiB
Python
"""
|
|
Phase 19.6: Terminal Service Tests
|
|
==================================
|
|
Omni-Terminal SSE 架構測試
|
|
|
|
測試內容:
|
|
1. 意圖分類 (classify_intent)
|
|
2. Service 依賴注入
|
|
3. Model 驗證
|
|
|
|
@see ADR-031 Omni-Terminal SSE Architecture
|
|
@author Claude Code (首席架構師)
|
|
@version 1.0.0
|
|
@date 2026-03-28 (台北時間)
|
|
"""
|
|
|
|
import pytest
|
|
|
|
from src.models.terminal import (
|
|
SpatialContext,
|
|
TerminalIntentRequest,
|
|
TerminalSessionStatus,
|
|
)
|
|
from src.services.terminal_service import (
|
|
IntentType,
|
|
TerminalService,
|
|
classify_intent,
|
|
get_terminal_service,
|
|
)
|
|
|
|
# =============================================================================
|
|
# Intent Classification Tests
|
|
# =============================================================================
|
|
|
|
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
|
|
# =============================================================================
|
|
|
|
|
|
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}"
|