annator-command-center / tests /coverage_expansion /test_episode_services_coverage.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 7)
cc036ff verified
Raw
History Blame Contribute Delete
19.3 kB
"""
Coverage expansion tests for episode services.
Tests cover critical code paths in:
- episode_segmentation_service.py: Episode segmentation logic
- episode_lifecycle_service.py: Episode creation, completion, archival
- agent_graduation_service.py: Graduation criteria evaluation
Target: Cover critical paths (happy path + error paths) to increase coverage.
"""
import pytest
from unittest.mock import Mock, patch, MagicMock
from datetime import datetime, timedelta
from sqlalchemy.orm import Session
from core.episode_segmentation_service import (
EpisodeSegmentationService,
EpisodeBoundaryDetector,
TIME_GAP_THRESHOLD_MINUTES,
SEMANTIC_SIMILARITY_THRESHOLD
)
from core.models import AgentRegistry, AgentStatus, ChatMessage, AgentExecution
class TestEpisodeBoundaryDetectorCoverage:
"""Coverage expansion for EpisodeBoundaryDetector."""
@pytest.fixture
def detector(self):
"""Get boundary detector instance."""
lancedb_mock = Mock()
lancedb_mock.embed_text = Mock(return_value=None)
return EpisodeBoundaryDetector(lancedb_mock)
# Test: time gap detection
def test_detect_time_gap_exceeds_threshold(self, detector):
"""Detect time gaps > threshold between messages."""
now = datetime.now()
messages = [
ChatMessage(id="1", content="First", created_at=now - timedelta(minutes=60)),
ChatMessage(id="2", content="Second", created_at=now - timedelta(minutes=30)),
ChatMessage(id="3", content="Third", created_at=now),
]
gaps = detector.detect_time_gap(messages)
# Should detect gap between message 1 and 2 (60 min > 30 min threshold)
assert len(gaps) >= 1
assert 1 in gaps # Gap after first message
def test_detect_time_gap_below_threshold(self, detector):
"""Don't detect gaps below threshold."""
now = datetime.now()
messages = [
ChatMessage(id="1", content="First", created_at=now - timedelta(minutes=5)),
ChatMessage(id="2", content="Second", created_at=now - timedelta(minutes=2)),
ChatMessage(id="3", content="Third", created_at=now),
]
gaps = detector.detect_time_gap(messages)
# No gaps > 30 minutes
assert len(gaps) == 0
def test_detect_time_gap_exactly_threshold(self, detector):
"""Gap of exactly threshold minutes does NOT trigger new segment."""
now = datetime.now()
messages = [
ChatMessage(id="1", content="First", created_at=now - timedelta(minutes=30)),
ChatMessage(id="2", content="Second", created_at=now),
]
gaps = detector.detect_time_gap(messages)
# 30 minutes is NOT > 30, so no gap
assert len(gaps) == 0
def test_detect_time_gap_empty_messages(self, detector):
"""Handle empty message list."""
gaps = detector.detect_time_gap([])
assert gaps == []
def test_detect_time_gap_single_message(self, detector):
"""Handle single message (no gaps possible)."""
messages = [
ChatMessage(id="1", content="First", created_at=datetime.now())
]
gaps = detector.detect_time_gap(messages)
assert gaps == []
# Test: topic change detection
def test_detect_topic_changes_with_embeddings(self, detector):
"""Detect topic changes using semantic similarity."""
# Mock embeddings with low similarity
detector.db.embed_text = Mock(side_effect=[
[0.1, 0.2, 0.3], # First message
[0.9, 0.8, 0.7], # Second message (different topic)
])
now = datetime.now()
messages = [
ChatMessage(id="1", content="Discussion about weather", created_at=now),
ChatMessage(id="2", content="Analysis of financial reports", created_at=now),
]
changes = detector.detect_topic_changes(messages)
# Low similarity should trigger topic change
assert len(changes) >= 1
def test_detect_topic_changes_fallback_to_keywords(self, detector):
"""Fallback to keyword-based similarity when embeddings fail."""
# Mock embeddings to return None (failure)
detector.db.embed_text = Mock(return_value=None)
now = datetime.now()
messages = [
ChatMessage(id="1", content="Discussion about weather", created_at=now),
ChatMessage(id="2", content="Analysis of financial reports", created_at=now),
]
changes = detector.detect_topic_changes(messages)
# Should use keyword similarity and detect change
assert isinstance(changes, list)
def test_detect_topic_changes_empty_messages(self, detector):
"""Handle empty message list for topic changes."""
changes = detector.detect_topic_changes([])
assert changes == []
def test_detect_topic_changes_single_message(self, detector):
"""Handle single message (no changes possible)."""
messages = [
ChatMessage(id="1", content="First", created_at=datetime.now())
]
changes = detector.detect_topic_changes(messages)
assert changes == []
# Test: cosine similarity calculation
def test_cosine_similarity_identical_vectors(self, detector):
"""Calculate cosine similarity for identical vectors."""
vec1 = [1.0, 2.0, 3.0]
vec2 = [1.0, 2.0, 3.0]
similarity = detector._cosine_similarity(vec1, vec2)
assert similarity == pytest.approx(1.0, rel=1e-5)
def test_cosine_similarity_orthogonal_vectors(self, detector):
"""Calculate cosine similarity for orthogonal vectors."""
vec1 = [1.0, 0.0, 0.0]
vec2 = [0.0, 1.0, 0.0]
similarity = detector._cosine_similarity(vec1, vec2)
assert similarity == pytest.approx(0.0, abs=1e-5)
def test_cosine_similarity_zero_vectors(self, detector):
"""Handle zero-magnitude vectors."""
vec1 = [0.0, 0.0, 0.0]
vec2 = [1.0, 2.0, 3.0]
similarity = detector._cosine_similarity(vec1, vec2)
assert similarity == 0.0
# Test: keyword similarity fallback
def test_keyword_similarity_identical_text(self, detector):
"""Calculate keyword similarity for identical text."""
similarity = detector._keyword_similarity("hello world", "hello world")
assert similarity == 1.0
def test_keyword_similarity_no_overlap(self, detector):
"""Calculate keyword similarity with no overlap."""
similarity = detector._keyword_similarity("hello world", "foo bar")
assert similarity == 0.0
def test_keyword_similarity_partial_overlap(self, detector):
"""Calculate keyword similarity with partial overlap."""
similarity = detector._keyword_similarity("hello world test", "hello foo bar")
# 1 word in common ("hello") out of 5 total
assert 0.0 < similarity < 1.0
def test_keyword_similarity_empty_text(self, detector):
"""Handle empty text for keyword similarity."""
similarity = detector._keyword_similarity("", "hello world")
assert similarity == 0.0
def test_keyword_similarity_case_insensitive(self, detector):
"""Keyword similarity is case-insensitive."""
sim1 = detector._keyword_similarity("Hello World", "hello world")
sim2 = detector._keyword_similarity("Hello World", "HELLO WORLD")
assert sim1 == sim2 == 1.0
# Test: task completion detection
def test_detect_task_completion(self, detector):
"""Detect task completion markers."""
executions = [
AgentExecution(id="1", status="completed", result_summary="Task done"),
AgentExecution(id="2", status="running", result_summary=None),
AgentExecution(id="3", status="completed", result_summary="Another done"),
]
completions = detector.detect_task_completion(executions)
assert len(completions) == 2
assert 0 in completions
assert 2 in completions
def test_detect_task_completion_empty(self, detector):
"""Handle empty execution list."""
completions = detector.detect_task_completion([])
assert completions == []
def test_detect_task_completion_no_completions(self, detector):
"""Handle executions with no completed tasks."""
executions = [
AgentExecution(id="1", status="running", result_summary=None),
AgentExecution(id="2", status="failed", result_summary=None),
]
completions = detector.detect_task_completion(executions)
assert completions == []
class TestEpisodeSegmentationServiceCoverage:
"""Coverage expansion for EpisodeSegmentationService."""
@pytest.fixture
def db_session(self):
"""Get test database session."""
from core.database import SessionLocal
session = SessionLocal()
yield session
session.rollback()
session.close()
@pytest.fixture
def segmentation_service(self, db_session):
"""Get segmentation service instance."""
return EpisodeSegmentationService(db_session)
# Test: time-based segmentation
def test_should_segment_by_time_gap(self, segmentation_service):
"""Segment episodes when time gap exceeds threshold."""
last_timestamp = datetime.now() - timedelta(hours=2)
current_timestamp = datetime.now()
should_segment = segmentation_service.should_segment_by_time(
last_timestamp=last_timestamp,
current_timestamp=current_timestamp,
threshold_minutes=30
)
assert should_segment == True
def test_should_not_segment_small_time_gap(self, segmentation_service):
"""Don't segment episodes for small time gaps."""
last_timestamp = datetime.now() - timedelta(minutes=5)
current_timestamp = datetime.now()
should_segment = segmentation_service.should_segment_by_time(
last_timestamp=last_timestamp,
current_timestamp=current_timestamp,
threshold_minutes=30
)
assert should_segment == False
# Test: create segment
def test_create_episode_segment(self, segmentation_service, db_session):
"""Create a new episode segment."""
from core.models import AgentEpisode
episode = AgentEpisode(
id="test-episode",
agent_id="test-agent",
workspace_id="default",
status="active"
)
db_session.add(episode)
db_session.commit()
segment = segmentation_service.create_segment(
episode_id="test-episode",
content="Test segment content",
sequence_order=0
)
assert segment.id is not None
assert segment.content == "Test segment content"
assert segment.sequence_order == 0
# Test: boundary detection integration
def test_detect_boundaries_integration(self, segmentation_service):
"""Test boundary detection with service integration."""
now = datetime.now()
messages = [
ChatMessage(id="1", content="First", created_at=now - timedelta(minutes=60)),
ChatMessage(id="2", content="Second", created_at=now),
]
boundaries = segmentation_service.detect_boundaries(messages)
assert isinstance(boundaries, list)
class TestEpisodeLifecycleServiceCoverage:
"""Coverage expansion for EpisodeLifecycleService."""
@pytest.fixture
def db_session(self):
"""Get test database session."""
from core.database import SessionLocal
session = SessionLocal()
yield session
session.rollback()
session.close()
@pytest.fixture
def lifecycle_service(self, db_session):
"""Get lifecycle service instance."""
from core.episode_lifecycle_service import EpisodeLifecycleService
return EpisodeLifecycleService(db_session)
# Test: create episode
def test_create_episode(self, lifecycle_service, db_session):
"""Create a new episode."""
episode = lifecycle_service.create_episode(
agent_id="test-agent",
workspace_id="default_shared"
)
assert episode.id is not None
assert episode.status == "active"
assert episode.agent_id == "test-agent"
assert episode.workspace_id == "default_shared"
# Test: complete episode
def test_complete_episode(self, lifecycle_service, db_session):
"""Complete an active episode."""
# Create episode
episode = lifecycle_service.create_episode(
agent_id="test-agent",
workspace_id="default_shared"
)
completed = lifecycle_service.complete_episode(
episode_id=episode.id,
outcome="success"
)
assert completed.status == "completed"
assert completed.outcome == "success"
assert completed.completed_at is not None
# Test: archive episode
def test_archive_old_episodes(self, lifecycle_service, db_session):
"""Archive episodes older than threshold."""
# Create old episode
episode = lifecycle_service.create_episode(
agent_id="test-agent",
workspace_id="default_shared"
)
episode.created_at = datetime.now() - timedelta(days=100)
db_session.commit()
archived_count = lifecycle_service.archive_episodes_older_than(days=90)
assert archived_count >= 0
# Test: get episode
def test_get_episode(self, lifecycle_service, db_session):
"""Retrieve episode by ID."""
episode = lifecycle_service.create_episode(
agent_id="test-agent",
workspace_id="default_shared"
)
retrieved = lifecycle_service.get_episode(episode.id)
assert retrieved is not None
assert retrieved.id == episode.id
def test_get_episode_not_found(self, lifecycle_service):
"""Return None for non-existent episode."""
retrieved = lifecycle_service.get_episode("nonexistent-episode")
assert retrieved is None
class TestAgentGraduationServiceCoverage:
"""Coverage expansion for AgentGraduationService."""
@pytest.fixture
def db_session(self):
"""Get test database session."""
from core.database import SessionLocal
session = SessionLocal()
yield session
session.rollback()
session.close()
@pytest.fixture
def graduation_service(self, db_session):
"""Get graduation service instance."""
from core.agent_graduation_service import AgentGraduationService
return AgentGraduationService(db_session)
# Test: STUDENT -> INTERN graduation criteria
def test_check_graduation_student_to_intern_met(self, graduation_service, db_session):
"""Check STUDENT -> INTERN graduation when criteria met."""
agent = AgentRegistry(
id="test-student",
name="Test Student",
category="test",
module_path="test",
class_name="TestStudent",
maturity=AgentStatus.STUDENT.value,
workspace_id="default"
)
db_session.add(agent)
db_session.commit()
# Mock episode count and intervention rate
with patch.object(graduation_service, '_get_episode_count', return_value=10):
with patch.object(graduation_service, '_get_intervention_rate', return_value=0.4):
result = graduation_service.check_graduation_criteria(agent_id="test-student")
assert result.can_graduate == True
def test_check_graduation_student_to_intern_not_met(self, graduation_service, db_session):
"""Check STUDENT -> INTERN graduation when criteria not met."""
agent = AgentRegistry(
id="test-student",
name="Test Student",
category="test",
module_path="test",
class_name="TestStudent",
maturity=AgentStatus.STUDENT.value,
workspace_id="default"
)
db_session.add(agent)
db_session.commit()
# Mock insufficient episodes
with patch.object(graduation_service, '_get_episode_count', return_value=5):
with patch.object(graduation_service, '_get_intervention_rate', return_value=0.4):
result = graduation_service.check_graduation_criteria(agent_id="test-student")
assert result.can_graduate == False
# Test: INTERN -> SUPERVISED graduation criteria
def test_check_graduation_intern_to_supervised_met(self, graduation_service, db_session):
"""Check INTERN -> SUPERVISED graduation when criteria met."""
agent = AgentRegistry(
id="test-intern",
name="Test Intern",
category="test",
module_path="test",
class_name="TestIntern",
maturity=AgentStatus.INTERN.value,
workspace_id="default"
)
db_session.add(agent)
db_session.commit()
with patch.object(graduation_service, '_get_episode_count', return_value=25):
with patch.object(graduation_service, '_get_intervention_rate', return_value=0.15):
result = graduation_service.check_graduation_criteria(agent_id="test-intern")
assert result.can_graduate == True
# Test: execute graduation
def test_execute_graduation(self, graduation_service, db_session):
"""Execute agent graduation."""
agent = AgentRegistry(
id="test-promote",
name="Test Promote",
category="test",
module_path="test",
class_name="TestPromote",
maturity=AgentStatus.STUDENT.value,
workspace_id="default"
)
db_session.add(agent)
db_session.commit()
with patch.object(graduation_service, 'check_graduation_criteria') as mock_check:
mock_check.return_value = Mock(
can_graduate=True,
target_maturity=AgentStatus.INTERN
)
graduated = graduation_service.execute_graduation(agent_id="test-promote")
assert graduated.maturity == AgentStatus.INTERN.value
def test_execute_graduation_not_eligible(self, graduation_service, db_session):
"""Execute graduation fails when not eligible."""
agent = AgentRegistry(
id="test-no-promote",
name="Test No Promote",
category="test",
module_path="test",
class_name="TestNoPromote",
maturity=AgentStatus.STUDENT.value,
workspace_id="default"
)
db_session.add(agent)
db_session.commit()
with patch.object(graduation_service, 'check_graduation_criteria') as mock_check:
mock_check.return_value = Mock(
can_graduate=False,
target_maturity=None
)
# Should not graduate
result = graduation_service.execute_graduation(agent_id="test-no-promote")
assert result.maturity == AgentStatus.STUDENT.value