"""叩心枢触发条件单元测试。""" # ruff: noqa: ARG002 — pytest fixture params are intentionally unused in test signatures from __future__ import annotations from datetime import UTC, datetime import pytest from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.modules.knocking.conditions import ( AnniversaryCondition, BigCycleCompletedCondition, ConditionResult, CuriosityCondition, EmotionChangeCondition, EvolutionAppliedCondition, MemoryRecallCondition, ProactiveCondition, RelationshipMilestoneCondition, TimeElapsedCondition, TimeOfDayCondition, UserReunionCondition, ) class TestTimeElapsedCondition: """空闲超时条件测试。""" async def test_not_triggered_when_no_history(self, db_session: AsyncSession) -> None: cond = TimeElapsedCondition(min_idle_minutes=360) result = await cond.check("char_nonexistent", db_session) assert not result.triggered async def test_triggered_when_idle_exceeds_threshold( self, db_session: AsyncSession, seed_old_message: None ) -> None: cond = TimeElapsedCondition(min_idle_minutes=1) result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "idle" async def test_not_triggered_when_recent_message( self, db_session: AsyncSession, seed_recent_message: None ) -> None: cond = TimeElapsedCondition(min_idle_minutes=360) now = datetime.now(UTC) result = await cond.check("char_test", db_session, now=now) assert not result.triggered class TestTimeOfDayCondition: async def test_morning_match(self) -> None: cond = TimeOfDayCondition(windows=["morning"]) now = datetime(2026, 6, 27, 7, 0, 0, tzinfo=UTC) result = await cond.check("char_test", None, now=now) assert result.triggered assert result.condition_name == "time_of_day" async def test_late_night_match(self) -> None: cond = TimeOfDayCondition(windows=["late_night"]) now = datetime(2026, 6, 27, 23, 0, 0, tzinfo=UTC) result = await cond.check("char_test", None, now=now) assert result.triggered assert result.context.get("time_of_day") == "late_night" async def test_no_match_outside_window(self) -> None: cond = TimeOfDayCondition(windows=["morning"]) now = datetime(2026, 6, 27, 14, 0, 0, tzinfo=UTC) result = await cond.check("char_test", None, now=now) assert not result.triggered class TestAnniversaryCondition: async def test_not_triggered_on_first_call_no_data(self, db_session: AsyncSession) -> None: cond = AnniversaryCondition() result = await cond.check("char_nonexistent", db_session) assert not result.triggered async def test_triggered_when_anniversary_exists( self, db_session: AsyncSession, seed_anniversary: None ) -> None: cond = AnniversaryCondition() now = datetime(2026, 6, 27, 12, 0, 0, tzinfo=UTC) result = await cond.check("char_test", db_session, now=now) assert result.triggered assert result.condition_name == "anniversary" assert "birthday" in result.context.get("anniversary_kind", "") async def test_not_duplicated_same_day( self, db_session: AsyncSession, seed_anniversary: None ) -> None: cond = AnniversaryCondition() now = datetime(2026, 6, 27, 12, 0, 0, tzinfo=UTC) result1 = await cond.check("char_test", db_session, now=now) assert result1.triggered result2 = await cond.check("char_test", db_session, now=now) assert not result2.triggered # 同一天不重复 class TestUserReunionCondition: async def test_not_triggered_when_no_event(self, db_session: AsyncSession) -> None: cond = UserReunionCondition() result = await cond.check("char_nonexistent", db_session) assert not result.triggered class TestEmotionChangeCondition: async def test_not_triggered_on_first_check( self, db_session: AsyncSession, seed_emotion_state: None ) -> None: cond = EmotionChangeCondition(min_delta=0.3) result = await cond.check("char_test", db_session) assert not result.triggered # 首次无基线 async def test_triggered_when_intensity_changes( self, db_session: AsyncSession, seed_emotion_state: None ) -> None: cond = EmotionChangeCondition(min_delta=0.3) await cond.check("char_test", db_session) # 首次建立基线 # 更新情绪 intensity 到 0.8(变化 0.6 > 0.3) from app.models.personality_emotion import CharacterEmotionState result = await db_session.execute( select(CharacterEmotionState).where(CharacterEmotionState.character_id == "char_test") ) state = result.scalar_one() state.intensity = 0.8 state.mood = "melancholy" await db_session.commit() result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "emotion" class TestCuriosityCondition: async def test_not_triggered_when_no_active_questions(self, db_session: AsyncSession) -> None: cond = CuriosityCondition() result = await cond.check("char_nonexistent", db_session) assert not result.triggered async def test_triggered_when_active_question_exists( self, db_session: AsyncSession, seed_curiosity: None ) -> None: cond = CuriosityCondition() result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "curiosity" assert "curiosity_question" in result.context assert result.context["curiosity_question"] == "你为什么总是沉默?" class TestEvolutionAppliedCondition: """演化应用条件测试。""" async def test_not_triggered_when_no_applied(self, db_session: AsyncSession) -> None: cond = EvolutionAppliedCondition() result = await cond.check("char_nonexistent", db_session) assert not result.triggered async def test_triggered_when_applied_exists( self, db_session: AsyncSession, seed_applied_evolution: None ) -> None: cond = EvolutionAppliedCondition() result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "evolution_applied" assert result.context.get("evolution_title") == "语气更柔和" assert result.context.get("evolution_level") == "L3" assert "evolution_proposal" in result.context async def test_not_duplicated( self, db_session: AsyncSession, seed_applied_evolution: None ) -> None: cond = EvolutionAppliedCondition() result1 = await cond.check("char_test", db_session) assert result1.triggered result2 = await cond.check("char_test", db_session) assert not result2.triggered # 同一条不重复触发 class TestBigCycleCompletedCondition: """大周天完成条件测试。""" async def test_not_triggered_when_no_completed(self, db_session: AsyncSession) -> None: cond = BigCycleCompletedCondition() result = await cond.check("char_nonexistent", db_session) assert not result.triggered async def test_triggered_when_completed_exists( self, db_session: AsyncSession, seed_completed_big_cycle: None ) -> None: cond = BigCycleCompletedCondition() result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "big_cycle_completed" assert "big_cycle_summary" in result.context assert "big_cycle_trigger" in result.context async def test_not_duplicated( self, db_session: AsyncSession, seed_completed_big_cycle: None ) -> None: cond = BigCycleCompletedCondition() result1 = await cond.check("char_test", db_session) assert result1.triggered result2 = await cond.check("char_test", db_session) assert not result2.triggered # 同一条不重复触发 class TestMemoryRecallCondition: """记忆唤醒条件测试。""" async def test_not_triggered_when_no_memory(self, db_session: AsyncSession) -> None: cond = MemoryRecallCondition() result = await cond.check("char_nonexistent", db_session) assert not result.triggered async def test_triggered_when_new_memory_exists( self, db_session: AsyncSession, seed_new_memory: None ) -> None: cond = MemoryRecallCondition() result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "memory_recall" assert result.context.get("memory_title") == "今天去了海边" assert "memory_content" in result.context assert "memory_type" in result.context async def test_not_duplicated(self, db_session: AsyncSession, seed_new_memory: None) -> None: cond = MemoryRecallCondition() result1 = await cond.check("char_test", db_session) assert result1.triggered result2 = await cond.check("char_test", db_session) assert not result2.triggered # 同一条不重复触发 class TestRelationshipMilestoneCondition: """关系里程碑条件测试。""" async def test_not_triggered_when_no_messages(self, db_session: AsyncSession) -> None: cond = RelationshipMilestoneCondition(message_milestones=[5], day_milestones=[30]) result = await cond.check("char_nonexistent", db_session) assert not result.triggered async def test_triggered_by_message_count( self, db_session: AsyncSession, seed_many_messages: None ) -> None: cond = RelationshipMilestoneCondition(message_milestones=[5], day_milestones=[9999]) result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "relationship_milestone" assert result.context.get("milestone_type") == "messages" assert result.context.get("milestone_value") == 5 assert result.context.get("milestone_threshold") == 5 async def test_triggered_by_days( self, db_session: AsyncSession, seed_old_relationship: None ) -> None: cond = RelationshipMilestoneCondition(message_milestones=[9999], day_milestones=[30]) result = await cond.check("char_test", db_session) assert result.triggered assert result.condition_name == "relationship_milestone" assert result.context.get("milestone_type") == "days" assert result.context.get("milestone_value") >= 30 assert result.context.get("milestone_threshold") == 30 async def test_not_duplicated(self, db_session: AsyncSession, seed_many_messages: None) -> None: cond = RelationshipMilestoneCondition(message_milestones=[5], day_milestones=[9999]) result1 = await cond.check("char_test", db_session) assert result1.triggered result2 = await cond.check("char_test", db_session) assert not result2.triggered # 同一里程碑不重复触发 async def test_prefers_messages_over_days( self, db_session: AsyncSession, seed_many_messages: None, seed_old_relationship: None ) -> None: """同时满足消息和天数里程碑时,优先触发消息里程碑。""" cond = RelationshipMilestoneCondition(message_milestones=[5], day_milestones=[30]) result = await cond.check("char_test", db_session) assert result.triggered assert result.context.get("milestone_type") == "messages" # 消息优先 @pytest.mark.asyncio async def test_proactive_condition_protocol() -> None: """验证 ProactiveCondition 可作为 Protocol 使用。""" class DummyCondition(ProactiveCondition): async def check( self, _character_id: str, _session: AsyncSession | None = None, *, _now: datetime | None = None, ) -> ConditionResult: return ConditionResult(triggered=True, trigger_reason="测试", condition_name="dummy") cond: ProactiveCondition = DummyCondition() result = await cond.check("char_test") assert result.triggered