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.
67 lines
2.1 KiB
Python
Executable File
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}
|