This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/timmy/test_cognitive_state.py
Kimi Agent 332fa373b8 fix: wire cognitive state to sensory bus (presence loop) (#414)
## Summary
- CognitiveTracker.update() now emits `cognitive_state_changed` events to the SensoryBus
- WorkshopHeartbeat (and other subscribers) react immediately to mood/engagement changes
- Closes the sense → memory → react loop described in the Workshop architecture
- Fire-and-forget emission — never blocks the chat response path
- Gracefully skips when no event loop is running (sync contexts/tests)

## Test plan
- [x] 3 new tests: event emission, mood change tracking, graceful skip without loop
- [x] All 1935 unit tests pass
- [x] Lint + format clean

Fixes #222

Co-authored-by: kimi <kimi@localhost>
Reviewed-on: http://localhost:3000/rockachopa/Timmy-time-dashboard/pulls/414
Co-authored-by: Kimi Agent <kimi@timmy.local>
Co-committed-by: Kimi Agent <kimi@timmy.local>
2026-03-19 03:23:03 -04:00

220 lines
7.4 KiB
Python

"""Tests for cognitive state tracking in src/timmy/cognitive_state.py."""
import asyncio
from unittest.mock import patch
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_dict_includes_public_fields(self):
state = CognitiveState(
focus_topic="loop architecture",
engagement="deep",
mood="curious",
conversation_depth=42,
last_initiative="proposed refactor",
active_commitments=["draft ticket", "review PR"],
)
d = state.to_dict()
assert d["focus_topic"] == "loop architecture"
assert d["engagement"] == "deep"
assert d["mood"] == "curious"
assert d["conversation_depth"] == 42
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 behaviour."""
def test_update_increments_depth(self):
tracker = CognitiveTracker()
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):
tracker = CognitiveTracker()
tracker.update(
"Python decorators", "Decorators are syntactic sugar for wrapping functions."
)
assert tracker.get_state().focus_topic == "Python decorators"
def test_reset_clears_state(self):
tracker = CognitiveTracker()
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):
import json
tracker = CognitiveTracker()
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)
async def test_update_emits_cognitive_state_changed(self):
"""CognitiveTracker.update() emits a sensory event."""
from timmy.event_bus import SensoryBus
mock_bus = SensoryBus()
received = []
mock_bus.subscribe("cognitive_state_changed", lambda e: received.append(e))
with patch("timmy.event_bus.get_sensory_bus", return_value=mock_bus):
tracker = CognitiveTracker()
tracker.update("debug the memory leak", "Looking at the stack trace now.")
# Give the fire-and-forget task a chance to run
await asyncio.sleep(0.05)
assert len(received) == 1
event = received[0]
assert event.source == "cognitive"
assert event.event_type == "cognitive_state_changed"
assert "mood" in event.data
assert "engagement" in event.data
assert "depth" in event.data
assert event.data["depth"] == 1
async def test_update_tracks_mood_change(self):
"""Event data includes whether mood/engagement changed."""
from timmy.event_bus import SensoryBus
mock_bus = SensoryBus()
received = []
mock_bus.subscribe("cognitive_state_changed", lambda e: received.append(e))
with patch("timmy.event_bus.get_sensory_bus", return_value=mock_bus):
tracker = CognitiveTracker()
# First message — "!" + "great" with high confidence → "energized"
tracker.update("wow", "That's a great discovery!")
await asyncio.sleep(0.05)
assert len(received) == 1
# Default mood is "settled", energized response → mood changes
assert received[0].data["mood"] == "energized"
assert received[0].data["mood_changed"] is True
def test_emit_skipped_without_event_loop(self):
"""Event emission gracefully skips when no async loop is running."""
tracker = CognitiveTracker()
# Should not raise — just silently skips
tracker.update("hello", "Hi there!")