1. 删除了 test/unit/external_systems/framework/ 下的废弃空测试目录 2. 修复多处测试断言逻辑、参数传递与测试数据构造 3. 新增日志级别、缓存令牌、流事件等DTO单元测试 4. 补充路由绑定、会话仓储、outbox仓储的测试覆盖 5. 更新测试用例中的异常类型、参数校验与业务逻辑断言
728 lines
24 KiB
Python
728 lines
24 KiB
Python
"""领域模型(dataclass) ↔ Output DTO(Pydantic) 转换单元测试。
|
||
|
||
覆盖 ``yuxi.scheduler.use_cases.mappers`` 模块的全部公开函数:
|
||
- ``to_task_output`` / ``to_run_log_output``:dataclass → Output DTO
|
||
- ``to_trigger_task_output``:dataclass + run_id → TriggerTaskOutput DTO
|
||
- ``to_handler_summary_output`` / ``to_list_handler_summary_output``
|
||
- ``to_count_by_status_output``:状态分布字典 → DTO
|
||
- ``to_get_health_output``:健康检查参数 → DTO
|
||
- ``daily_stat_to_dict`` / ``to_daily_stat_output`` / ``to_list_daily_stats_output``
|
||
- ``to_list_upcoming_output``
|
||
- ``to_list_anomalies_output``:异常任务分组映射 → DTO
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
import pytest
|
||
|
||
from yuxi.scheduler.core.models import (
|
||
DailyStat,
|
||
HandlerSummary,
|
||
ScheduledTask,
|
||
ScheduledTaskRunLog,
|
||
)
|
||
from yuxi.scheduler.use_cases.dto.scheduler import SchedulerConfigOutput
|
||
from yuxi.scheduler.use_cases.mappers import (
|
||
daily_stat_to_dict,
|
||
to_count_by_status_output,
|
||
to_daily_stat_output,
|
||
to_get_health_output,
|
||
to_handler_summary_output,
|
||
to_list_anomalies_output,
|
||
to_list_daily_stats_output,
|
||
to_list_handler_summary_output,
|
||
to_list_upcoming_output,
|
||
to_run_log_output,
|
||
to_task_output,
|
||
to_trigger_task_output,
|
||
)
|
||
|
||
|
||
pytestmark = pytest.mark.unit
|
||
|
||
|
||
# ─── to_task_output ───────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToTaskOutput:
|
||
def test_basic_mapping(self):
|
||
# Arrange
|
||
task = ScheduledTask(
|
||
task_id="task-001",
|
||
handler_name="my_handler",
|
||
owner_scope="system",
|
||
owner_id="test:1",
|
||
schedule_kind="cron",
|
||
cron_expression="*/15 * * * *",
|
||
tz="Asia/Shanghai",
|
||
payload={"k": "v"},
|
||
enabled=True,
|
||
stagger_seconds=30,
|
||
status="active",
|
||
next_run_at=datetime(2024, 1, 1, 0, 15, 0),
|
||
id=1,
|
||
)
|
||
# Act
|
||
output = to_task_output(task)
|
||
# Assert
|
||
assert output.task_id == "task-001"
|
||
assert output.handler_name == "my_handler"
|
||
assert output.owner_scope == "system"
|
||
assert output.schedule_kind == "cron"
|
||
assert output.cron_expression == "*/15 * * * *"
|
||
assert output.payload == {"k": "v"}
|
||
assert output.stagger_seconds == 30
|
||
assert output.status == "active"
|
||
assert output.id == 1
|
||
|
||
def test_datetime_fields_formatted_as_iso(self):
|
||
# Arrange - naive datetime 按 UTC 处理(与 DB 存储约定一致)
|
||
next_run = datetime(2024, 1, 1, 0, 15, 0)
|
||
task = ScheduledTask(
|
||
task_id="t1",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
next_run_at=next_run,
|
||
)
|
||
# Act
|
||
output = to_task_output(task)
|
||
# Assert - DateTime 字段格式化为 ISO 8601 字符串,naive UTC 不偏移
|
||
assert isinstance(output.next_run_at, str)
|
||
assert output.next_run_at == "2024-01-01T00:15:00Z"
|
||
|
||
def test_none_datetime_fields_return_none(self):
|
||
# Arrange
|
||
task = ScheduledTask(
|
||
task_id="t1",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
)
|
||
# Act
|
||
output = to_task_output(task)
|
||
# Assert
|
||
assert output.next_run_at is None
|
||
assert output.last_run_at is None
|
||
assert output.run_at is None
|
||
assert output.created_at is None
|
||
assert output.updated_at is None
|
||
assert output.deleted_at is None
|
||
|
||
def test_none_payload_returns_empty_dict(self):
|
||
# Arrange - dataclass payload 为 None 时 mapper 兜底为空 dict
|
||
task = ScheduledTask(
|
||
task_id="t1",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
payload=None, # type: ignore[arg-type]
|
||
)
|
||
# Act
|
||
output = to_task_output(task)
|
||
# Assert
|
||
assert output.payload == {}
|
||
|
||
|
||
# ─── to_trigger_task_output ───────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToTriggerTaskOutput:
|
||
def test_basic_mapping(self):
|
||
# Arrange
|
||
task = ScheduledTask(
|
||
task_id="task-001",
|
||
handler_name="my_handler",
|
||
owner_scope="system",
|
||
owner_id="test:1",
|
||
schedule_kind="cron",
|
||
)
|
||
run_id = "run-abc"
|
||
# Act
|
||
output = to_trigger_task_output(task, run_id)
|
||
# Assert
|
||
assert output.run_id == "run-abc"
|
||
assert output.task.task_id == "task-001"
|
||
assert output.task.handler_name == "my_handler"
|
||
|
||
def test_task_snapshot_is_full_task_output(self):
|
||
# Arrange - task 字段为完整 TaskOutput,包含所有映射字段
|
||
task = ScheduledTask(
|
||
task_id="t1",
|
||
handler_name="h",
|
||
owner_scope="business",
|
||
owner_id="o",
|
||
schedule_kind="at",
|
||
run_at=datetime(2024, 1, 1, 0, 0, 0),
|
||
status="active",
|
||
id=42,
|
||
)
|
||
# Act
|
||
output = to_trigger_task_output(task, "run-1")
|
||
# Assert
|
||
assert output.task.schedule_kind == "at"
|
||
assert output.task.run_at == "2024-01-01T00:00:00Z"
|
||
assert output.task.id == 42
|
||
|
||
|
||
# ─── to_run_log_output ────────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToRunLogOutput:
|
||
def test_basic_mapping(self):
|
||
# Arrange
|
||
log = ScheduledTaskRunLog(
|
||
task_id="task-001",
|
||
run_id="run-001",
|
||
triggered_by="auto",
|
||
status="success",
|
||
output={"rows": 10},
|
||
started_at=datetime(2024, 1, 1, 0, 15, 0),
|
||
finished_at=datetime(2024, 1, 1, 0, 15, 5),
|
||
id=1,
|
||
)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert
|
||
assert output.task_id == "task-001"
|
||
assert output.run_id == "run-001"
|
||
assert output.triggered_by == "auto"
|
||
assert output.status == "success"
|
||
assert output.output == {"rows": 10}
|
||
assert output.id == 1
|
||
|
||
def test_datetime_fields_formatted_as_iso(self):
|
||
# Arrange - naive datetime 按 UTC 处理(与 DB 存储约定一致)
|
||
started = datetime(2024, 1, 1, 0, 15, 0)
|
||
log = ScheduledTaskRunLog(
|
||
task_id="t",
|
||
run_id="r",
|
||
started_at=started,
|
||
)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert - naive UTC 不偏移
|
||
assert isinstance(output.started_at, str)
|
||
assert output.started_at == "2024-01-01T00:15:00Z"
|
||
|
||
def test_none_datetime_fields_return_none(self):
|
||
log = ScheduledTaskRunLog(task_id="t", run_id="r")
|
||
output = to_run_log_output(log)
|
||
assert output.started_at is None
|
||
assert output.finished_at is None
|
||
|
||
def test_none_output_returns_none(self):
|
||
# Arrange - output 为 None 时 mapper 透传 None
|
||
log = ScheduledTaskRunLog(task_id="t", run_id="r", output=None)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert
|
||
assert output.output is None
|
||
|
||
def test_none_error_message_returns_none(self):
|
||
# Arrange - error_message 为 None 时 mapper 透传 None
|
||
log = ScheduledTaskRunLog(task_id="t", run_id="r", error_message=None)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert
|
||
assert output.error_message is None
|
||
|
||
def test_all_fields_mapped(self):
|
||
# Arrange - 验证全字段映射
|
||
log = ScheduledTaskRunLog(
|
||
task_id="t1",
|
||
handler_name="email_handler",
|
||
run_id="r1",
|
||
triggered_by="manual",
|
||
status="failure",
|
||
error_message="boom",
|
||
output={"error": True},
|
||
started_at=datetime(2024, 1, 1, 0, 15, 0),
|
||
finished_at=datetime(2024, 1, 1, 0, 15, 5),
|
||
created_by="admin",
|
||
updated_by="operator",
|
||
id=42,
|
||
created_at=datetime(2024, 1, 1, 0, 0, 0),
|
||
updated_at=datetime(2024, 1, 1, 0, 0, 1),
|
||
is_deleted=0,
|
||
deleted_at=None,
|
||
)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert
|
||
assert output.task_id == "t1"
|
||
assert output.handler_name == "email_handler"
|
||
assert output.run_id == "r1"
|
||
assert output.triggered_by == "manual"
|
||
assert output.status == "failure"
|
||
assert output.error_message == "boom"
|
||
assert output.output == {"error": True}
|
||
assert output.created_by == "admin"
|
||
assert output.updated_by == "operator"
|
||
assert output.id == 42
|
||
assert output.is_deleted == 0
|
||
assert output.deleted_at is None
|
||
|
||
def test_duration_seconds_calculated_from_started_and_finished(self):
|
||
# Arrange - finished_at - started_at = 5.0s
|
||
log = ScheduledTaskRunLog(
|
||
task_id="t",
|
||
run_id="r",
|
||
started_at=datetime(2024, 1, 1, 0, 15, 0),
|
||
finished_at=datetime(2024, 1, 1, 0, 15, 5),
|
||
)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert
|
||
assert output.duration_seconds == 5.0
|
||
|
||
def test_duration_seconds_none_when_finished_at_missing(self):
|
||
# Arrange - running 状态 finished_at 为 None
|
||
log = ScheduledTaskRunLog(
|
||
task_id="t",
|
||
run_id="r",
|
||
started_at=datetime(2024, 1, 1, 0, 15, 0),
|
||
finished_at=None,
|
||
)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert
|
||
assert output.duration_seconds is None
|
||
|
||
def test_duration_seconds_with_milliseconds(self):
|
||
# Arrange - 1.5s 时长,验证毫秒精度(保留 3 位小数)
|
||
log = ScheduledTaskRunLog(
|
||
task_id="t",
|
||
run_id="r",
|
||
started_at=datetime(2024, 1, 1, 0, 15, 0, 0),
|
||
finished_at=datetime(2024, 1, 1, 0, 15, 1, 500000),
|
||
)
|
||
# Act
|
||
output = to_run_log_output(log)
|
||
# Assert
|
||
assert output.duration_seconds == 1.5
|
||
|
||
|
||
# ─── to_handler_summary_output ────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToHandlerSummaryOutput:
|
||
def test_basic_mapping(self):
|
||
# Arrange
|
||
summary = HandlerSummary(
|
||
name="my_handler",
|
||
task_count=5,
|
||
active_task_count=3,
|
||
dead_letter_count=1,
|
||
last_active_at=datetime(2024, 1, 1, 0, 0, 0),
|
||
)
|
||
# Act
|
||
output = to_handler_summary_output(summary)
|
||
# Assert
|
||
assert output.handler_name == "my_handler"
|
||
assert output.task_count == 5
|
||
assert output.active_task_count == 3
|
||
assert output.dead_letter_count == 1
|
||
assert isinstance(output.last_active_at, str)
|
||
|
||
def test_none_last_active_at(self):
|
||
summary = HandlerSummary(name="h", task_count=0, last_active_at=None)
|
||
output = to_handler_summary_output(summary)
|
||
assert output.last_active_at is None
|
||
|
||
def test_active_task_count_defaults_zero(self):
|
||
summary = HandlerSummary(name="h", task_count=0)
|
||
output = to_handler_summary_output(summary)
|
||
assert output.active_task_count == 0
|
||
|
||
def test_dead_letter_count_defaults_zero(self):
|
||
# Arrange - dead_letter_count 未提供时默认 0
|
||
summary = HandlerSummary(name="h", task_count=0)
|
||
# Act
|
||
output = to_handler_summary_output(summary)
|
||
# Assert
|
||
assert output.dead_letter_count == 0
|
||
|
||
|
||
# ─── to_list_handler_summary_output ───────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToListHandlerSummaryOutput:
|
||
def test_empty_list(self):
|
||
output = to_list_handler_summary_output([])
|
||
assert output.items == []
|
||
|
||
def test_multiple_items(self):
|
||
summaries = [
|
||
HandlerSummary(name="h1", task_count=1),
|
||
HandlerSummary(name="h2", task_count=2),
|
||
]
|
||
output = to_list_handler_summary_output(summaries)
|
||
assert len(output.items) == 2
|
||
assert output.items[0].handler_name == "h1"
|
||
assert output.items[1].handler_name == "h2"
|
||
|
||
|
||
# ─── to_count_by_status_output ────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToCountByStatusOutput:
|
||
def test_all_statuses_present(self):
|
||
# Arrange
|
||
dist = {"active": 10, "paused": 5, "dead_letter": 2}
|
||
# Act
|
||
output = to_count_by_status_output(dist)
|
||
# Assert
|
||
assert output.active == 10
|
||
assert output.paused == 5
|
||
assert output.dead_letter == 2
|
||
|
||
def test_missing_statuses_default_zero(self):
|
||
# Arrange - 仓储层未出现的状态不在字典中
|
||
dist = {"active": 10}
|
||
# Act
|
||
output = to_count_by_status_output(dist)
|
||
# Assert
|
||
assert output.active == 10
|
||
assert output.paused == 0
|
||
assert output.dead_letter == 0
|
||
|
||
def test_empty_dict(self):
|
||
output = to_count_by_status_output({})
|
||
assert output.active == 0
|
||
assert output.paused == 0
|
||
assert output.dead_letter == 0
|
||
|
||
|
||
# ─── to_get_health_output ─────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToGetHealthOutput:
|
||
@staticmethod
|
||
def _make_config() -> SchedulerConfigOutput:
|
||
return SchedulerConfigOutput(
|
||
enabled=True,
|
||
tick_interval_seconds=60,
|
||
task_timeout_seconds=600,
|
||
max_consecutive_errors=5,
|
||
backoff_schedule=[10, 30, 120, 600, 3600],
|
||
stagger_max_seconds=300,
|
||
run_log_retention_days=90,
|
||
idempotency_retention_hours=24,
|
||
min_cron_interval_minutes=10,
|
||
)
|
||
|
||
def test_healthy_status(self):
|
||
# Arrange
|
||
last_tick = datetime(2024, 1, 1, 0, 0, 0)
|
||
# Act
|
||
output = to_get_health_output(
|
||
last_tick_at=last_tick,
|
||
active_task_count=10,
|
||
dead_letter_count=2,
|
||
running_count=3,
|
||
healthy=True,
|
||
config=self._make_config(),
|
||
)
|
||
# Assert
|
||
assert output.status == "healthy"
|
||
assert output.active_task_count == 10
|
||
assert output.dead_letter_count == 2
|
||
assert output.running_count == 3
|
||
assert isinstance(output.last_tick_at, str)
|
||
assert output.config.enabled is True
|
||
|
||
def test_unhealthy_status(self):
|
||
output = to_get_health_output(
|
||
last_tick_at=None,
|
||
active_task_count=0,
|
||
dead_letter_count=0,
|
||
running_count=0,
|
||
healthy=False,
|
||
config=self._make_config(),
|
||
)
|
||
assert output.status == "unhealthy"
|
||
assert output.last_tick_at is None
|
||
|
||
|
||
# ─── daily_stat_to_dict ───────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestDailyStatToDict:
|
||
def test_basic_mapping(self):
|
||
# Arrange
|
||
stat = DailyStat(
|
||
stat_date="2024-01-01",
|
||
handler_name="h",
|
||
success_count=10,
|
||
failure_count=2,
|
||
timeout_count=1,
|
||
dead_letter_count=0,
|
||
)
|
||
# Act
|
||
result = daily_stat_to_dict(stat)
|
||
# Assert
|
||
assert result["stat_date"] == "2024-01-01"
|
||
assert result["handler_name"] == "h"
|
||
assert result["success_count"] == 10
|
||
assert result["failure_count"] == 2
|
||
assert result["timeout_count"] == 1
|
||
assert result["dead_letter_count"] == 0
|
||
|
||
|
||
# ─── to_daily_stat_output ─────────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToDailyStatOutput:
|
||
def test_basic_mapping_with_derived_fields(self):
|
||
# Arrange
|
||
stat = DailyStat(
|
||
stat_date=datetime(2024, 1, 1).date(),
|
||
handler_name="h",
|
||
success_count=10,
|
||
failure_count=2,
|
||
timeout_count=1,
|
||
dead_letter_count=0,
|
||
)
|
||
# Act
|
||
output = to_daily_stat_output(stat)
|
||
# Assert
|
||
assert output.stat_date == "2024-01-01"
|
||
assert output.handler_name == "h"
|
||
assert output.success_count == 10
|
||
assert output.failure_count == 2
|
||
assert output.timeout_count == 1
|
||
assert output.dead_letter_count == 0
|
||
# 派生字段:total = 10 + 2 + 1 = 13
|
||
assert output.total_count == 13
|
||
# success_rate = 10 / 13
|
||
assert output.success_rate == pytest.approx(10 / 13)
|
||
|
||
def test_zero_total_count_success_rate_zero(self):
|
||
# Arrange - 所有计数为 0
|
||
stat = DailyStat(
|
||
stat_date=datetime(2024, 1, 1).date(),
|
||
handler_name="h",
|
||
success_count=0,
|
||
failure_count=0,
|
||
timeout_count=0,
|
||
dead_letter_count=0,
|
||
)
|
||
# Act
|
||
output = to_daily_stat_output(stat)
|
||
# Assert
|
||
assert output.total_count == 0
|
||
assert output.success_rate == 0.0
|
||
|
||
def test_dead_letter_count_included_in_total(self):
|
||
# Arrange - dead_letter_count 视为失败终态,纳入 total_count 分母
|
||
stat = DailyStat(
|
||
stat_date=datetime(2024, 1, 1).date(),
|
||
handler_name="h",
|
||
success_count=5,
|
||
failure_count=2,
|
||
timeout_count=1,
|
||
dead_letter_count=3,
|
||
)
|
||
# Act
|
||
output = to_daily_stat_output(stat)
|
||
# Assert - total = 5 + 2 + 1 + 3 = 11(含 dead_letter)
|
||
assert output.total_count == 11
|
||
assert output.success_rate == pytest.approx(5 / 11)
|
||
|
||
|
||
# ─── to_list_daily_stats_output ───────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToListDailyStatsOutput:
|
||
def test_empty_list(self):
|
||
output = to_list_daily_stats_output([])
|
||
assert output.items == []
|
||
|
||
def test_multiple_items(self):
|
||
stats = [
|
||
DailyStat(
|
||
stat_date=datetime(2024, 1, 1).date(),
|
||
handler_name="h1",
|
||
success_count=1,
|
||
failure_count=0,
|
||
timeout_count=0,
|
||
dead_letter_count=0,
|
||
),
|
||
DailyStat(
|
||
stat_date=datetime(2024, 1, 2).date(),
|
||
handler_name="h2",
|
||
success_count=2,
|
||
failure_count=0,
|
||
timeout_count=0,
|
||
dead_letter_count=0,
|
||
),
|
||
]
|
||
output = to_list_daily_stats_output(stats)
|
||
assert len(output.items) == 2
|
||
assert output.items[0].handler_name == "h1"
|
||
assert output.items[1].handler_name == "h2"
|
||
|
||
|
||
# ─── to_list_upcoming_output ──────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToListUpcomingOutput:
|
||
def test_empty_list(self):
|
||
output = to_list_upcoming_output([])
|
||
assert output.items == []
|
||
|
||
def test_multiple_tasks(self):
|
||
tasks = [
|
||
ScheduledTask(
|
||
task_id="t1",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
),
|
||
ScheduledTask(
|
||
task_id="t2",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
),
|
||
]
|
||
output = to_list_upcoming_output(tasks)
|
||
assert len(output.items) == 2
|
||
assert output.items[0].task_id == "t1"
|
||
assert output.items[1].task_id == "t2"
|
||
|
||
|
||
# ─── to_list_anomalies_output ─────────────────────────────────────────────
|
||
|
||
|
||
@pytest.mark.unit
|
||
class TestToListAnomaliesOutput:
|
||
def test_empty_groups_returns_empty_lists(self):
|
||
# Arrange & Act - 空分组字典
|
||
output = to_list_anomalies_output({})
|
||
# Assert - 三个列表都应为空
|
||
assert output.dead_letter == []
|
||
assert output.consecutive_failure == []
|
||
assert output.stale == []
|
||
|
||
def test_dead_letter_group_mapped(self):
|
||
# Arrange
|
||
task = ScheduledTask(
|
||
task_id="t1",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
status="dead_letter",
|
||
)
|
||
# Act
|
||
output = to_list_anomalies_output({"dead_letter": [task]})
|
||
# Assert
|
||
assert len(output.dead_letter) == 1
|
||
assert output.dead_letter[0].task_id == "t1"
|
||
assert output.consecutive_failure == []
|
||
assert output.stale == []
|
||
|
||
def test_consecutive_failure_group_mapped(self):
|
||
# Arrange
|
||
task = ScheduledTask(
|
||
task_id="t2",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
consecutive_errors=5,
|
||
)
|
||
# Act
|
||
output = to_list_anomalies_output({"consecutive_failure": [task]})
|
||
# Assert
|
||
assert len(output.consecutive_failure) == 1
|
||
assert output.consecutive_failure[0].task_id == "t2"
|
||
assert output.dead_letter == []
|
||
assert output.stale == []
|
||
|
||
def test_stale_group_mapped(self):
|
||
# Arrange
|
||
task = ScheduledTask(
|
||
task_id="t3",
|
||
handler_name="h",
|
||
owner_scope="s",
|
||
owner_id="o",
|
||
schedule_kind="cron",
|
||
last_run_at=datetime(2024, 1, 1, 0, 0, 0),
|
||
)
|
||
# Act
|
||
output = to_list_anomalies_output({"stale": [task]})
|
||
# Assert
|
||
assert len(output.stale) == 1
|
||
assert output.stale[0].task_id == "t3"
|
||
assert output.dead_letter == []
|
||
assert output.consecutive_failure == []
|
||
|
||
def test_all_groups_mapped(self):
|
||
# Arrange - 三类异常任务同时存在
|
||
groups = {
|
||
"dead_letter": [
|
||
ScheduledTask(
|
||
task_id="dl-1", handler_name="h", owner_scope="s",
|
||
owner_id="o", schedule_kind="cron", status="dead_letter",
|
||
),
|
||
],
|
||
"consecutive_failure": [
|
||
ScheduledTask(
|
||
task_id="cf-1", handler_name="h", owner_scope="s",
|
||
owner_id="o", schedule_kind="cron", consecutive_errors=3,
|
||
),
|
||
],
|
||
"stale": [
|
||
ScheduledTask(
|
||
task_id="st-1", handler_name="h", owner_scope="s",
|
||
owner_id="o", schedule_kind="cron",
|
||
last_run_at=datetime(2024, 1, 1, 0, 0, 0),
|
||
),
|
||
],
|
||
}
|
||
# Act
|
||
output = to_list_anomalies_output(groups)
|
||
# Assert
|
||
assert len(output.dead_letter) == 1
|
||
assert output.dead_letter[0].task_id == "dl-1"
|
||
assert len(output.consecutive_failure) == 1
|
||
assert output.consecutive_failure[0].task_id == "cf-1"
|
||
assert len(output.stale) == 1
|
||
assert output.stale[0].task_id == "st-1"
|
||
|
||
def test_missing_group_key_defaults_to_empty_list(self):
|
||
# Arrange - 分组字典只含部分 key,缺失的 key 应默认空列表
|
||
groups = {"dead_letter": [
|
||
ScheduledTask(
|
||
task_id="t1", handler_name="h", owner_scope="s",
|
||
owner_id="o", schedule_kind="cron",
|
||
),
|
||
]}
|
||
# Act
|
||
output = to_list_anomalies_output(groups)
|
||
# Assert - 缺失的 consecutive_failure / stale 默认空列表
|
||
assert len(output.dead_letter) == 1
|
||
assert output.consecutive_failure == []
|
||
assert output.stale == []
|