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