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>
170 lines
6.5 KiB
Python
170 lines
6.5 KiB
Python
# 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
|