# 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) # 方案:抽取 AIRouter._aggregate_feedback_stats 純函數,直接單元測試,零 DB 依賴。 # DB 聚合查詢行為已由 integration/test_ai_router_feedback_integration.py 覆蓋。 """Unit tests for AIRouter._aggregate_feedback_stats — 純邏輯,無 DB。""" import pytest from src.services.ai_router import AIRouter # ============================================================================= # _aggregate_feedback_stats 純函數測試(無 DB 依賴) # ============================================================================= def test_feedback_aggregates_by_model(): 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}, ] out = AIRouter._aggregate_feedback_stats(stats) assert out["elephant-alpha"] == 0.8 assert out["gemini-pro"] == 1.0 def test_feedback_filters_by_repo(): 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}, ] out = AIRouter._aggregate_feedback_stats(stats, repo="awoooi") assert out == {"elephant-alpha": 0.8} def test_feedback_handles_empty_stats(): out = AIRouter._aggregate_feedback_stats([]) assert out == {} def test_feedback_skips_missing_model(): stats = [ {"repo": "awoooi", "model": None, "success_rate": 0.9}, {"repo": "awoooi", "model": "gemini-pro", "success_rate": 0.7}, ] out = AIRouter._aggregate_feedback_stats(stats) assert list(out.keys()) == ["gemini-pro"] # ============================================================================= # feedback_from_aider_events — DB 失敗降級行為(error path,無 FakeRepo) # ============================================================================= @pytest.mark.asyncio async def test_feedback_returns_empty_on_db_failure(monkeypatch): monkeypatch.setattr( "src.services.ai_router.get_session_factory", lambda: (_ for _ in ()).throw(RuntimeError("DB unavailable")), raising=False, ) r = AIRouter() out = await r.feedback_from_aider_events(days=7) assert out == {}