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.
This commit is contained in:
Executable
+66
@@ -0,0 +1,66 @@
|
||||
"""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}
|
||||
Reference in New Issue
Block a user