126 lines
3.9 KiB
Python
126 lines
3.9 KiB
Python
from datetime import datetime
|
|
from decimal import Decimal
|
|
|
|
import pytest
|
|
|
|
from app.core.errors import InvalidStateError
|
|
from app.model.profile_tag import AdvisorProfileDriftReview, AdvisorProfileTag
|
|
from app.service.profile_governance_service import ProfileGovernanceService
|
|
|
|
|
|
class _AsyncContext:
|
|
async def __aenter__(self) -> object:
|
|
return None
|
|
|
|
async def __aexit__(self, *_args: object) -> bool:
|
|
return False
|
|
|
|
|
|
class _Session:
|
|
def __init__(self, pending: object = None) -> None:
|
|
self.pending = pending
|
|
|
|
def begin(self) -> _AsyncContext:
|
|
return _AsyncContext()
|
|
|
|
|
|
class _SessionFactory:
|
|
def __init__(self, session: _Session) -> None:
|
|
self.session = session
|
|
|
|
def __call__(self) -> "_SessionContext":
|
|
return _SessionContext(self.session)
|
|
|
|
|
|
class _SessionContext:
|
|
def __init__(self, session: _Session) -> None:
|
|
self.session = session
|
|
|
|
async def __aenter__(self) -> _Session:
|
|
return self.session
|
|
|
|
async def __aexit__(self, *_args: object) -> bool:
|
|
return False
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_pending_profile_blocks_advisory_operation(monkeypatch) -> None:
|
|
session = _Session(pending=object())
|
|
|
|
async def pending(_self, _customer_id: int, *, lock: bool = False) -> object:
|
|
del lock
|
|
return session.pending
|
|
|
|
monkeypatch.setattr(
|
|
"app.service.profile_governance_service.RiskQuestionnaireRepository.pending_drift_review",
|
|
pending,
|
|
)
|
|
|
|
with pytest.raises(InvalidStateError, match="漂移正在复核"):
|
|
await ProfileGovernanceService(_SessionFactory(session)).require_operable(7)
|
|
|
|
|
|
class _Repository:
|
|
def __init__(self) -> None:
|
|
self.session = self
|
|
self.added: list[object] = []
|
|
self.sync_targets: list[str] = []
|
|
|
|
async def execute(self, _statement: object) -> None:
|
|
return None
|
|
|
|
async def deactivate_current_profile(self, _customer_id: int, _now: datetime) -> None:
|
|
return None
|
|
|
|
async def active_tags(
|
|
self, _customer_id: int, *, lock: bool = False
|
|
) -> list[AdvisorProfileTag]:
|
|
del lock
|
|
return []
|
|
|
|
async def supersede_active_tags(
|
|
self, _customer_id: int, _tag_keys: tuple[str, ...], _now: datetime
|
|
) -> None:
|
|
return None
|
|
|
|
def add_profile(self, profile: object) -> None:
|
|
self.added.append(profile)
|
|
|
|
def add_sync_event(self, event: object) -> None:
|
|
self.added.append(event)
|
|
self.sync_targets.append(event.target_store)
|
|
|
|
|
|
def _review() -> AdvisorProfileDriftReview:
|
|
now = datetime(2026, 9, 11)
|
|
return AdvisorProfileDriftReview(
|
|
id=4, drift_no="drift-4", customer_id=7, source_assessment_id=9,
|
|
candidate_profile_uuid="candidate-uuid", candidate_profile_version=2,
|
|
candidate_snapshot={"risk_level": "C3"},
|
|
candidate_generation_basis={"source": "assessment"},
|
|
changed_tags=[], status="pending_review", reviewer_user_id=None,
|
|
reviewed_at=None, review_comment=None, created_at=now, updated_at=now,
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_approved_review_activates_candidate_and_enqueues_both_projections() -> None:
|
|
repository = _Repository()
|
|
tag = AdvisorProfileTag(
|
|
id=11, tag_uuid="tag-11", customer_id=7, tag_key="risk_level", tag_value="C3",
|
|
tag_value_hash="x", confidence=Decimal("0.9000"), source_type="assessment",
|
|
source_reference="assessment:9", source_confidence=Decimal("1.0000"),
|
|
profile_version=2, drift_review_id=4, previous_tag_id=1, drift_reason="value_changed",
|
|
status="pending_review", active_customer_tag=None,
|
|
created_at=datetime(2026, 9, 11), updated_at=datetime(2026, 9, 11),
|
|
)
|
|
|
|
await ProfileGovernanceService()._approve(
|
|
repository, _review(), [tag], datetime(2026, 9, 11)
|
|
)
|
|
|
|
assert tag.status == "active"
|
|
assert tag.active_customer_tag == "7:risk_level"
|
|
assert repository.added[0].is_current is True
|
|
assert repository.sync_targets == ["milvus", "neo4j"]
|