"""P1-E 回归测试: 启动时将过期 pending/running ingest_jobs 标 failed。 运行: pytest tests/test_p1_e_stale_ingest_jobs.py -v (使用 mock session,验证 UPDATE 过滤条件与 SET 值。) """ from __future__ import annotations from unittest.mock import MagicMock, patch import pytest import src.api.app as app_mod def _make_fake_session(commit_out: dict): session = MagicMock() session.__aenter__ = MagicMock(return_value=session) session.__aexit__ = MagicMock(return_value=False) async def execute(stmt): commit_out["sql"] = str(stmt) result = MagicMock() result.rowcount = 2 return result async def commit(): commit_out["commit"] = commit_out.get("commit", 0) + 1 session.execute = execute session.commit = commit return session @pytest.mark.asyncio async def test_fail_stale_updates_pending_and_running_only(): """P1-E: UPDATE 必须过滤 status IN ('pending','running'),SET status=failed。""" out = {} fake = _make_fake_session(out) class FakeSessionLocal: def __init__(self): self._s = fake async def __aenter__(self): return self._s async def __aexit__(self, *a): return None # patch src.db.base.AsyncSessionLocal(函数内 from-import 每次调用从此取) import src.db.base as _base with patch.object(_base, "AsyncSessionLocal", FakeSessionLocal): await app_mod._fail_stale_ingest_jobs() sql = out.get("sql", "").lower() # WHERE 子句过滤 status IN (绑定参数,SQLAlchemy 用 __[postcompile_x] 占位) assert "status in" in sql, f"SQL 缺少 status IN 过滤: {sql}" # SET status=failed assert "status=:status" in sql or "status=" in sql, f"SQL 缺少 status 更新: {sql}" # 应清理成功(commit 被调用) assert out.get("commit", 0) >= 1, f"应已提交,实际 commit 调用: {out}" @pytest.mark.asyncio async def test_fail_stale_does_not_raise_on_db_error(): """P1-E: DB 异常不得阻断启动(仅记 warning)。""" class BoomSessionLocal: async def __aenter__(self): raise RuntimeError("DB down") async def __aexit__(self, *a): return None import src.db.base as _base with patch.object(_base, "AsyncSessionLocal", BoomSessionLocal): # 不应抛异常 await app_mod._fail_stale_ingest_jobs() assert True