refactor(api): Phase 22 P1 模組化修復 - Router→Service 封裝
All checks were successful
E2E Health Check / e2e-health (push) Successful in 24s

修復內容:

1. e2e_network_test.py: 移除 unittest.mock
   - 將 16 個 patch.object 改為 pytest monkeypatch
   - 符合 feedback_no_mock_testing.md

2. audit_logs.py: Router→Service 層封裝
   - 新增 AuditLogService (audit_log_service.py)
   - Router 改用 get_audit_log_service()
   - 移除直接 Repository 存取

3. incidents.py:463: DEBUG 端點重構
   - 移除 get_incident_repository() 直接呼叫
   - 完全透過 IncidentService 操作
   - 簡化回傳結構

遵循規範:
- Skill 09: Router 層禁止直接外部 API 呼叫
- feedback_lewooogo_modular_enforcement.md: Service 層封裝
- feedback_no_mock_testing.md: 禁止 MagicMock/AsyncMock

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-03-31 16:25:00 +08:00
parent 9dd9e6325b
commit 31c9117ae7
4 changed files with 274 additions and 132 deletions

View File

@@ -18,7 +18,7 @@ from pydantic import BaseModel
from src.core.logging import get_logger
from src.db.models import AuditLog
from src.repositories.audit_log_repository import get_audit_log_repository
from src.services.audit_log_service import get_audit_log_service
router = APIRouter(prefix="/audit-logs", tags=["Audit Logs"])
logger = get_logger("awoooi.audit")
@@ -133,17 +133,16 @@ async def list_audit_logs(
Returns:
AuditLogListResponse: 分頁稽核日誌
"""
# Phase 17 P0: Router 層違規修復 - 改用 Repository 層
repo = get_audit_log_repository()
# Phase 22 P1: Router 層違規修復 - 改用 Service 層 (2026-03-31)
service = get_audit_log_service()
logs, total_count = await repo.list_logs(
logs, total_count = await service.list_logs(
page=page,
page_size=page_size,
success_only=success,
namespace=namespace,
operation_type=operation_type,
)
# TODO: operation_type 篩選暫未實作,需要擴展 Repository
total_pages = (total_count + page_size - 1) // page_size if total_count > 0 else 1
logger.info(
@@ -188,9 +187,9 @@ async def get_audit_stats() -> AuditStatsResponse:
Returns:
AuditStatsResponse: 統計資訊
"""
# Phase 17 P0: Router 層違規修復 - 改用 Repository 層
repo = get_audit_log_repository()
stats = await repo.get_stats(since_hours=24)
# Phase 22 P1: Router 層違規修復 - 改用 Service 層 (2026-03-31)
service = get_audit_log_service()
stats = await service.get_stats(since_hours=24)
logger.info(
"audit_stats_fetched",
@@ -233,9 +232,9 @@ async def get_audit_log(log_id: str) -> AuditLogResponse:
Raises:
HTTPException: 404 找不到日誌
"""
# Phase 17 P0: Router 層違規修復 - 改用 Repository 層
repo = get_audit_log_repository()
log = await repo.get_by_id(log_id)
# Phase 22 P1: Router 層違規修復 - 改用 Service 層 (2026-03-31)
service = get_audit_log_service()
log = await service.get_by_id(log_id)
if log is None:
raise HTTPException(

View File

@@ -26,8 +26,8 @@ from src.core.logging import get_logger
from src.models.approval import ApprovalRequestResponse
from src.models.incident import Incident, IncidentStatus, Severity
# Phase 16 R3.3b (2026-03-25 台北時區): Repository 層整合
from src.repositories.incident_repository import get_incident_repository
# Phase 22 P1: 移除 Repository 直接存取 (2026-03-31)
# Phase 16 R3.3b (2026-03-25 台北時區): Repository 層整合 - 已移至 Service 層
from src.services.decision_manager import get_decision_manager
from src.services.incident_service import get_incident_service
from src.services.proposal_service import get_proposal_service
@@ -456,59 +456,43 @@ async def debug_resolve_incident(incident_id: str) -> dict[str, Any]:
DEBUG: 直接更新 Incident 狀態為 RESOLVED
用於測試 resolve_incident_after_approval 邏輯
Phase 17 P0: Router 層違規修復 - 用 Service 層
Phase 22 P1: Router 層違規修復 - 完全使用 Service 層 (2026-03-31)
"""
# Phase 17 P0: 改用 Service 層
incident_service = get_incident_service()
repo = get_incident_repository()
error_msg = None
# 1. 取得更新前狀態
before_redis = None
before_db = None
# 1. 取得更新前狀態 (透過 Service)
before_status = None
try:
incident = await incident_service.get_from_working_memory(incident_id)
if incident:
before_redis = incident.status.value
before_status = incident.status.value
except Exception as e:
error_msg = f"Redis read error: {e}"
try:
before_db = await repo.get_status(incident_id)
except Exception as e:
error_msg = f"DB read error: {e}"
error_msg = f"Read error: {e}"
# 2. 透過 Service 更新狀態
redis_updated = False
db_updated = False
updated = False
try:
resolved = await incident_service.resolve_incident(incident_id)
if resolved:
redis_updated = True
db_updated = True
updated = True
except Exception as e:
error_msg = f"Resolve error: {e}"
# 3. 驗證更新後狀態
after_redis = None
after_db = None
# 3. 驗證更新後狀態 (透過 Service)
after_status = None
try:
incident = await incident_service.get_from_working_memory(incident_id)
if incident:
after_redis = incident.status.value
except Exception:
pass
try:
after_db = await repo.get_status(incident_id)
after_status = incident.status.value
except Exception:
pass
return {
"incident_id": incident_id,
"before": {"redis": before_redis, "db": before_db},
"after": {"redis": after_redis, "db": after_db},
"updates": {"redis": redis_updated, "db": db_updated},
"before_status": before_status,
"after_status": after_status,
"updated": updated,
"error": error_msg,
}

View File

@@ -0,0 +1,142 @@
"""
Audit Log Service - Phase 22 P1 模組化修復
==========================================
Router 層禁止直接存取 Repository抽取至 Service 層
遵循規範:
- Skill 09: Router 層禁止直接外部 API 呼叫
- feedback_lewooogo_modular_enforcement.md: Service 層封裝
功能:
- 分頁查詢稽核日誌
- 取得單筆稽核日誌
- 統計資訊查詢
版本: v1.0
建立: 2026-03-31 (台北時區)
建立者: Claude Code (首席架構師 P1 修復)
"""
from typing import Any
import structlog
from src.db.models import AuditLog
from src.repositories.audit_log_repository import get_audit_log_repository
logger = structlog.get_logger(__name__)
# =============================================================================
# Audit Log Service
# =============================================================================
class AuditLogService:
"""
Audit Log Service
統一稽核日誌存取,符合 leWOOOgo 積木化原則
2026-03-31 Claude Code (Phase 22 P1 修復)
"""
def __init__(self) -> None:
self._repository = get_audit_log_repository()
async def list_logs(
self,
page: int = 1,
page_size: int = 20,
success_only: bool | None = None,
namespace: str | None = None,
operation_type: str | None = None,
) -> tuple[list[AuditLog], int]:
"""
分頁查詢稽核日誌
Args:
page: 頁碼 (從 1 開始)
page_size: 每頁筆數
success_only: 篩選成功/失敗
namespace: 篩選 Namespace
operation_type: 篩選操作類型 (暫未實作)
Returns:
(logs, total_count)
"""
logs, total_count = await self._repository.list_logs(
page=page,
page_size=page_size,
success_only=success_only,
namespace=namespace,
)
logger.debug(
"audit_logs_listed",
page=page,
count=len(logs),
total=total_count,
)
return logs, total_count
async def get_by_id(self, log_id: str) -> AuditLog | None:
"""
取得單筆稽核日誌
Args:
log_id: 稽核日誌 ID
Returns:
AuditLog | None
"""
log = await self._repository.get_by_id(log_id)
if log:
logger.debug(
"audit_log_fetched",
log_id=log_id,
)
else:
logger.debug(
"audit_log_not_found",
log_id=log_id,
)
return log
async def get_stats(self, since_hours: int = 24) -> dict[str, Any]:
"""
取得稽核統計資訊
Args:
since_hours: 統計時間範圍 (小時)
Returns:
統計資訊 dict
"""
stats = await self._repository.get_stats(since_hours=since_hours)
logger.debug(
"audit_stats_fetched",
total=stats.get("total", 0),
success_rate=stats.get("success_rate", 0.0),
)
return stats
# =============================================================================
# Singleton
# =============================================================================
_audit_log_service: AuditLogService | None = None
def get_audit_log_service() -> AuditLogService:
"""取得 AuditLogService singleton"""
global _audit_log_service
if _audit_log_service is None:
_audit_log_service = AuditLogService()
return _audit_log_service