"""灵核情绪状态机纯计算测试(modules/personality/emotion.py)。""" from __future__ import annotations from app.modules.personality.emotion import ( MOODS, EmotionState, apply_signal, decay, map_signal_to_mood, parse_baseline, signal_intensity_delta, to_prompt_text, ) def _baseline(mood: str = "neutral", intensity: float = 0.2) -> EmotionState: return EmotionState(mood=mood, intensity=intensity) # ---- map_signal_to_mood ---- def test_map_reunion_to_touched() -> None: assert map_signal_to_mood("time.reunion", {"reunion_intensity": 0.8}) == "touched" def test_map_user_care_default_to_concerned() -> None: assert map_signal_to_mood("time.user_care", {"care_kind": "late_night"}) == "concerned" def test_map_user_care_festival_lonely_to_melancholy() -> None: assert map_signal_to_mood("time.user_care", {"care_kind": "festival_lonely"}) == "melancholy" def test_map_sentiment_default_to_wistful() -> None: assert map_signal_to_mood("time.sentiment", {"trigger_type": "reunion_memory"}) == "wistful" def test_map_sentiment_late_night_to_melancholy() -> None: assert map_signal_to_mood("time.sentiment", {"trigger_type": "late_night"}) == "melancholy" def test_map_unknown_event_returns_none() -> None: assert map_signal_to_mood("tool.started", {}) is None # ---- signal_intensity_delta ---- def test_delta_reunion_uses_reunion_intensity() -> None: assert signal_intensity_delta("time.reunion", {"reunion_intensity": 0.7}) == 0.7 def test_delta_user_care_uses_care_suggest() -> None: assert signal_intensity_delta("time.user_care", {"care_suggest": 0.6}) == 0.6 def test_delta_sentiment_default_05() -> None: assert signal_intensity_delta("time.sentiment", {}) == 0.5 def test_delta_unknown_event_zero() -> None: assert signal_intensity_delta("tool.started", {}) == 0.0 # ---- apply_signal ---- def test_apply_signal_changes_mood_and_sets_intensity_from_baseline() -> None: state = _baseline() # neutral 0.2 new = apply_signal(state, "time.reunion", {"reunion_intensity": 0.6}, _baseline()) assert new.mood == "touched" assert new.intensity == 0.8 # 0.2 baseline + 0.6 delta def test_apply_signal_same_mood_accumulates() -> None: state = EmotionState(mood="touched", intensity=0.5) new = apply_signal(state, "time.reunion", {"reunion_intensity": 0.3}, _baseline()) assert new.mood == "touched" assert new.intensity == 0.8 # 0.5 current + 0.3 delta def test_apply_signal_clamps_to_1() -> None: state = EmotionState(mood="touched", intensity=0.9) new = apply_signal(state, "time.reunion", {"reunion_intensity": 0.5}, _baseline()) assert new.mood == "touched" assert new.intensity == 1.0 def test_apply_signal_unknown_event_returns_unchanged() -> None: state = EmotionState(mood="pleased", intensity=0.5) new = apply_signal(state, "tool.started", {}, _baseline()) assert new == state # ---- decay ---- def test_decay_toward_baseline_half_life() -> None: state = EmotionState(mood="touched", intensity=0.8) baseline = _baseline() # neutral 0.2 # 半衰期 6 回合,elapsed 6 → intensity 向 baseline 走一半 decayed = decay(state, baseline, elapsed_turns=6.0) assert decayed.mood == "touched" assert abs(decayed.intensity - 0.5) < 0.01 # 0.2 + (0.8-0.2)*0.5 = 0.5 def test_decay_below_threshold_returns_baseline_mood() -> None: state = EmotionState(mood="touched", intensity=0.3) baseline = _baseline() # elapsed 大 → intensity 衰减到 < 0.15 → mood 回 baseline decayed = decay(state, baseline, elapsed_turns=30.0) assert decayed.mood == baseline.mood assert decayed.intensity == baseline.intensity def test_decay_zero_elapsed_returns_unchanged() -> None: state = EmotionState(mood="touched", intensity=0.8) baseline = _baseline() assert decay(state, baseline, elapsed_turns=0) == state # ---- parse_baseline ---- def test_parse_baseline_from_dict() -> None: assert parse_baseline({"mood": "neutral", "intensity": 0.3}) == EmotionState( mood="neutral", intensity=0.3 ) def test_parse_baseline_from_valid_json() -> None: assert parse_baseline('{"mood": "pleased", "intensity": 0.4}') == EmotionState( mood="pleased", intensity=0.4 ) def test_parse_baseline_invalid_json_returns_neutral_default() -> None: assert parse_baseline("not json") == EmotionState() def test_parse_baseline_none_returns_neutral_default() -> None: assert parse_baseline(None) == EmotionState() def test_parse_baseline_clamps_intensity() -> None: assert parse_baseline({"mood": "neutral", "intensity": 1.5}).intensity == 1.0 assert parse_baseline({"mood": "neutral", "intensity": -0.2}).intensity == 0.0 def test_parse_baseline_unknown_mood_falls_back_neutral() -> None: assert parse_baseline({"mood": "unknown_mood", "intensity": 0.5}).mood == "neutral" # ---- to_prompt_text ---- def test_to_prompt_text_includes_chinese_label_and_pct() -> None: text = to_prompt_text(EmotionState(mood="touched", intensity=0.7)) assert "感动" in text assert "70" in text def test_to_prompt_text_unknown_mood_uses_raw() -> None: text = to_prompt_text(EmotionState(mood="custom", intensity=0.5)) assert "custom" in text # ---- MOODS ---- def test_moods_contains_seven() -> None: assert set(MOODS) == { "neutral", "pleased", "touched", "wistful", "concerned", "festive", "melancholy", }