新增覆盖以下模块的单元测试: 1. 核心契约与校验函数 2. 持久化层工作单元与ORM映射 3. 运行时处理器注册表 4. 业务用例工具类(退避、抖动、Cron计算) 5. 基础设施容器装配 6. 框架内置清理处理器(运行日志、幂等记录) 7. 测试共享Fixture与桩实现
631 lines
24 KiB
Python
631 lines
24 KiB
Python
"""SqlAlchemy 仓储适配器单元测试。
|
||
|
||
覆盖 ``yuxi.scheduler.adapters.persistence`` 下的 4 个仓储适配器:
|
||
- ``SqlAlchemyTaskRepository``:委托 ScheduledTaskRepository
|
||
- ``SqlAlchemyRunLogRepository``:委托 ScheduledTaskRunLogRepository
|
||
- ``SqlAlchemyRunLogDailyRepository``:委托 ScheduledTaskRunLogDailyRepository
|
||
- ``SqlAlchemyIdempotencyRepository``:委托 ScheduledTaskIdempotencyRepository
|
||
|
||
验证薄委托行为:调用实际仓储方法后通过 mappers 转换为 core dataclass,
|
||
``commit`` 等控制参数透传,不包含业务逻辑。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import date, datetime
|
||
from types import SimpleNamespace
|
||
from unittest.mock import AsyncMock, MagicMock, patch
|
||
|
||
import pytest
|
||
|
||
from yuxi.scheduler.adapters.persistence.sqlalchemy_idempotency_repo import (
|
||
SqlAlchemyIdempotencyRepository,
|
||
)
|
||
from yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_daily_repo import (
|
||
SqlAlchemyRunLogDailyRepository,
|
||
)
|
||
from yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_repo import (
|
||
SqlAlchemyRunLogRepository,
|
||
)
|
||
from yuxi.scheduler.adapters.persistence.sqlalchemy_task_repo import (
|
||
SqlAlchemyTaskRepository,
|
||
)
|
||
from yuxi.scheduler.core.models import (
|
||
DailyStat,
|
||
HandlerSummary,
|
||
ScheduledTask,
|
||
ScheduledTaskRunLog,
|
||
)
|
||
|
||
|
||
pytestmark = pytest.mark.unit
|
||
|
||
|
||
# ─── 测试辅助 ─────────────────────────────────────────────────────────────
|
||
|
||
|
||
def _make_task_orm(**overrides) -> SimpleNamespace:
|
||
"""构造类似 ORM ScheduledTask 的 SimpleNamespace。"""
|
||
defaults = {
|
||
"id": 1,
|
||
"task_id": "task-001",
|
||
"handler_name": "my_handler",
|
||
"owner_scope": "system",
|
||
"owner_id": "test:1",
|
||
"schedule_kind": "cron",
|
||
"cron_expression": "*/15 * * * *",
|
||
"run_at": None,
|
||
"tz": "Asia/Shanghai",
|
||
"payload": {"k": "v"},
|
||
"enabled": True,
|
||
"delete_after_run": False,
|
||
"block_strategy": "discard_later",
|
||
"stagger_seconds": 30,
|
||
"consecutive_errors": 0,
|
||
"status": "active",
|
||
"last_run_at": None,
|
||
"next_run_at": datetime(2024, 1, 1, 0, 15, 0),
|
||
"last_error": None,
|
||
"created_by": "system",
|
||
"updated_by": None,
|
||
"created_at": datetime(2024, 1, 1, 0, 0, 0),
|
||
"updated_at": None,
|
||
"is_deleted": 0,
|
||
"deleted_at": None,
|
||
}
|
||
defaults.update(overrides)
|
||
return SimpleNamespace(**defaults)
|
||
|
||
|
||
def _make_run_log_orm(**overrides) -> SimpleNamespace:
|
||
defaults = {
|
||
"id": 1,
|
||
"task_id": "task-001",
|
||
"run_id": "run-001",
|
||
"triggered_by": "auto",
|
||
"status": "success",
|
||
"error_message": None,
|
||
"output": {"rows": 10},
|
||
"started_at": datetime(2024, 1, 1, 0, 15, 0),
|
||
"finished_at": datetime(2024, 1, 1, 0, 15, 5),
|
||
"created_by": "system",
|
||
"updated_by": None,
|
||
"created_at": datetime(2024, 1, 1, 0, 0, 0),
|
||
"updated_at": None,
|
||
"is_deleted": 0,
|
||
"deleted_at": None,
|
||
}
|
||
defaults.update(overrides)
|
||
return SimpleNamespace(**defaults)
|
||
|
||
|
||
# ─── SqlAlchemyTaskRepository ─────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestSqlAlchemyTaskRepository:
|
||
@pytest.fixture
|
||
def repo_with_mock(self):
|
||
"""构造 SqlAlchemyTaskRepository,内部 ORM 仓储为 AsyncMock。"""
|
||
db = MagicMock()
|
||
with patch(
|
||
"yuxi.scheduler.adapters.persistence.sqlalchemy_task_repo.ORMScheduledTaskRepository"
|
||
) as mock_cls:
|
||
mock_orm_repo = AsyncMock()
|
||
mock_cls.return_value = mock_orm_repo
|
||
repo = SqlAlchemyTaskRepository(db)
|
||
return repo, mock_orm_repo
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_delegates_and_maps(self, repo_with_mock):
|
||
# Arrange
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.create = AsyncMock(return_value=_make_task_orm())
|
||
data = {"task_id": "task-001"}
|
||
# Act
|
||
result = await repo.create(data, commit=False)
|
||
# Assert
|
||
mock_orm.create.assert_awaited_once_with(data, commit=False)
|
||
assert isinstance(result, ScheduledTask)
|
||
assert result.task_id == "task-001"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_by_task_id_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get_by_task_id = AsyncMock(return_value=_make_task_orm())
|
||
# Act
|
||
result = await repo.get_by_task_id("task-001", for_update=True)
|
||
# Assert
|
||
mock_orm.get_by_task_id.assert_awaited_once_with("task-001", for_update=True)
|
||
assert isinstance(result, ScheduledTask)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_by_task_id_returns_none_when_orm_returns_none(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get_by_task_id = AsyncMock(return_value=None)
|
||
# Act
|
||
result = await repo.get_by_task_id("nonexistent")
|
||
# Assert
|
||
assert result is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_by_id_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get_by_id = AsyncMock(return_value=_make_task_orm())
|
||
# Act
|
||
result = await repo.get_by_id(1, for_update=False)
|
||
# Assert
|
||
mock_orm.get_by_id.assert_awaited_once_with(1, for_update=False)
|
||
assert isinstance(result, ScheduledTask)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.list = AsyncMock(return_value=([_make_task_orm(), _make_task_orm()], 2))
|
||
# Act
|
||
tasks, total = await repo.list(page=1, page_size=20)
|
||
# Assert
|
||
assert len(tasks) == 2
|
||
assert total == 2
|
||
assert all(isinstance(t, ScheduledTask) for t in tasks)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.update = AsyncMock(return_value=_make_task_orm(status="paused"))
|
||
# Act
|
||
result = await repo.update("task-001", {"status": "paused"}, commit=False)
|
||
# Assert
|
||
mock_orm.update.assert_awaited_once_with("task-001", {"status": "paused"}, commit=False)
|
||
assert isinstance(result, ScheduledTask)
|
||
assert result.status == "paused"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_delete_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.delete = AsyncMock(return_value=True)
|
||
# Act
|
||
result = await repo.delete("task-001", commit=False)
|
||
# Assert
|
||
mock_orm.delete.assert_awaited_once_with("task-001", commit=False)
|
||
assert result is True
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_due_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.list_due = AsyncMock(return_value=[_make_task_orm()])
|
||
now = datetime(2024, 1, 1, 0, 0, 0)
|
||
# Act
|
||
result = await repo.list_due(now, limit=500)
|
||
# Assert
|
||
mock_orm.list_due.assert_awaited_once_with(now, limit=500)
|
||
assert len(result) == 1
|
||
assert isinstance(result[0], ScheduledTask)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_upcoming_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.list_upcoming = AsyncMock(return_value=[_make_task_orm()])
|
||
after = datetime(2024, 1, 1, 0, 0, 0)
|
||
before = datetime(2024, 1, 2, 0, 0, 0)
|
||
# Act
|
||
result = await repo.list_upcoming(after=after, before=before, limit=50)
|
||
# Assert
|
||
mock_orm.list_upcoming.assert_awaited_once_with(
|
||
after=after, before=before, limit=50, handler_name=None
|
||
)
|
||
assert len(result) == 1
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_handler_summary_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
row = SimpleNamespace(name="h", task_count=5, last_active_at=None)
|
||
mock_orm.list_handler_summary = AsyncMock(return_value=[row])
|
||
# Act
|
||
result = await repo.list_handler_summary()
|
||
# Assert
|
||
assert len(result) == 1
|
||
assert isinstance(result[0], HandlerSummary)
|
||
assert result[0].name == "h"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_count_by_status_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.count_by_status = AsyncMock(return_value={"active": 10})
|
||
# Act
|
||
result = await repo.count_by_status(owner_scope="system")
|
||
# Assert
|
||
mock_orm.count_by_status.assert_awaited_once_with(
|
||
owner_scope="system", owner_id=None
|
||
)
|
||
assert result == {"active": 10}
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_acquire_for_run_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.acquire_for_run = AsyncMock(return_value=True)
|
||
now = datetime(2024, 1, 1, 0, 0, 0)
|
||
next_run = datetime(2024, 1, 1, 0, 15, 0)
|
||
# Act
|
||
result = await repo.acquire_for_run(
|
||
"task-001", run_id="run-001", now=now, next_run_at=next_run
|
||
)
|
||
# Assert
|
||
mock_orm.acquire_for_run.assert_awaited_once_with(
|
||
"task-001", run_id="run-001", now=now, next_run_at=next_run
|
||
)
|
||
assert result is True
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_pause_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.pause = AsyncMock(return_value=_make_task_orm(status="paused"))
|
||
# Act
|
||
result = await repo.pause("task-001", commit=False)
|
||
# Assert
|
||
mock_orm.pause.assert_awaited_once_with("task-001", commit=False)
|
||
assert isinstance(result, ScheduledTask)
|
||
assert result.status == "paused"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_resume_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.resume = AsyncMock(return_value=_make_task_orm(status="active"))
|
||
next_run = datetime(2024, 1, 1, 0, 15, 0)
|
||
# Act
|
||
result = await repo.resume("task-001", next_run_at=next_run, commit=False)
|
||
# Assert
|
||
mock_orm.resume.assert_awaited_once_with(
|
||
"task-001", next_run_at=next_run, commit=False
|
||
)
|
||
assert isinstance(result, ScheduledTask)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_mark_dead_letter_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.mark_dead_letter = AsyncMock(return_value=_make_task_orm(status="dead_letter"))
|
||
# Act
|
||
result = await repo.mark_dead_letter("task-001", commit=False)
|
||
# Assert
|
||
mock_orm.mark_dead_letter.assert_awaited_once_with("task-001", commit=False)
|
||
assert result.status == "dead_letter"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_reset_from_dead_letter_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.reset_from_dead_letter = AsyncMock(return_value=_make_task_orm(status="active"))
|
||
next_run = datetime(2024, 1, 1, 0, 15, 0)
|
||
# Act
|
||
result = await repo.reset_from_dead_letter("task-001", next_run_at=next_run)
|
||
# Assert
|
||
mock_orm.reset_from_dead_letter.assert_awaited_once_with(
|
||
"task-001", next_run_at=next_run, commit=True
|
||
)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_record_run_result_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.record_run_result = AsyncMock(
|
||
return_value=_make_task_orm(consecutive_errors=1)
|
||
)
|
||
# Act
|
||
result = await repo.record_run_result(
|
||
"task-001", success=False, error="boom", commit=False
|
||
)
|
||
# Assert
|
||
mock_orm.record_run_result.assert_awaited_once_with(
|
||
"task-001", success=False, error="boom", backoff_until=None, commit=False
|
||
)
|
||
assert result.consecutive_errors == 1
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_deleted_by_task_id_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get_deleted_by_task_id = AsyncMock(return_value=_make_task_orm(is_deleted=1))
|
||
# Act
|
||
result = await repo.get_deleted_by_task_id("task-001")
|
||
# Assert
|
||
assert isinstance(result, ScheduledTask)
|
||
assert result.is_deleted == 1
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_deleted_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.list_deleted = AsyncMock(return_value=[_make_task_orm(is_deleted=1)])
|
||
# Act
|
||
result = await repo.list_deleted(limit=10, offset=0)
|
||
# Assert
|
||
assert len(result) == 1
|
||
assert isinstance(result[0], ScheduledTask)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_hard_delete_by_id_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.hard_delete_by_id = AsyncMock(return_value=1)
|
||
# Act
|
||
result = await repo.hard_delete_by_id(1)
|
||
# Assert
|
||
mock_orm.hard_delete_by_id.assert_awaited_once_with(1, commit=True)
|
||
assert result == 1
|
||
|
||
|
||
# ─── SqlAlchemyRunLogRepository ───────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestSqlAlchemyRunLogRepository:
|
||
@pytest.fixture
|
||
def repo_with_mock(self):
|
||
db = MagicMock()
|
||
with patch(
|
||
"yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_repo.ORMScheduledTaskRunLogRepository"
|
||
) as mock_cls:
|
||
mock_orm_repo = AsyncMock()
|
||
mock_cls.return_value = mock_orm_repo
|
||
repo = SqlAlchemyRunLogRepository(db)
|
||
return repo, mock_orm_repo
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_create_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.create = AsyncMock(return_value=_make_run_log_orm())
|
||
# Act
|
||
result = await repo.create({"task_id": "t1"}, commit=False)
|
||
# Assert
|
||
mock_orm.create.assert_awaited_once_with({"task_id": "t1"}, commit=False)
|
||
assert isinstance(result, ScheduledTaskRunLog)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_by_run_id_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get_by_run_id = AsyncMock(return_value=_make_run_log_orm())
|
||
# Act
|
||
result = await repo.get_by_run_id("run-001")
|
||
# Assert
|
||
mock_orm.get_by_run_id.assert_awaited_once_with("run-001")
|
||
assert isinstance(result, ScheduledTaskRunLog)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_by_run_id_returns_none_when_not_found(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get_by_run_id = AsyncMock(return_value=None)
|
||
# Act
|
||
result = await repo.get_by_run_id("nonexistent")
|
||
# Assert
|
||
assert result is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.update = AsyncMock(return_value=_make_run_log_orm(status="success"))
|
||
# Act
|
||
result = await repo.update("run-001", {"status": "success"}, commit=False)
|
||
# Assert
|
||
mock_orm.update.assert_awaited_once_with("run-001", {"status": "success"}, commit=False)
|
||
assert result.status == "success"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_by_task_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.list_by_task = AsyncMock(return_value=([_make_run_log_orm()], 1))
|
||
# Act
|
||
logs, total = await repo.list_by_task("task-001", page=1, page_size=20)
|
||
# Assert
|
||
assert len(logs) == 1
|
||
assert total == 1
|
||
assert isinstance(logs[0], ScheduledTaskRunLog)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_by_status_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.list_by_status = AsyncMock(return_value=([_make_run_log_orm()], 1))
|
||
# Act
|
||
logs, total = await repo.list_by_status(status="running", page=1, page_size=20)
|
||
# Assert
|
||
assert len(logs) == 1
|
||
assert total == 1
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_running_by_task_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get_running_by_task = AsyncMock(return_value=_make_run_log_orm(status="running"))
|
||
# Act
|
||
result = await repo.get_running_by_task("task-001")
|
||
# Assert
|
||
assert isinstance(result, ScheduledTaskRunLog)
|
||
assert result.status == "running"
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_count_running_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.count_running = AsyncMock(return_value=3)
|
||
# Act
|
||
result = await repo.count_running()
|
||
# Assert
|
||
assert result == 3
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_reclaim_stale_runs_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.reclaim_stale_runs = AsyncMock(return_value=5)
|
||
stale_before = datetime(2024, 1, 1, 0, 0, 0)
|
||
# Act
|
||
result = await repo.reclaim_stale_runs(stale_before=stale_before)
|
||
# Assert
|
||
mock_orm.reclaim_stale_runs.assert_awaited_once_with(
|
||
stale_before=stale_before,
|
||
error_message="reclaimed by scheduler restart",
|
||
commit=True,
|
||
)
|
||
assert result == 5
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_cleanup_old_logs_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.cleanup_old_logs = AsyncMock(return_value=10)
|
||
before = datetime(2024, 1, 1, 0, 0, 0)
|
||
# Act
|
||
result = await repo.cleanup_old_logs(before, commit=False)
|
||
# Assert
|
||
mock_orm.cleanup_old_logs.assert_awaited_once_with(before, commit=False)
|
||
assert result == 10
|
||
|
||
|
||
# ─── SqlAlchemyRunLogDailyRepository ──────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestSqlAlchemyRunLogDailyRepository:
|
||
@pytest.fixture
|
||
def repo_with_mock(self):
|
||
db = MagicMock()
|
||
with patch(
|
||
"yuxi.scheduler.adapters.persistence.sqlalchemy_run_log_daily_repo.ORMScheduledTaskRunLogDailyRepository"
|
||
) as mock_cls:
|
||
mock_orm_repo = AsyncMock()
|
||
mock_cls.return_value = mock_orm_repo
|
||
repo = SqlAlchemyRunLogDailyRepository(db)
|
||
return repo, mock_orm_repo
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_upsert_daily_stat_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.upsert_daily_stat = AsyncMock(return_value=None)
|
||
stat_date = date(2024, 1, 1)
|
||
# Act
|
||
await repo.upsert_daily_stat(stat_date, "h", "success", commit=False)
|
||
# Assert
|
||
mock_orm.upsert_daily_stat.assert_awaited_once_with(
|
||
stat_date, "h", "success", commit=False
|
||
)
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_daily_stat_delegates_and_maps(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
row = SimpleNamespace(
|
||
stat_date=date(2024, 1, 1),
|
||
handler_name="h",
|
||
success_count=10,
|
||
failure_count=2,
|
||
timeout_count=1,
|
||
dead_letter_count=0,
|
||
)
|
||
mock_orm.list_daily_stat = AsyncMock(return_value=[row])
|
||
# Act
|
||
result = await repo.list_daily_stat(
|
||
start_date=date(2024, 1, 1), end_date=date(2024, 1, 31)
|
||
)
|
||
# Assert
|
||
assert len(result) == 1
|
||
assert isinstance(result[0], DailyStat)
|
||
assert result[0].success_count == 10
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_cleanup_old_daily_stats_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.cleanup_old_daily_stats = AsyncMock(return_value=5)
|
||
before = date(2023, 1, 1)
|
||
# Act
|
||
result = await repo.cleanup_old_daily_stats(before, commit=False)
|
||
# Assert
|
||
mock_orm.cleanup_old_daily_stats.assert_awaited_once_with(before, commit=False)
|
||
assert result == 5
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_deleted_by_id_passes_through_any(self, repo_with_mock):
|
||
# Arrange - core 层无对应 dataclass,按端口契约以 Any 透传 ORM
|
||
repo, mock_orm = repo_with_mock
|
||
orm_obj = SimpleNamespace(id=1)
|
||
mock_orm.get_deleted_by_id = AsyncMock(return_value=orm_obj)
|
||
# Act
|
||
result = await repo.get_deleted_by_id(1)
|
||
# Assert
|
||
assert result is orm_obj
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_list_deleted_passes_through_any(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
orm_list = [SimpleNamespace(id=1), SimpleNamespace(id=2)]
|
||
mock_orm.list_deleted = AsyncMock(return_value=orm_list)
|
||
# Act
|
||
result = await repo.list_deleted(limit=10)
|
||
# Assert
|
||
assert result == orm_list
|
||
|
||
|
||
# ─── SqlAlchemyIdempotencyRepository ──────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestSqlAlchemyIdempotencyRepository:
|
||
@pytest.fixture
|
||
def repo_with_mock(self):
|
||
db = MagicMock()
|
||
with patch(
|
||
"yuxi.scheduler.adapters.persistence.sqlalchemy_idempotency_repo.ORMScheduledTaskIdempotencyRepository"
|
||
) as mock_cls:
|
||
mock_orm_repo = AsyncMock()
|
||
mock_cls.return_value = mock_orm_repo
|
||
repo = SqlAlchemyIdempotencyRepository(db)
|
||
return repo, mock_orm_repo
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_acquire_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.acquire = AsyncMock(return_value=True)
|
||
# Act
|
||
result = await repo.acquire("key-001", task_id="t1", operation="run")
|
||
# Assert
|
||
mock_orm.acquire.assert_awaited_once_with("key-001", task_id="t1", operation="run")
|
||
assert result is True
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_update_response_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.update_response = AsyncMock(return_value=None)
|
||
# Act
|
||
await repo.update_response("key-001", {"result": "ok"})
|
||
# Assert
|
||
mock_orm.update_response.assert_awaited_once_with("key-001", {"result": "ok"})
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get = AsyncMock(return_value={"result": "ok"})
|
||
# Act
|
||
result = await repo.get("key-001")
|
||
# Assert
|
||
mock_orm.get.assert_awaited_once_with("key-001")
|
||
assert result == {"result": "ok"}
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_returns_none_when_not_found(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.get = AsyncMock(return_value=None)
|
||
# Act
|
||
result = await repo.get("nonexistent")
|
||
# Assert
|
||
assert result is None
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_cleanup_old_delegates(self, repo_with_mock):
|
||
repo, mock_orm = repo_with_mock
|
||
mock_orm.cleanup_old = AsyncMock(return_value=20)
|
||
before = datetime(2024, 1, 1, 0, 0, 0)
|
||
# Act
|
||
result = await repo.cleanup_old(before, commit=False)
|
||
# Assert
|
||
mock_orm.cleanup_old.assert_awaited_once_with(before, commit=False)
|
||
assert result == 20
|
||
|
||
@pytest.mark.asyncio
|
||
async def test_get_deleted_by_id_passes_through_any(self, repo_with_mock):
|
||
# Arrange - core 层无对应 dataclass,按端口契约以 Any 透传 ORM
|
||
repo, mock_orm = repo_with_mock
|
||
orm_obj = SimpleNamespace(id=1)
|
||
mock_orm.get_deleted_by_id = AsyncMock(return_value=orm_obj)
|
||
# Act
|
||
result = await repo.get_deleted_by_id(1)
|
||
# Assert
|
||
assert result is orm_obj
|