Files
meida_backend/tests/unit/test_task_state.py
T
xsl 93b88db2f5 test: unit suite for the gateway invariants plus an integration slice
Covers routing, failover, the circuit breaker, structured-output retries, the
cache-prefix validator, cost computation including cached-token pricing, the
daily budget breaker, the quota protocol, the task state machine and log
redaction. HTTP is mocked -- no unit test spends money. The integration test
runs the real slice against PostgreSQL and Redis with a faked provider.
2026-08-14 00:26:39 +08:00

67 lines
2.1 KiB
Python
Executable File

"""tk_task state machine (架构文档 §6.2 task 域, FR-TASK-01)."""
from __future__ import annotations
from itertools import pairwise
import pytest
from packages.domain.errors import InvalidTaskTransitionError
from packages.domain.task.state import (
ALLOWED_TRANSITIONS,
TaskStatus,
TaskType,
assert_transition,
can_transition,
is_terminal,
needs_refund,
progress_for,
)
def test_happy_path() -> None:
path = [TaskStatus.PENDING, TaskStatus.QUEUED, TaskStatus.RUNNING, TaskStatus.SUCCEEDED]
for current, target in pairwise(path):
assert can_transition(current, target)
def test_terminal_states_are_final_except_retry() -> None:
assert ALLOWED_TRANSITIONS[TaskStatus.SUCCEEDED] == frozenset()
assert ALLOWED_TRANSITIONS[TaskStatus.CANCELED] == frozenset()
# FR-TASK-03: 失败项有原因与重试入口
assert can_transition(TaskStatus.FAILED, TaskStatus.QUEUED)
def test_illegal_transition_names_both_states() -> None:
with pytest.raises(InvalidTaskTransitionError) as excinfo:
assert_transition(TaskStatus.SUCCEEDED, TaskStatus.RUNNING)
assert excinfo.value.current == "succeeded"
assert excinfo.value.target == "running"
def test_cannot_skip_straight_from_pending_to_succeeded() -> None:
assert not can_transition(TaskStatus.PENDING, TaskStatus.SUCCEEDED)
def test_terminal_and_refund_classification() -> None:
assert is_terminal(TaskStatus.SUCCEEDED)
assert not is_terminal(TaskStatus.RUNNING)
# FR-TRY-05 / FR-BILL-05: 无「扣了额度但没出图」case
assert needs_refund(TaskStatus.FAILED)
assert needs_refund(TaskStatus.CANCELED)
assert not needs_refund(TaskStatus.SUCCEEDED)
def test_progress_is_defined_for_every_status() -> None:
for status in TaskStatus:
assert 0 <= progress_for(status) <= 100
assert progress_for(TaskStatus.SUCCEEDED) == 100
def test_every_status_has_a_transition_entry() -> None:
assert set(ALLOWED_TRANSITIONS) == set(TaskStatus)
def test_task_types_cover_the_long_running_work() -> None:
assert {"report", "tryon", "hairstyle", "matting", "render"} <= {t.value for t in TaskType}