Files
awoooi/apps/api/tests/test_aider_event_processor.py
Your Name 479f8d8971
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 35s
refactor(tests): 技術債清零 — 移除 FakeRepo/FakeSession Mock DB 違規
## ai_router.py
- 抽取 _aggregate_feedback_stats() 純函數,feedback_from_aider_events 呼叫它

## aider_event_processor.py
- _process_one 加 _session_factory=None DI 參數(預設 get_session_factory())
- 可注入測試 factory,不改既有生產邏輯

## test_ai_router_feedback.py(完全重寫)
- 移除 FakeRepo/FakeSession,改為直接測試 _aggregate_feedback_stats 純函數
- 新增 test_feedback_skips_missing_model 邊界條件
- DB 失敗降級行為 test 保留(只 patch get_session_factory,無 FakeRepo)

## test_aider_event_processor.py(完全重寫)
- 移除 FakeRepo/FakeSession,改用真實 PostgreSQL(real_factory fixture)
- Redis xack + IncidentEngine 保留 mock(外部 broker/AI 服務,符合例外)
- 每個測試後 rollback,不污染 dev DB

## setup_test_schema.sql
- 補入 aider_events_payload_gin GIN index(與 adr091 生產 migration 一致)

## integration/conftest.py
- 補注解說明密碼名稱 awoooi_prod_2026 的歷史混淆
- 修正 assert 邏輯:檢查 DB 名稱而非 URL 字串,避免密碼含 prod 觸發誤判

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

164 lines
6.2 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.
# test_aider_event_processor | 2026-04-22 @ Asia/Taipei
# 重構:移除 FakeRepo/FakeSession違反 feedback_no_mock_testing.md
# 方案_process_one 加 _session_factory DI 參數,測試注入真實 asyncpg 連線。
# Redis xack + IncidentEngine 仍 mock外部 broker/AI 服務,符合「外部 API 例外」)。
"""Unit tests for AiderEventProcessor — 真實 DB + mock 外部服務。"""
import json
import os
from datetime import datetime, timedelta, timezone
from unittest.mock import AsyncMock, MagicMock
import pytest
import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from src.repositories.aider_event_repository import AiderEventRepository
from src.workers.aider_event_processor import AiderEventProcessor
TAIPEI = timezone(timedelta(hours=8))
_DEV_DB_URL = os.environ.get(
"TEST_DATABASE_URL",
"postgresql+asyncpg://awoooi:awoooi_prod_2026@192.168.0.188:5432/awoooi_dev?ssl=disable",
)
# =============================================================================
# 真實 DB session factory fixture每個測試後 rollback
# =============================================================================
@pytest_asyncio.fixture
async def real_factory():
"""提供真實 PostgreSQL session factory測試後 rollback不污染 DB"""
engine = create_async_engine(_DEV_DB_URL, echo=False)
conn = await engine.connect().__aenter__()
await conn.begin()
session = AsyncSession(bind=conn, expire_on_commit=False, autoflush=False)
# 回傳一個永遠回傳同一個 session 的 factory讓 _process_one 正常使用 async with
class _SingleSessionFactory:
def __call__(self):
return _SessionCtx(session)
class _SessionCtx:
def __init__(self, sess): self._sess = sess
async def __aenter__(self): return self._sess
async def __aexit__(self, *a): pass
yield _SingleSessionFactory()
await conn.rollback()
await conn.__aexit__(None, None, None)
await engine.dispose()
def _payload_dict(type_="error"):
base = {
"ts": datetime.now(TAIPEI).isoformat(),
"session_id": "test-s1", "host": "ogt-mac",
"type": type_,
"payload": {"cwd": "/r", "model": "elephant-alpha",
"kind": "api_rate_limit", "message": "429",
"context_50chars": ""},
}
if type_ == "session_start":
base["payload"] = {"cwd": "/r", "model": "m", "aider_args": [],
"aider_pid": 1, "cli_version": "0"}
return base
# =============================================================================
# Tests — 真實 DB write
# =============================================================================
@pytest.mark.asyncio
async def test_process_one_error_event_creates_incident_and_writes_db(real_factory):
fake_incident = MagicMock()
fake_incident.incident_id = "inc-123"
fake_engine = MagicMock()
fake_engine.process_signal = AsyncMock(return_value=fake_incident)
fake_r = MagicMock()
fake_r.xack = AsyncMock()
proc = AiderEventProcessor()
# Patch 外部服務Redis + IncidentEngine保留真實 DB
proc._redis = fake_r
data = {b"payload": json.dumps(_payload_dict("error")).encode()}
import unittest.mock as um
with um.patch("src.workers.aider_event_processor.get_incident_engine", return_value=fake_engine), \
um.patch("src.workers.aider_event_processor.get_worker_redis", return_value=fake_r):
await proc._process_one("stream", "1-0", data, _session_factory=real_factory)
assert fake_engine.process_signal.called
fake_r.xack.assert_called_once()
# 驗證真實 DB 寫入
async with real_factory() as sess:
repo = AiderEventRepository(sess)
count = await repo.count_by_session("test-s1")
assert count == 1
@pytest.mark.asyncio
async def test_process_one_session_start_no_incident(real_factory):
fake_engine = MagicMock()
fake_engine.process_signal = AsyncMock()
fake_r = MagicMock()
fake_r.xack = AsyncMock()
proc = AiderEventProcessor()
data = {b"payload": json.dumps(_payload_dict("session_start")).encode()}
import unittest.mock as um
with um.patch("src.workers.aider_event_processor.get_incident_engine", return_value=fake_engine), \
um.patch("src.workers.aider_event_processor.get_worker_redis", return_value=fake_r):
await proc._process_one("stream", "1-0", data, _session_factory=real_factory)
assert not fake_engine.process_signal.called # session_start 不建 incident
fake_r.xack.assert_called_once()
async with real_factory() as sess:
repo = AiderEventRepository(sess)
count = await repo.count_by_session("test-s1")
assert count == 1
@pytest.mark.asyncio
async def test_process_one_malformed_payload_acks_and_skips(monkeypatch):
"""malformed JSON 應 ACK 避免卡 pending且不觸碰 DB。"""
fake_r = MagicMock()
fake_r.xack = AsyncMock()
monkeypatch.setattr("src.workers.aider_event_processor.get_worker_redis",
lambda: fake_r)
proc = AiderEventProcessor()
data = {b"payload": b"this is not json"}
await proc._process_one("stream", "1-0", data)
fake_r.xack.assert_called_once()
@pytest.mark.asyncio
async def test_incident_failure_still_writes_db(real_factory):
"""incident engine 壞掉時event 仍要進 DB不丟資料"""
fake_engine = MagicMock()
fake_engine.process_signal = AsyncMock(side_effect=RuntimeError("engine down"))
fake_r = MagicMock()
fake_r.xack = AsyncMock()
proc = AiderEventProcessor()
data = {b"payload": json.dumps(_payload_dict("error")).encode()}
import unittest.mock as um
with um.patch("src.workers.aider_event_processor.get_incident_engine", return_value=fake_engine), \
um.patch("src.workers.aider_event_processor.get_worker_redis", return_value=fake_r):
await proc._process_one("stream", "1-0", data, _session_factory=real_factory)
fake_r.xack.assert_called_once() # 仍 ACK
async with real_factory() as sess:
repo = AiderEventRepository(sess)
count = await repo.count_by_session("test-s1")
assert count == 1 # DB 依然寫入,即使 incident engine 壞了