"""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}