Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 35s
## 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>
150 lines
6.1 KiB
Python
150 lines
6.1 KiB
Python
# tests/integration/test_ai_router_feedback_integration.py | 2026-04-22 @ Asia/Taipei
|
||
"""AIRouter.feedback_from_aider_events() 整合測試 — 使用真實 awoooi_dev PostgreSQL
|
||
|
||
替換 tests/test_ai_router_feedback.py 中違反 feedback_no_mock_testing.md 的
|
||
FakeRepo / FakeSession mock。
|
||
|
||
AIRouter.feedback_from_aider_events() 本質是聚合查詢 — 直接用真實 DB 驗證
|
||
比 mock DB 更準確,且能抓到 SQL 語法錯誤。
|
||
|
||
規則: 每個測試後 rollback(由 integration/conftest.py db_session fixture 保證)
|
||
禁止 Mock Repository / Session — 直接使用真實 DB 連線。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime, timezone, timedelta
|
||
|
||
import pytest
|
||
from sqlalchemy import text
|
||
|
||
from src.repositories.aider_event_repository import AiderEventRepository
|
||
|
||
TAIPEI = timezone(timedelta(hours=8))
|
||
|
||
|
||
def _ts(offset_days: int = 0) -> datetime:
|
||
return datetime.now(TAIPEI) - timedelta(days=offset_days)
|
||
|
||
|
||
async def _insert_session(db_session, session_id: str, model: str,
|
||
repo_cwd: str, has_error: bool = False) -> None:
|
||
"""插入一組 session_start + (可選) error event。"""
|
||
repo = AiderEventRepository(db_session)
|
||
await repo.insert(
|
||
session_id=session_id,
|
||
ts=_ts(),
|
||
type_="session_start",
|
||
host="ogt-mac",
|
||
payload={"cwd": repo_cwd, "model": model,
|
||
"aider_args": [], "aider_pid": 1, "cli_version": "0.86"},
|
||
)
|
||
if has_error:
|
||
await repo.insert(
|
||
session_id=session_id,
|
||
ts=_ts(),
|
||
type_="error",
|
||
host="ogt-mac",
|
||
payload={"cwd": repo_cwd, "model": model,
|
||
"kind": "api_rate_limit", "message": "429",
|
||
"context_50chars": ""},
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# model_stats_since() 聚合正確性
|
||
# =============================================================================
|
||
|
||
class TestModelStatsAggregation:
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_empty_db_returns_empty_list(self, db_session):
|
||
"""無資料時 model_stats_since 應回傳空 list(不崩潰)。"""
|
||
repo = AiderEventRepository(db_session)
|
||
result = await repo.model_stats_since(days=1)
|
||
# 可能有其他 session 留存(dev DB),但至少型別正確
|
||
assert isinstance(result, list)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_inserted_sessions_appear_in_stats(self, db_session):
|
||
"""插入 2 筆 session(1 成功 1 失敗),stats 應正確回傳。"""
|
||
await _insert_session(db_session, "s-ok-001", "elephant-alpha",
|
||
"/awoooi", has_error=False)
|
||
await _insert_session(db_session, "s-err-001", "elephant-alpha",
|
||
"/awoooi", has_error=True)
|
||
await db_session.flush()
|
||
|
||
repo = AiderEventRepository(db_session)
|
||
result = await repo.model_stats_since(days=1)
|
||
|
||
# 找出我們插入的 model
|
||
elephant_rows = [r for r in result
|
||
if r.get("model") == "elephant-alpha"
|
||
and r.get("repo") is not None
|
||
and "/awoooi" in (r.get("repo") or "")]
|
||
assert len(elephant_rows) >= 1
|
||
|
||
row = elephant_rows[0]
|
||
assert row["total"] >= 2
|
||
assert 0.0 <= float(row["success_rate"]) <= 1.0
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_success_rate_field_is_float(self, db_session):
|
||
"""success_rate 欄位必須可轉換為 float(AIRouter 依賴此保證)。"""
|
||
await _insert_session(db_session, "s-float-001", "gemini-pro",
|
||
"/clawbot", has_error=False)
|
||
await db_session.flush()
|
||
|
||
repo = AiderEventRepository(db_session)
|
||
result = await repo.model_stats_since(days=1)
|
||
for row in result:
|
||
# 不應 raise
|
||
_ = float(row.get("success_rate") or 0)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_repo_filter_works(self, db_session):
|
||
"""插入兩個不同 cwd 的 session,手動 filter by repo 應只回傳對應資料。"""
|
||
await _insert_session(db_session, "s-awoooi-001", "elephant-alpha",
|
||
"/awoooi", has_error=False)
|
||
await _insert_session(db_session, "s-other-001", "elephant-alpha",
|
||
"/other-repo", has_error=True)
|
||
await db_session.flush()
|
||
|
||
repo = AiderEventRepository(db_session)
|
||
all_stats = await repo.model_stats_since(days=1)
|
||
|
||
# 手動過濾(模擬 AIRouter.feedback_from_aider_events(repo="awoooi"))
|
||
awoooi_rows = [r for r in all_stats if "/awoooi" in (r.get("repo") or "")]
|
||
other_rows = [r for r in all_stats if "/other-repo" in (r.get("repo") or "")]
|
||
|
||
# 若兩筆都有資料,它們的 success_rate 應該不同(一個 1.0,一個 0.0)
|
||
# 這裡只確認 filter 邏輯本身不混淆
|
||
for row in awoooi_rows:
|
||
assert "/other-repo" not in (row.get("repo") or "")
|
||
for row in other_rows:
|
||
assert "/awoooi" not in (row.get("repo") or "")
|
||
|
||
|
||
# =============================================================================
|
||
# AIRouter.feedback_from_aider_events() error handling(不 mock Session)
|
||
# =============================================================================
|
||
|
||
class TestAIRouterFeedbackDBBehavior:
|
||
"""驗證 feedback_from_aider_events() 不崩潰即可(透過 model_stats_since 間接測試)。"""
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_model_stats_since_does_not_raise_on_empty(self, db_session):
|
||
"""空 DB 時聚合查詢不應拋例外。"""
|
||
repo = AiderEventRepository(db_session)
|
||
try:
|
||
result = await repo.model_stats_since(days=7)
|
||
assert isinstance(result, list)
|
||
except Exception as e:
|
||
pytest.fail(f"model_stats_since raised unexpectedly: {e}")
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_daily_pattern_candidates_no_error(self, db_session):
|
||
"""daily_pattern_candidates() 不應崩潰。"""
|
||
repo = AiderEventRepository(db_session)
|
||
result = await repo.daily_pattern_candidates(days=1)
|
||
assert isinstance(result, list)
|