Files
awoooi/apps/api/tests/test_aider_event_processor.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

170 lines
6.5 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-20 @ Asia/Taipei
# 2026-04-22 @ Asia/Taipei: DB/Redis mock 違反 feedback_no_mock_testing.md
# - FakeRepo / FakeSession → 已遷移至 integration/test_aider_event_repository.py真實 DB
# - fake_r (Redis xack) → 屬外部 broker保留 mock 符合「外部 API 例外」
# - fake_engine (IncidentEngine) → 屬外部 AI 呼叫,保留 mock 符合「外部 API 例外」
# 此檔案保留 _process_one 的 parse / ACK / incident routing 邏輯測試,
# DB 寫入行為已由 integration test 覆蓋。
"""Unit tests for AiderEventProcessor — parse/ACK/incident routing 邏輯。"""
import pytest
import json
from datetime import datetime, timezone, timedelta
from unittest.mock import AsyncMock, MagicMock, patch
from src.workers.aider_event_processor import AiderEventProcessor
TAIPEI = timezone(timedelta(hours=8))
def _payload_dict():
"""基本的 aider event payload。"""
return {
"ts": datetime.now(TAIPEI).isoformat(),
"session_id": "s1", "host": "ogt-mac",
"type": "error",
"payload": {"cwd": "/r", "model": "elephant-alpha",
"kind": "api_rate_limit", "message": "429",
"context_50chars": ""},
}
@pytest.mark.asyncio
async def test_process_one_error_event_creates_incident_and_writes_db(monkeypatch):
"""error event 應建 incident + 寫 DB。"""
# Mock incident engine
fake_incident = MagicMock()
fake_incident.incident_id = "inc-123"
fake_engine = MagicMock()
fake_engine.process_signal = AsyncMock(return_value=fake_incident)
monkeypatch.setattr("src.workers.aider_event_processor.get_incident_engine",
lambda: fake_engine)
# Mock DB session factory + repo
inserted = {}
class FakeRepo:
def __init__(self, sess): pass
async def insert(self, **kw): inserted.update(kw); return 1
class FakeSession:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def commit(self): pass
monkeypatch.setattr("src.workers.aider_event_processor.AiderEventRepository",
FakeRepo)
monkeypatch.setattr("src.workers.aider_event_processor.get_session_factory",
lambda: (lambda: FakeSession()))
# Mock redis ack
fake_r = MagicMock()
fake_r.xack = AsyncMock()
monkeypatch.setattr("src.workers.aider_event_processor.get_worker_redis",
lambda: fake_r)
# Act
proc = AiderEventProcessor()
payload = _payload_dict()
data = {b"payload": json.dumps(payload).encode()}
await proc._process_one("stream", "1-0", data)
# Assert
assert fake_engine.process_signal.called
assert inserted.get("incident_id") == "inc-123"
assert inserted.get("type_") == "error"
fake_r.xack.assert_called_once()
@pytest.mark.asyncio
async def test_process_one_session_start_no_incident(monkeypatch):
"""session_start 不應建 incident但應寫 DB。"""
fake_engine = MagicMock()
fake_engine.process_signal = AsyncMock()
monkeypatch.setattr("src.workers.aider_event_processor.get_incident_engine",
lambda: fake_engine)
inserted = {}
class FakeRepo:
def __init__(self, sess): pass
async def insert(self, **kw): inserted.update(kw); return 1
class FakeSession:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def commit(self): pass
monkeypatch.setattr("src.workers.aider_event_processor.AiderEventRepository",
FakeRepo)
monkeypatch.setattr("src.workers.aider_event_processor.get_session_factory",
lambda: (lambda: FakeSession()))
fake_r = MagicMock()
fake_r.xack = AsyncMock()
monkeypatch.setattr("src.workers.aider_event_processor.get_worker_redis",
lambda: fake_r)
# Act
proc = AiderEventProcessor()
payload = _payload_dict()
payload["type"] = "session_start"
payload["payload"] = {"cwd": "/r", "model": "m", "aider_args": [],
"aider_pid": 1, "cli_version": "0"}
data = {b"payload": json.dumps(payload).encode()}
await proc._process_one("stream", "1-0", data)
# Assert
assert not fake_engine.process_signal.called # session_start 不建 incident
assert inserted.get("incident_id") is None # DB 依然寫入
assert inserted.get("type_") == "session_start"
fake_r.xack.assert_called_once()
@pytest.mark.asyncio
async def test_process_one_malformed_payload_acks_and_skips(monkeypatch, caplog):
"""malformed JSON 應 ACK 避免卡 pending但不建 DB record。"""
fake_r = MagicMock()
fake_r.xack = AsyncMock()
monkeypatch.setattr("src.workers.aider_event_processor.get_worker_redis",
lambda: fake_r)
# Act
proc = AiderEventProcessor()
data = {b"payload": b"this is not json"}
await proc._process_one("stream", "1-0", data)
# Assert
fake_r.xack.assert_called_once() # 壞 payload ACK 避免卡 pending
@pytest.mark.asyncio
async def test_incident_failure_still_writes_db(monkeypatch):
"""incident engine 壞掉時event 仍要進 DB不丟資料"""
fake_engine = MagicMock()
fake_engine.process_signal = AsyncMock(side_effect=RuntimeError("engine down"))
monkeypatch.setattr("src.workers.aider_event_processor.get_incident_engine",
lambda: fake_engine)
inserted = {}
class FakeRepo:
def __init__(self, sess): pass
async def insert(self, **kw): inserted.update(kw); return 1
class FakeSession:
async def __aenter__(self): return self
async def __aexit__(self, *a): return False
async def commit(self): pass
monkeypatch.setattr("src.workers.aider_event_processor.AiderEventRepository",
FakeRepo)
monkeypatch.setattr("src.workers.aider_event_processor.get_session_factory",
lambda: (lambda: FakeSession()))
fake_r = MagicMock()
fake_r.xack = AsyncMock()
monkeypatch.setattr("src.workers.aider_event_processor.get_worker_redis",
lambda: fake_r)
# Act
proc = AiderEventProcessor()
payload = _payload_dict()
data = {b"payload": json.dumps(payload).encode()}
await proc._process_one("stream", "1-0", data)
# Assert
assert inserted.get("type_") == "error"
assert inserted.get("incident_id") is None # engine 壞,無 id
fake_r.xack.assert_called_once() # 仍 ACK