200 lines
7.3 KiB
Python
200 lines
7.3 KiB
Python
|
|
"""Tests for the Nexus Introspection Engine."""
|
||
|
|
|
||
|
|
from unittest.mock import MagicMock, patch
|
||
|
|
|
||
|
|
from timmy.nexus.introspection import (
|
||
|
|
CognitiveSummary,
|
||
|
|
IntrospectionSnapshot,
|
||
|
|
NexusIntrospector,
|
||
|
|
SessionAnalytics,
|
||
|
|
ThoughtSummary,
|
||
|
|
)
|
||
|
|
|
||
|
|
# ── Data model tests ─────────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestCognitiveSummary:
|
||
|
|
def test_defaults(self):
|
||
|
|
s = CognitiveSummary()
|
||
|
|
assert s.mood == "settled"
|
||
|
|
assert s.engagement == "idle"
|
||
|
|
assert s.focus_topic is None
|
||
|
|
|
||
|
|
def test_to_dict(self):
|
||
|
|
s = CognitiveSummary(mood="curious", engagement="deep", focus_topic="architecture")
|
||
|
|
d = s.to_dict()
|
||
|
|
assert d["mood"] == "curious"
|
||
|
|
assert d["engagement"] == "deep"
|
||
|
|
assert d["focus_topic"] == "architecture"
|
||
|
|
|
||
|
|
|
||
|
|
class TestThoughtSummary:
|
||
|
|
def test_to_dict(self):
|
||
|
|
t = ThoughtSummary(
|
||
|
|
id="t1", content="Hello world", seed_type="freeform", created_at="2026-01-01"
|
||
|
|
)
|
||
|
|
d = t.to_dict()
|
||
|
|
assert d["id"] == "t1"
|
||
|
|
assert d["seed_type"] == "freeform"
|
||
|
|
assert d["parent_id"] is None
|
||
|
|
|
||
|
|
|
||
|
|
class TestSessionAnalytics:
|
||
|
|
def test_defaults(self):
|
||
|
|
a = SessionAnalytics()
|
||
|
|
assert a.total_messages == 0
|
||
|
|
assert a.avg_response_length == 0.0
|
||
|
|
assert a.topics_discussed == []
|
||
|
|
|
||
|
|
|
||
|
|
class TestIntrospectionSnapshot:
|
||
|
|
def test_to_dict_structure(self):
|
||
|
|
snap = IntrospectionSnapshot()
|
||
|
|
d = snap.to_dict()
|
||
|
|
assert "cognitive" in d
|
||
|
|
assert "recent_thoughts" in d
|
||
|
|
assert "analytics" in d
|
||
|
|
assert "timestamp" in d
|
||
|
|
|
||
|
|
def test_to_dict_with_data(self):
|
||
|
|
snap = IntrospectionSnapshot(
|
||
|
|
cognitive=CognitiveSummary(mood="energized"),
|
||
|
|
recent_thoughts=[
|
||
|
|
ThoughtSummary(id="x", content="test", seed_type="s", created_at="now"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
d = snap.to_dict()
|
||
|
|
assert d["cognitive"]["mood"] == "energized"
|
||
|
|
assert len(d["recent_thoughts"]) == 1
|
||
|
|
|
||
|
|
|
||
|
|
# ── Introspector tests ───────────────────────────────────────────────────────
|
||
|
|
|
||
|
|
|
||
|
|
class TestNexusIntrospector:
|
||
|
|
def test_snapshot_empty_log(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
snap = intro.snapshot(conversation_log=[])
|
||
|
|
assert isinstance(snap, IntrospectionSnapshot)
|
||
|
|
assert snap.analytics.total_messages == 0
|
||
|
|
|
||
|
|
def test_snapshot_with_messages(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
log = [
|
||
|
|
{"role": "user", "content": "hello", "timestamp": "10:00:00"},
|
||
|
|
{"role": "assistant", "content": "Hi there!", "timestamp": "10:00:01"},
|
||
|
|
{"role": "user", "content": "architecture question", "timestamp": "10:00:02"},
|
||
|
|
]
|
||
|
|
snap = intro.snapshot(conversation_log=log)
|
||
|
|
assert snap.analytics.total_messages == 3
|
||
|
|
assert snap.analytics.user_messages == 2
|
||
|
|
assert snap.analytics.assistant_messages == 1
|
||
|
|
assert snap.analytics.avg_response_length > 0
|
||
|
|
|
||
|
|
def test_record_memory_hits(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
intro.record_memory_hits(3)
|
||
|
|
intro.record_memory_hits(2)
|
||
|
|
snap = intro.snapshot(
|
||
|
|
conversation_log=[{"role": "user", "content": "x", "timestamp": "t"}]
|
||
|
|
)
|
||
|
|
assert snap.analytics.memory_hits_total == 5
|
||
|
|
|
||
|
|
def test_reset_clears_state(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
intro.record_memory_hits(10)
|
||
|
|
intro.reset()
|
||
|
|
snap = intro.snapshot(
|
||
|
|
conversation_log=[{"role": "user", "content": "x", "timestamp": "t"}]
|
||
|
|
)
|
||
|
|
assert snap.analytics.memory_hits_total == 0
|
||
|
|
|
||
|
|
def test_topics_deduplication(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
log = [
|
||
|
|
{"role": "user", "content": "hello", "timestamp": "t"},
|
||
|
|
{"role": "user", "content": "hello", "timestamp": "t"},
|
||
|
|
{"role": "user", "content": "different topic", "timestamp": "t"},
|
||
|
|
]
|
||
|
|
snap = intro.snapshot(conversation_log=log)
|
||
|
|
assert len(snap.analytics.topics_discussed) == 2
|
||
|
|
|
||
|
|
def test_topics_capped_at_8(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
log = [{"role": "user", "content": f"topic {i}", "timestamp": "t"} for i in range(15)]
|
||
|
|
snap = intro.snapshot(conversation_log=log)
|
||
|
|
assert len(snap.analytics.topics_discussed) <= 8
|
||
|
|
|
||
|
|
def test_cognitive_read_fallback(self):
|
||
|
|
"""If cognitive read fails, snapshot still works with defaults."""
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
# Patch the module-level import inside _read_cognitive
|
||
|
|
with patch.dict("sys.modules", {"timmy.cognitive_state": None}):
|
||
|
|
snap = intro.snapshot(conversation_log=[])
|
||
|
|
# Should not raise — fallback to default
|
||
|
|
assert snap.cognitive.mood == "settled"
|
||
|
|
|
||
|
|
def test_thoughts_read_fallback(self):
|
||
|
|
"""If thought read fails, snapshot still works with empty list."""
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
with patch.dict("sys.modules", {"timmy.thinking": None}):
|
||
|
|
snap = intro.snapshot(conversation_log=[])
|
||
|
|
assert snap.recent_thoughts == []
|
||
|
|
|
||
|
|
def test_read_cognitive_from_tracker(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
mock_state = MagicMock()
|
||
|
|
mock_state.mood = "curious"
|
||
|
|
mock_state.engagement = "deep"
|
||
|
|
mock_state.focus_topic = "sovereignty"
|
||
|
|
mock_state.conversation_depth = 5
|
||
|
|
mock_state.active_commitments = ["build something"]
|
||
|
|
mock_state.last_initiative = "build something"
|
||
|
|
|
||
|
|
mock_tracker = MagicMock()
|
||
|
|
mock_tracker.get_state.return_value = mock_state
|
||
|
|
|
||
|
|
with patch("timmy.cognitive_state.cognitive_tracker", mock_tracker):
|
||
|
|
summary = intro._read_cognitive()
|
||
|
|
|
||
|
|
assert summary.mood == "curious"
|
||
|
|
assert summary.engagement == "deep"
|
||
|
|
assert summary.focus_topic == "sovereignty"
|
||
|
|
assert summary.conversation_depth == 5
|
||
|
|
|
||
|
|
def test_read_thoughts_from_engine(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
mock_thought = MagicMock()
|
||
|
|
mock_thought.id = "t1"
|
||
|
|
mock_thought.content = "Deep thought about sovereignty"
|
||
|
|
mock_thought.seed_type = "existential"
|
||
|
|
mock_thought.created_at = "2026-03-23T10:00:00"
|
||
|
|
mock_thought.parent_id = None
|
||
|
|
|
||
|
|
mock_engine = MagicMock()
|
||
|
|
mock_engine.get_recent_thoughts.return_value = [mock_thought]
|
||
|
|
|
||
|
|
with patch("timmy.thinking.thinking_engine", mock_engine):
|
||
|
|
thoughts = intro._read_thoughts(limit=5)
|
||
|
|
|
||
|
|
assert len(thoughts) == 1
|
||
|
|
assert thoughts[0].id == "t1"
|
||
|
|
assert thoughts[0].seed_type == "existential"
|
||
|
|
|
||
|
|
def test_read_thoughts_truncates_long_content(self):
|
||
|
|
intro = NexusIntrospector()
|
||
|
|
mock_thought = MagicMock()
|
||
|
|
mock_thought.id = "t2"
|
||
|
|
mock_thought.content = "x" * 300
|
||
|
|
mock_thought.seed_type = "freeform"
|
||
|
|
mock_thought.created_at = "2026-03-23"
|
||
|
|
mock_thought.parent_id = None
|
||
|
|
|
||
|
|
mock_engine = MagicMock()
|
||
|
|
mock_engine.get_recent_thoughts.return_value = [mock_thought]
|
||
|
|
|
||
|
|
with patch("timmy.thinking.thinking_engine", mock_engine):
|
||
|
|
thoughts = intro._read_thoughts(limit=5)
|
||
|
|
|
||
|
|
assert len(thoughts[0].content) <= 201 # 200 + "…"
|