[claude] Add agent emotional state simulation (#1013) (#1144)
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled

Co-authored-by: Claude (Opus 4.6) <claude@hermes.local>
Co-committed-by: Claude (Opus 4.6) <claude@hermes.local>
This commit was merged in pull request #1144.
This commit is contained in:
2026-03-23 18:36:52 +00:00
committed by Google Gemini
parent 1c1bfb6407
commit cd1bc2bf6b
9 changed files with 546 additions and 9 deletions

View File

@@ -0,0 +1,196 @@
"""Tests for agent emotional state simulation (src/timmy/agents/emotional_state.py)."""
import time
from unittest.mock import patch
from timmy.agents.emotional_state import (
EMOTION_PROMPT_MODIFIERS,
EMOTIONAL_STATES,
EVENT_TRANSITIONS,
EmotionalState,
EmotionalStateTracker,
_intensity_label,
)
class TestEmotionalState:
"""Test the EmotionalState dataclass."""
def test_defaults(self):
state = EmotionalState()
assert state.current_emotion == "calm"
assert state.intensity == 0.5
assert state.previous_emotion == "calm"
assert state.trigger_event == ""
def test_to_dict_includes_label(self):
state = EmotionalState(current_emotion="analytical")
d = state.to_dict()
assert d["emotion_label"] == "Analytical"
assert d["current_emotion"] == "analytical"
def test_to_dict_all_fields(self):
state = EmotionalState(
current_emotion="frustrated",
intensity=0.8,
previous_emotion="calm",
trigger_event="task_failure",
)
d = state.to_dict()
assert d["current_emotion"] == "frustrated"
assert d["intensity"] == 0.8
assert d["previous_emotion"] == "calm"
assert d["trigger_event"] == "task_failure"
class TestEmotionalStates:
"""Validate the emotional states and transitions are well-defined."""
def test_all_states_are_strings(self):
for state in EMOTIONAL_STATES:
assert isinstance(state, str)
def test_all_states_have_prompt_modifiers(self):
for state in EMOTIONAL_STATES:
assert state in EMOTION_PROMPT_MODIFIERS
def test_all_transitions_target_valid_states(self):
for event_type, (emotion, intensity) in EVENT_TRANSITIONS.items():
assert emotion in EMOTIONAL_STATES, f"{event_type} targets unknown state: {emotion}"
assert 0.0 <= intensity <= 1.0, f"{event_type} has invalid intensity: {intensity}"
class TestEmotionalStateTracker:
"""Test the EmotionalStateTracker."""
def test_initial_emotion_default(self):
tracker = EmotionalStateTracker()
assert tracker.state.current_emotion == "calm"
def test_initial_emotion_custom(self):
tracker = EmotionalStateTracker(initial_emotion="analytical")
assert tracker.state.current_emotion == "analytical"
def test_initial_emotion_invalid_falls_back(self):
tracker = EmotionalStateTracker(initial_emotion="invalid_state")
assert tracker.state.current_emotion == "calm"
def test_process_known_event(self):
tracker = EmotionalStateTracker()
state = tracker.process_event("task_success")
assert state.current_emotion == "confident"
assert state.trigger_event == "task_success"
assert state.previous_emotion == "calm"
def test_process_unknown_event_ignored(self):
tracker = EmotionalStateTracker()
state = tracker.process_event("unknown_event_xyz")
assert state.current_emotion == "calm" # unchanged
def test_repeated_same_emotion_amplifies(self):
tracker = EmotionalStateTracker()
tracker.process_event("task_success")
initial_intensity = tracker.state.intensity
tracker.process_event("user_praise") # also targets confident
assert tracker.state.intensity >= initial_intensity
def test_different_emotion_replaces(self):
tracker = EmotionalStateTracker()
tracker.process_event("task_success")
assert tracker.state.current_emotion == "confident"
tracker.process_event("task_failure")
assert tracker.state.current_emotion == "frustrated"
assert tracker.state.previous_emotion == "confident"
def test_decay_no_effect_when_recent(self):
tracker = EmotionalStateTracker()
tracker.process_event("task_failure")
emotion_before = tracker.state.current_emotion
tracker.decay()
assert tracker.state.current_emotion == emotion_before
def test_decay_resets_to_calm_after_long_time(self):
tracker = EmotionalStateTracker()
tracker.process_event("task_failure")
assert tracker.state.current_emotion == "frustrated"
# Simulate passage of time (30+ minutes)
tracker.state.updated_at = time.time() - 2000
tracker.decay()
assert tracker.state.current_emotion == "calm"
def test_get_profile_returns_expected_keys(self):
tracker = EmotionalStateTracker()
profile = tracker.get_profile()
assert "current_emotion" in profile
assert "emotion_label" in profile
assert "intensity" in profile
assert "intensity_label" in profile
assert "previous_emotion" in profile
assert "trigger_event" in profile
assert "prompt_modifier" in profile
def test_get_prompt_modifier_returns_string(self):
tracker = EmotionalStateTracker(initial_emotion="cautious")
modifier = tracker.get_prompt_modifier()
assert isinstance(modifier, str)
assert "cautious" in modifier.lower()
def test_reset(self):
tracker = EmotionalStateTracker()
tracker.process_event("task_failure")
tracker.reset()
assert tracker.state.current_emotion == "calm"
assert tracker.state.intensity == 0.5
def test_process_event_with_context(self):
"""Context dict is accepted without error."""
tracker = EmotionalStateTracker()
state = tracker.process_event("error", {"details": "connection timeout"})
assert state.current_emotion == "cautious"
def test_event_chain_scenario(self):
"""Simulate: task assigned → success → new discovery → idle."""
tracker = EmotionalStateTracker()
tracker.process_event("task_assigned")
assert tracker.state.current_emotion == "analytical"
tracker.process_event("task_success")
assert tracker.state.current_emotion == "confident"
tracker.process_event("new_discovery")
assert tracker.state.current_emotion == "curious"
tracker.process_event("idle")
assert tracker.state.current_emotion == "calm"
def test_health_events(self):
tracker = EmotionalStateTracker()
tracker.process_event("health_low")
assert tracker.state.current_emotion == "cautious"
tracker.process_event("health_recovered")
assert tracker.state.current_emotion == "calm"
def test_quest_completed_triggers_adventurous(self):
tracker = EmotionalStateTracker()
tracker.process_event("quest_completed")
assert tracker.state.current_emotion == "adventurous"
class TestIntensityLabel:
def test_overwhelming(self):
assert _intensity_label(0.9) == "overwhelming"
def test_strong(self):
assert _intensity_label(0.7) == "strong"
def test_moderate(self):
assert _intensity_label(0.5) == "moderate"
def test_mild(self):
assert _intensity_label(0.3) == "mild"
def test_faint(self):
assert _intensity_label(0.1) == "faint"

View File

@@ -435,14 +435,14 @@ class TestStatusAndCapabilities:
tools=["calc"],
)
status = agent.get_status()
assert status == {
"agent_id": "bot-1",
"name": "TestBot",
"role": "assistant",
"model": "qwen3:30b",
"status": "ready",
"tools": ["calc"],
}
assert status["agent_id"] == "bot-1"
assert status["name"] == "TestBot"
assert status["role"] == "assistant"
assert status["model"] == "qwen3:30b"
assert status["status"] == "ready"
assert status["tools"] == ["calc"]
assert "emotional_profile" in status
assert status["emotional_profile"]["current_emotion"] == "calm"
# ── SubAgent.execute_task ────────────────────────────────────────────────────