fix(approval): replay immutable action responses
This commit is contained in:
@@ -228,6 +228,37 @@ def test_risk_disposition_idempotency_and_optimistic_version_are_enforced(
|
||||
assert replay.replayed is True
|
||||
assert replay.event.id == first.event.id
|
||||
assert db.scalar(select(func.count()).select_from(RiskDispositionEvent)) == 1
|
||||
stored_event = db.get(RiskDispositionEvent, first.event.id)
|
||||
assert stored_event is not None
|
||||
assert stored_event.response_json is not None
|
||||
assert stored_event.response_json["replayed"] is False
|
||||
assert stored_event.response_json["disposition"]["version"] == 1
|
||||
|
||||
advanced = service.execute_action(
|
||||
observation.id,
|
||||
_action(
|
||||
"start_remediation",
|
||||
version=1,
|
||||
request_id="request-remediation-after-idempotent",
|
||||
),
|
||||
tenant_id="default",
|
||||
actor_id="finance-1",
|
||||
actor_name="财务甲",
|
||||
)
|
||||
late_replay = service.execute_action(
|
||||
observation.id,
|
||||
payload,
|
||||
tenant_id="default",
|
||||
actor_id="finance-1",
|
||||
actor_name="财务甲",
|
||||
)
|
||||
|
||||
assert advanced.disposition.version == 2
|
||||
assert late_replay.replayed is True
|
||||
assert late_replay.disposition.version == 1
|
||||
assert late_replay.disposition.lifecycle_status == "open"
|
||||
assert [item.version for item in late_replay.disposition.events] == [1]
|
||||
assert db.scalar(select(func.count()).select_from(RiskDispositionEvent)) == 2
|
||||
|
||||
with pytest.raises(RiskDispositionIdempotencyConflictError):
|
||||
service.execute_action(
|
||||
@@ -268,7 +299,56 @@ def test_risk_disposition_idempotency_and_optimistic_version_are_enforced(
|
||||
actor_id="finance-1",
|
||||
actor_name="财务甲",
|
||||
)
|
||||
assert error.value.current_version == 1
|
||||
assert error.value.current_version == 2
|
||||
|
||||
|
||||
def test_legacy_event_replay_rebuilds_only_target_version_history(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
monkeypatch.setenv("FEW_SHOT_INJECTION_ENABLED", "false")
|
||||
with _build_session() as db:
|
||||
observation = RiskObservationService(db).upsert_observation(
|
||||
_observation_payload("risk:typed:legacy-replay")
|
||||
)
|
||||
db.commit()
|
||||
service = RiskDispositionService(db)
|
||||
payload = _action("confirm", version=0, request_id="request-legacy-replay-001")
|
||||
first = service.execute_action(
|
||||
observation.id,
|
||||
payload,
|
||||
tenant_id="default",
|
||||
actor_id="finance-1",
|
||||
actor_name="财务甲",
|
||||
)
|
||||
legacy_event = db.get(RiskDispositionEvent, first.event.id)
|
||||
assert legacy_event is not None
|
||||
legacy_event.response_json = None
|
||||
db.commit()
|
||||
|
||||
service.execute_action(
|
||||
observation.id,
|
||||
_action(
|
||||
"start_remediation",
|
||||
version=1,
|
||||
request_id="request-legacy-replay-later",
|
||||
),
|
||||
tenant_id="default",
|
||||
actor_id="finance-1",
|
||||
actor_name="财务甲",
|
||||
)
|
||||
replay = service.execute_action(
|
||||
observation.id,
|
||||
payload,
|
||||
tenant_id="default",
|
||||
actor_id="finance-1",
|
||||
actor_name="财务甲",
|
||||
)
|
||||
|
||||
assert replay.replayed is True
|
||||
assert replay.event.id == first.event.id
|
||||
assert replay.disposition.version == 1
|
||||
assert replay.disposition.lifecycle_status == "open"
|
||||
assert [item.version for item in replay.disposition.events] == [1]
|
||||
|
||||
|
||||
def test_risk_observation_api_enforces_pool_claim_and_typed_action_permissions(
|
||||
@@ -360,6 +440,26 @@ def test_risk_observation_api_enforces_pool_claim_and_typed_action_permissions(
|
||||
"comment": "人工复核确认",
|
||||
},
|
||||
)
|
||||
advanced = client.post(
|
||||
f"/api/v1/risk-observations/{observation_id}/disposition/actions",
|
||||
headers=finance_headers,
|
||||
json={
|
||||
"action": "request_supplement",
|
||||
"expected_version": 1,
|
||||
"request_id": "api-supplement-request-001",
|
||||
"comment": "请补充完整行程材料",
|
||||
},
|
||||
)
|
||||
late_replay = client.post(
|
||||
f"/api/v1/risk-observations/{observation_id}/disposition/actions",
|
||||
headers=finance_headers,
|
||||
json={
|
||||
"action": "confirm",
|
||||
"expected_version": 0,
|
||||
"request_id": "api-confirm-request-001",
|
||||
"comment": "人工复核确认",
|
||||
},
|
||||
)
|
||||
stale = client.post(
|
||||
f"/api/v1/risk-observations/{observation_id}/disposition/actions",
|
||||
headers=finance_headers,
|
||||
@@ -410,6 +510,13 @@ def test_risk_observation_api_enforces_pool_claim_and_typed_action_permissions(
|
||||
assert replay.status_code == 200
|
||||
assert replay.json()["replayed"] is True
|
||||
assert replay.json()["event"]["id"] == first.json()["event"]["id"]
|
||||
assert advanced.status_code == 200
|
||||
assert advanced.json()["disposition"]["version"] == 2
|
||||
assert late_replay.status_code == 200
|
||||
assert late_replay.json()["replayed"] is True
|
||||
assert late_replay.json()["disposition"]["version"] == 1
|
||||
assert late_replay.json()["disposition"]["lifecycle_status"] == "open"
|
||||
assert [item["version"] for item in late_replay.json()["disposition"]["events"]] == [1]
|
||||
assert stale.status_code == 409
|
||||
assert stale.json()["detail"]["code"] == "RISK_DISPOSITION_VERSION_CONFLICT"
|
||||
assert stale.json()["detail"]["message"] == "风险处置状态已更新,请刷新证据链后重试。"
|
||||
@@ -423,8 +530,8 @@ def test_risk_observation_api_enforces_pool_claim_and_typed_action_permissions(
|
||||
headers=finance_headers,
|
||||
)
|
||||
assert detail.status_code == 200
|
||||
assert detail.json()["disposition"]["version"] == 1
|
||||
assert len(detail.json()["disposition"]["events"]) == 1
|
||||
assert detail.json()["disposition"]["version"] == 2
|
||||
assert len(detail.json()["disposition"]["events"]) == 2
|
||||
|
||||
|
||||
def test_current_claim_approver_can_manage_disposition_without_pool_access() -> None:
|
||||
|
||||
Reference in New Issue
Block a user