"""Web run cancellation: broker fast path and shared-DB fallback.""" from __future__ import annotations import unittest from contextlib import contextmanager from types import SimpleNamespace from unittest.mock import MagicMock, patch from uuid import uuid4 from sqlalchemy.exc import SQLAlchemyError class RunCancelCheckTests(unittest.TestCase): def test_broker_signal_stops_without_db_poll(self) -> None: from web import runs tid = uuid4() broker = MagicMock() broker.is_cancelled.return_value = True with ( patch.object(runs, "broker", broker), patch.object(runs, "session_scope") as scope, ): check = runs._RunCancelCheck(tid) self.assertTrue(check()) scope.assert_not_called() broker.is_cancelled.assert_called_once_with(tid) def test_db_cancelling_falls_back_across_processes(self) -> None: from web import runs tid = uuid4() broker = MagicMock() broker.is_cancelled.return_value = False result = MagicMock() result.scalar_one_or_none.return_value = "cancelling" session = SimpleNamespace(execute=MagicMock(return_value=result)) @contextmanager def fake_scope(): yield session with ( patch.object(runs, "broker", broker), patch.object(runs, "session_scope", fake_scope), patch.object(runs.time, "monotonic", side_effect=[10.0, 10.5, 11.0]), ): check = runs._RunCancelCheck(tid) self.assertFalse(check(), "一秒节流窗口内不查询 DB") self.assertTrue(check(), "共享 DB 的 cancelling 应成为可靠兜底") session.execute.assert_called_once() def test_db_poll_failure_does_not_abort_run(self) -> None: from web import runs tid = uuid4() broker = MagicMock() broker.is_cancelled.return_value = False @contextmanager def broken_scope(): raise SQLAlchemyError("db unavailable") yield with ( patch.object(runs, "broker", broker), patch.object(runs, "session_scope", broken_scope), patch.object(runs.time, "monotonic", side_effect=[20.0, 21.0]), ): self.assertFalse(runs._RunCancelCheck(tid)()) if __name__ == "__main__": unittest.main()