183 lines
5.4 KiB
Python
183 lines
5.4 KiB
Python
"""灵核情绪状态机纯计算测试(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",
|
|
}
|