fix: add cognitive state as observable signal for Matrix avatar (#358)
Co-authored-by: Kimi Agent <kimi@timmy.local> Co-committed-by: Kimi Agent <kimi@timmy.local>
This commit was merged in pull request #358.
This commit is contained in:
198
tests/timmy/test_cognitive_state.py
Normal file
198
tests/timmy/test_cognitive_state.py
Normal file
@@ -0,0 +1,198 @@
|
||||
"""Tests for cognitive state tracking in src/timmy/cognitive_state.py."""
|
||||
|
||||
from timmy.cognitive_state import (
|
||||
ENGAGEMENT_LEVELS,
|
||||
MOOD_VALUES,
|
||||
CognitiveState,
|
||||
CognitiveTracker,
|
||||
_extract_commitments,
|
||||
_extract_topic,
|
||||
_infer_engagement,
|
||||
_infer_mood,
|
||||
)
|
||||
|
||||
|
||||
class TestCognitiveState:
|
||||
"""Test the CognitiveState dataclass."""
|
||||
|
||||
def test_defaults(self):
|
||||
state = CognitiveState()
|
||||
assert state.focus_topic is None
|
||||
assert state.engagement == "idle"
|
||||
assert state.mood == "settled"
|
||||
assert state.conversation_depth == 0
|
||||
assert state.last_initiative is None
|
||||
assert state.active_commitments == []
|
||||
|
||||
def test_to_dict_excludes_private_fields(self):
|
||||
state = CognitiveState(focus_topic="testing")
|
||||
d = state.to_dict()
|
||||
assert "focus_topic" in d
|
||||
assert "_confidence_sum" not in d
|
||||
assert "_confidence_count" not in d
|
||||
|
||||
def test_to_state_lines_format(self):
|
||||
state = CognitiveState(
|
||||
focus_topic="loop architecture",
|
||||
engagement="deep",
|
||||
mood="curious",
|
||||
conversation_depth=42,
|
||||
last_initiative="proposed refactor",
|
||||
active_commitments=["draft ticket", "review PR"],
|
||||
)
|
||||
lines = state.to_state_lines()
|
||||
text = "\n".join(lines)
|
||||
assert "FOCUS_TOPIC: loop architecture" in text
|
||||
assert "ENGAGEMENT: deep" in text
|
||||
assert "MOOD: curious" in text
|
||||
assert "CONVERSATION_DEPTH: 42" in text
|
||||
assert "LAST_INITIATIVE: proposed refactor" in text
|
||||
assert "ACTIVE_COMMITMENTS: draft ticket; review PR" in text
|
||||
|
||||
def test_to_state_lines_none_topic(self):
|
||||
state = CognitiveState()
|
||||
lines = state.to_state_lines()
|
||||
text = "\n".join(lines)
|
||||
assert "FOCUS_TOPIC: none" in text
|
||||
|
||||
def test_to_state_lines_no_commitments(self):
|
||||
state = CognitiveState()
|
||||
lines = state.to_state_lines()
|
||||
text = "\n".join(lines)
|
||||
assert "ACTIVE_COMMITMENTS" not in text
|
||||
|
||||
|
||||
class TestInferEngagement:
|
||||
"""Test engagement level inference."""
|
||||
|
||||
def test_deep_keywords(self):
|
||||
assert _infer_engagement("help me debug this", "looking at the stack trace") == "deep"
|
||||
|
||||
def test_architecture_is_deep(self):
|
||||
assert (
|
||||
_infer_engagement("explain the architecture", "the system has three layers") == "deep"
|
||||
)
|
||||
|
||||
def test_short_response_is_surface(self):
|
||||
assert _infer_engagement("hi", "hello there") == "surface"
|
||||
|
||||
def test_normal_conversation_is_surface(self):
|
||||
result = _infer_engagement("what time is it", "It is 3pm right now.")
|
||||
assert result == "surface"
|
||||
|
||||
|
||||
class TestInferMood:
|
||||
"""Test mood inference."""
|
||||
|
||||
def test_low_confidence_is_hesitant(self):
|
||||
assert _infer_mood("I'm not really sure about this", 0.3) == "hesitant"
|
||||
|
||||
def test_exclamation_with_positive_words_is_energized(self):
|
||||
assert _infer_mood("That's a great idea!", 0.8) == "energized"
|
||||
|
||||
def test_question_words_are_curious(self):
|
||||
assert _infer_mood("I wonder if that would work", 0.6) == "curious"
|
||||
|
||||
def test_neutral_is_settled(self):
|
||||
assert _infer_mood("The answer is 42.", 0.7) == "settled"
|
||||
|
||||
def test_valid_mood_values(self):
|
||||
for mood in MOOD_VALUES:
|
||||
assert isinstance(mood, str)
|
||||
|
||||
|
||||
class TestExtractTopic:
|
||||
"""Test topic extraction from messages."""
|
||||
|
||||
def test_simple_message(self):
|
||||
assert _extract_topic("Python decorators") == "Python decorators"
|
||||
|
||||
def test_strips_question_prefix(self):
|
||||
topic = _extract_topic("what is a monad")
|
||||
assert topic == "a monad"
|
||||
|
||||
def test_truncates_long_messages(self):
|
||||
long_msg = "a" * 100
|
||||
topic = _extract_topic(long_msg)
|
||||
assert len(topic) <= 60
|
||||
|
||||
def test_empty_returns_none(self):
|
||||
assert _extract_topic("") is None
|
||||
assert _extract_topic(" ") is None
|
||||
|
||||
|
||||
class TestExtractCommitments:
|
||||
"""Test commitment extraction from responses."""
|
||||
|
||||
def test_i_will_commitment(self):
|
||||
result = _extract_commitments("I will draft the skeleton ticket for you.")
|
||||
assert len(result) >= 1
|
||||
assert "I will draft the skeleton ticket for you" in result[0]
|
||||
|
||||
def test_let_me_commitment(self):
|
||||
result = _extract_commitments("Let me look into that for you.")
|
||||
assert len(result) >= 1
|
||||
|
||||
def test_no_commitments(self):
|
||||
result = _extract_commitments("The answer is 42.")
|
||||
assert result == []
|
||||
|
||||
def test_caps_at_three(self):
|
||||
text = "I will do A. I'll do B. Let me do C. I'm going to do D."
|
||||
result = _extract_commitments(text)
|
||||
assert len(result) <= 3
|
||||
|
||||
|
||||
class TestCognitiveTracker:
|
||||
"""Test the CognitiveTracker singleton behaviour."""
|
||||
|
||||
def test_update_increments_depth(self, tmp_path):
|
||||
tracker = CognitiveTracker(state_file=tmp_path / "state.txt")
|
||||
tracker.update("hello", "Hi there, how can I help?")
|
||||
assert tracker.get_state().conversation_depth == 1
|
||||
tracker.update("thanks", "You're welcome!")
|
||||
assert tracker.get_state().conversation_depth == 2
|
||||
|
||||
def test_update_sets_focus_topic(self, tmp_path):
|
||||
tracker = CognitiveTracker(state_file=tmp_path / "state.txt")
|
||||
tracker.update(
|
||||
"Python decorators", "Decorators are syntactic sugar for wrapping functions."
|
||||
)
|
||||
assert tracker.get_state().focus_topic == "Python decorators"
|
||||
|
||||
def test_update_persists_to_file(self, tmp_path):
|
||||
state_file = tmp_path / "state.txt"
|
||||
tracker = CognitiveTracker(state_file=state_file)
|
||||
tracker.update("debug the loop", "Let me investigate the issue.")
|
||||
assert state_file.exists()
|
||||
content = state_file.read_text()
|
||||
assert "ENGAGEMENT:" in content
|
||||
assert "MOOD:" in content
|
||||
|
||||
def test_reset_clears_state(self, tmp_path):
|
||||
tracker = CognitiveTracker(state_file=tmp_path / "state.txt")
|
||||
tracker.update("hello", "world")
|
||||
tracker.reset()
|
||||
state = tracker.get_state()
|
||||
assert state.conversation_depth == 0
|
||||
assert state.focus_topic is None
|
||||
|
||||
def test_to_json(self, tmp_path):
|
||||
import json
|
||||
|
||||
tracker = CognitiveTracker(state_file=tmp_path / "state.txt")
|
||||
tracker.update("test", "response")
|
||||
data = json.loads(tracker.to_json())
|
||||
assert "focus_topic" in data
|
||||
assert "engagement" in data
|
||||
assert "mood" in data
|
||||
|
||||
def test_engagement_values_are_valid(self):
|
||||
for level in ENGAGEMENT_LEVELS:
|
||||
assert isinstance(level, str)
|
||||
|
||||
def test_creates_parent_directory(self, tmp_path):
|
||||
state_file = tmp_path / "nested" / "dir" / "state.txt"
|
||||
tracker = CognitiveTracker(state_file=state_file)
|
||||
tracker.update("test", "response")
|
||||
assert state_file.exists()
|
||||
Reference in New Issue
Block a user