baseline: 批次A-D 成果 + membership 半成品(测试红)

This commit is contained in:
agent
2026-09-11 23:11:35 +08:00
commit b3f3095d53
311 changed files with 40540 additions and 0 deletions
View File
+204
View File
@@ -0,0 +1,204 @@
"""状态机服务测试(纯内存:use_transaction=False 跳过 DB)。"""
import pytest
from apps.core.workflow import StateMachine, StateTransitionError, TransitionContext
class Stub:
def __init__(self, state="draft"):
self.state = state
self.saved = []
def save(self, update_fields=None):
self.saved.append((self.state, update_fields))
def make_sm():
return StateMachine(
states=("draft", "confirmed", "posted", "closed", "cancelled"),
initial="draft",
transitions={
"draft": {"confirm": "confirmed", "cancel": "cancelled"},
"confirmed": {"post": "posted", "cancel": "cancelled"},
"posted": {"close": "closed"},
"closed": {},
"cancelled": {},
},
)
def test_get_next_states():
sm = make_sm()
assert sm.get_next_states("draft") == ["confirmed", "cancelled"]
assert sm.get_next_states("posted") == ["closed"]
assert sm.get_next_states("closed") == []
def test_can_transition():
sm = make_sm()
obj = Stub()
assert sm.can_transition(obj, "confirm") is True
assert sm.can_transition(obj, "post") is False
def test_transition_changes_state():
sm = make_sm()
obj = Stub("draft")
ctx = sm.transition(obj, "confirm", use_transaction=False)
assert obj.state == "confirmed"
assert obj.saved == [] # state machine 不自动 save
assert isinstance(ctx, TransitionContext)
assert ctx.from_state == "draft"
assert ctx.to_state == "confirmed"
assert ctx.event == "confirm"
def test_invalid_event_raises():
sm = make_sm()
obj = Stub("posted")
with pytest.raises(StateTransitionError):
sm.transition(obj, "confirm", use_transaction=False)
def test_invalid_from_state_raises():
sm = make_sm()
obj = Stub("unknown_state")
with pytest.raises(StateTransitionError):
sm.transition(obj, "confirm", use_transaction=False)
def test_guard_blocks_transition():
blocked = []
def guard(ctx):
blocked.append(ctx)
return False # 显式拒绝
sm = StateMachine(
states=("draft", "confirmed"),
initial="draft",
transitions={"draft": {"confirm": "confirmed"}},
guards={("draft", "confirmed", "confirm"): guard},
)
obj = Stub("draft")
with pytest.raises(StateTransitionError):
sm.transition(obj, "confirm", user="alice", use_transaction=False)
assert blocked
assert obj.state == "draft" # 状态未被改
def test_guard_raising_also_blocks():
"""guard 抛异常也视为拒绝。"""
class BusinessError(Exception):
pass
def guard(ctx):
raise BusinessError("nope")
sm = StateMachine(
states=("draft", "confirmed"),
initial="draft",
transitions={"draft": {"confirm": "confirmed"}},
guards={("draft", "confirmed", "confirm"): guard},
)
obj = Stub("draft")
with pytest.raises(BusinessError):
sm.transition(obj, "confirm", use_transaction=False)
assert obj.state == "draft"
def test_guard_returns_ok_passes():
sm = StateMachine(
states=("draft", "confirmed"),
initial="draft",
transitions={"draft": {"confirm": "confirmed"}},
guards={("draft", "confirmed", "confirm"): lambda ctx: None},
)
obj = Stub("draft")
sm.transition(obj, "confirm", use_transaction=False)
assert obj.state == "confirmed"
def test_action_runs_after_state_change():
fired = []
def action(ctx):
fired.append((ctx.from_state, ctx.to_state))
sm = StateMachine(
states=("draft", "confirmed"),
initial="draft",
transitions={"draft": {"confirm": "confirmed"}},
actions={("draft", "confirmed", "confirm"): action},
)
obj = Stub("draft")
sm.transition(obj, "confirm", use_transaction=False)
assert fired == [("draft", "confirmed")]
def test_hooks_runs_after_action():
fired = []
def hook(ctx):
fired.append(ctx.event)
sm = StateMachine(
states=("draft", "confirmed"),
initial="draft",
transitions={"draft": {"confirm": "confirmed"}},
hooks={"confirm": [hook]},
)
obj = Stub("draft")
sm.transition(obj, "confirm", use_transaction=False)
assert fired == ["confirm"]
def test_extra_passed_in_context():
seen = []
def action(ctx):
seen.append(ctx.extra)
sm = StateMachine(
states=("draft", "confirmed"),
initial="draft",
transitions={"draft": {"confirm": "confirmed"}},
actions={("draft", "confirmed", "confirm"): action},
)
obj = Stub("draft")
sm.transition(obj, "confirm", extra={"warehouse_id": 1}, use_transaction=False)
assert seen == [{"warehouse_id": 1}]
def test_full_workflow_chain():
sm = make_sm()
obj = Stub("draft")
sm.transition(obj, "confirm", use_transaction=False)
assert obj.state == "confirmed"
sm.transition(obj, "post", use_transaction=False)
assert obj.state == "posted"
sm.transition(obj, "close", use_transaction=False)
assert obj.state == "closed"
with pytest.raises(StateTransitionError):
sm.transition(obj, "close", use_transaction=False)
def test_initial_must_be_in_states():
with pytest.raises(ValueError):
StateMachine(
states=("draft",),
initial="wrong",
transitions={},
)
def test_cancel_branch():
sm = make_sm()
obj = Stub("confirmed")
sm.transition(obj, "cancel", use_transaction=False)
assert obj.state == "cancelled"
# cancelled 是终态
with pytest.raises(StateTransitionError):
sm.transition(obj, "post", use_transaction=False)