Files
awoooi/apps/api/tests/test_ai_router_feedback.py
Your Name d0591c54b0
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 35s
fix(security): 體健修復 — 7項 Critical/Major 安全問題全修
## Critical 修復 (C1-C5)
- C1: git rm --cached 03-secrets.yaml(CHANGE_ME 模板不再追蹤)
- C2: git rm --cached awoooi.db + .gitignore 加 *.db(SQLite HARD_RULES 違規)
- C3: sentry-tunnel SENTRY_HOST 改為 process.env fallback
- C4: config.py DATABASE_URL 移除 changeme default,改為必填
- C5: run_migration.py 改為 os.environ["DATABASE_URL"]

## Major 修復 (M1-M4)
- M1: auto_repair /execute 加 CSRF 保護 + AutoRepairPanel.tsx 同步
- M2: drift /rollback /adopt 加 CSRF 保護(/internal/scan 保持無 CSRF)
- M3: terminal /intent 加 CSRF 保護 + terminal.store.ts 同步
- M4: live-dashboard HOST_IPS + host-grid VIP 改為 env var

## 其他
- 新增 apps/web/.env.example(6 個 env var 說明)
- K8s deployment-web 補入 3 個新 env var
- 整合測試:新增 aider_event_repository + ai_router_feedback 真實 DB 測試
- test_terminal.py CSRF dependency override 修復

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 01:27:39 +08:00

100 lines
3.8 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.
# apps/api/tests/test_ai_router_feedback.py | 2026-04-20 @ Asia/Taipei
# 2026-04-22 @ Asia/Taipei: FakeRepo / FakeSession 違反 feedback_no_mock_testing.md
# → DB 聚合查詢測試已遷移至 integration/test_ai_router_feedback_integration.py真實 DB
# 此檔案保留的測試驗證「DB 不可用時的降級行為」fail_sf — 此為錯誤路徑邏輯,
# 非正常 DB 查詢,可留作 unit 層覆蓋。
# FakeRepo 測試test_feedback_aggregates_by_model 等)已被 integration test 取代,
# 下方保留作參考,但實際 DB 行為請以 integration test 為準。
"""Task A8: AIRouter.feedback_from_aider_events — 降級行為 + 邊界條件測試。"""
import pytest
from unittest.mock import AsyncMock, MagicMock
from src.services.ai_router import AIRouter
@pytest.mark.asyncio
async def test_feedback_aggregates_by_model(monkeypatch):
stats = [
{"repo": "awoooi", "model": "elephant-alpha", "total": 10,
"errors": 2, "success_rate": 0.8},
{"repo": "awoooi", "model": "gemini-pro", "total": 5,
"errors": 0, "success_rate": 1.0},
]
class FakeRepo:
def __init__(self, sess): pass
async def model_stats_since(self, days): return stats
class FakeSession:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
monkeypatch.setattr("src.services.ai_router.get_session_factory",
lambda: (lambda: FakeSession()), raising=False)
monkeypatch.setattr("src.repositories.aider_event_repository.AiderEventRepository",
FakeRepo)
r = AIRouter()
out = await r.feedback_from_aider_events(days=7)
assert out["elephant-alpha"] == 0.8
assert out["gemini-pro"] == 1.0
@pytest.mark.asyncio
async def test_feedback_filters_by_repo(monkeypatch):
stats = [
{"repo": "awoooi", "model": "elephant-alpha", "total": 5,
"errors": 1, "success_rate": 0.8},
{"repo": "other-repo", "model": "elephant-alpha", "total": 3,
"errors": 3, "success_rate": 0.0},
]
class FakeRepo:
def __init__(self, sess): pass
async def model_stats_since(self, days): return stats
class FakeSession:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
monkeypatch.setattr("src.services.ai_router.get_session_factory",
lambda: (lambda: FakeSession()), raising=False)
monkeypatch.setattr("src.repositories.aider_event_repository.AiderEventRepository",
FakeRepo)
r = AIRouter()
out = await r.feedback_from_aider_events(repo="awoooi", days=7)
assert out == {"elephant-alpha": 0.8} # other-repo 過濾掉
@pytest.mark.asyncio
async def test_feedback_returns_empty_on_db_failure(monkeypatch):
def fail_sf():
raise RuntimeError("DB unavailable")
monkeypatch.setattr("src.services.ai_router.get_session_factory",
fail_sf, raising=False)
r = AIRouter()
out = await r.feedback_from_aider_events(days=7)
assert out == {} # 降級為空 dictcaller 不該崩
@pytest.mark.asyncio
async def test_feedback_handles_empty_stats(monkeypatch):
class FakeRepo:
def __init__(self, sess): pass
async def model_stats_since(self, days): return []
class FakeSession:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
monkeypatch.setattr("src.services.ai_router.get_session_factory",
lambda: (lambda: FakeSession()), raising=False)
monkeypatch.setattr("src.repositories.aider_event_repository.AiderEventRepository",
FakeRepo)
r = AIRouter()
out = await r.feedback_from_aider_events()
assert out == {}