work in progress.
This commit is contained in:
@@ -11,10 +11,17 @@ from loyal_companion.models import (
|
||||
Conversation,
|
||||
Message,
|
||||
User,
|
||||
UserAttachmentProfile,
|
||||
UserFact,
|
||||
UserRelationship,
|
||||
)
|
||||
from loyal_companion.services.ai_service import AIService
|
||||
from loyal_companion.services.attachment_service import (
|
||||
AttachmentContext,
|
||||
AttachmentService,
|
||||
AttachmentState,
|
||||
AttachmentStyle,
|
||||
)
|
||||
from loyal_companion.services.fact_extraction_service import FactExtractionService
|
||||
from loyal_companion.services.mood_service import MoodLabel, MoodService, MoodState
|
||||
from loyal_companion.services.opinion_service import OpinionService, extract_topics_from_message
|
||||
@@ -618,3 +625,362 @@ class TestAIService:
|
||||
service._provider = MagicMock()
|
||||
|
||||
assert service.model == "gpt-4o-mini"
|
||||
|
||||
|
||||
class TestAttachmentService:
|
||||
"""Tests for AttachmentService."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_or_create_profile_new(self, db_session, sample_user):
|
||||
"""Test creating a new attachment profile."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
|
||||
assert profile.id is not None
|
||||
assert profile.user_id == sample_user.id
|
||||
assert profile.primary_style == "unknown"
|
||||
assert profile.current_state == "regulated"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_or_create_profile_existing(self, db_session, sample_user):
|
||||
"""Test getting an existing attachment profile."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
# Create first
|
||||
profile1 = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
await db_session.commit()
|
||||
|
||||
# Get again
|
||||
profile2 = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
|
||||
assert profile1.id == profile2.id
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_message_no_indicators(self, db_session, sample_user):
|
||||
"""Test analyzing a message with no attachment indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content="Hello, how are you today?",
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
assert context.current_state == AttachmentState.REGULATED
|
||||
assert len(context.recent_indicators) == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_message_anxious_indicators(self, db_session, sample_user):
|
||||
"""Test analyzing a message with anxious attachment indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content="Are you still there? Do you still like me? Did I do something wrong?",
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
assert context.current_state == AttachmentState.ACTIVATED
|
||||
assert len(context.recent_indicators) > 0
|
||||
|
||||
# Check profile was updated
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
assert profile.anxious_indicators > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_message_avoidant_indicators(self, db_session, sample_user):
|
||||
"""Test analyzing a message with avoidant attachment indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content="It's fine, whatever. I don't need anyone. I'm better alone.",
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
assert context.current_state == AttachmentState.ACTIVATED
|
||||
assert len(context.recent_indicators) > 0
|
||||
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
assert profile.avoidant_indicators > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_message_disorganized_indicators(self, db_session, sample_user):
|
||||
"""Test analyzing a message with disorganized attachment indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content="I don't know what I want. I'm so confused and torn.",
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
# Should detect disorganized patterns
|
||||
assert len(context.recent_indicators) > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_message_mixed_state(self, db_session, sample_user):
|
||||
"""Test that mixed indicators result in mixed state."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
# Message with both anxious and avoidant indicators
|
||||
context = await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content="Are you still there? Actually, it's fine, I don't care anyway.",
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
assert context.current_state == AttachmentState.MIXED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_analyze_message_secure_indicators(self, db_session, sample_user):
|
||||
"""Test analyzing a message with secure attachment indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content="I'm feeling sad today and I need to talk about it. Thank you for listening.",
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
assert profile.secure_indicators > 0
|
||||
|
||||
def test_find_indicators_anxious(self, db_session):
|
||||
"""Test finding anxious indicators in text."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
matches = service._find_indicators(
|
||||
"do you still like me?",
|
||||
service.ANXIOUS_INDICATORS,
|
||||
)
|
||||
|
||||
assert len(matches) > 0
|
||||
|
||||
def test_find_indicators_none(self, db_session):
|
||||
"""Test finding no indicators in neutral text."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
matches = service._find_indicators(
|
||||
"the weather is nice today",
|
||||
service.ANXIOUS_INDICATORS,
|
||||
)
|
||||
|
||||
assert len(matches) == 0
|
||||
|
||||
def test_determine_state_regulated(self, db_session):
|
||||
"""Test state determination with no indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
state, intensity = service._determine_state([], [], [])
|
||||
|
||||
assert state == AttachmentState.REGULATED
|
||||
assert intensity == 0.0
|
||||
|
||||
def test_determine_state_activated(self, db_session):
|
||||
"""Test state determination with single style indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
state, intensity = service._determine_state(["pattern1", "pattern2"], [], [])
|
||||
|
||||
assert state == AttachmentState.ACTIVATED
|
||||
assert intensity > 0
|
||||
|
||||
def test_determine_state_mixed(self, db_session):
|
||||
"""Test state determination with mixed indicators."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
state, intensity = service._determine_state(["anxious1"], ["avoidant1"], [])
|
||||
|
||||
assert state == AttachmentState.MIXED
|
||||
|
||||
def test_get_attachment_prompt_modifier_regulated(self, db_session):
|
||||
"""Test prompt modifier for regulated state."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = AttachmentContext(
|
||||
primary_style=AttachmentStyle.UNKNOWN,
|
||||
style_confidence=0.0,
|
||||
current_state=AttachmentState.REGULATED,
|
||||
state_intensity=0.0,
|
||||
recent_indicators=[],
|
||||
effective_responses=[],
|
||||
)
|
||||
|
||||
modifier = service.get_attachment_prompt_modifier(context, "friend")
|
||||
|
||||
assert modifier == ""
|
||||
|
||||
def test_get_attachment_prompt_modifier_anxious_activated(self, db_session):
|
||||
"""Test prompt modifier for anxious activated state."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = AttachmentContext(
|
||||
primary_style=AttachmentStyle.ANXIOUS,
|
||||
style_confidence=0.7,
|
||||
current_state=AttachmentState.ACTIVATED,
|
||||
state_intensity=0.6,
|
||||
recent_indicators=["pattern1"],
|
||||
effective_responses=[],
|
||||
)
|
||||
|
||||
modifier = service.get_attachment_prompt_modifier(context, "friend")
|
||||
|
||||
assert "reassurance" in modifier.lower()
|
||||
assert "present" in modifier.lower()
|
||||
|
||||
def test_get_attachment_prompt_modifier_avoidant_activated(self, db_session):
|
||||
"""Test prompt modifier for avoidant activated state."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = AttachmentContext(
|
||||
primary_style=AttachmentStyle.AVOIDANT,
|
||||
style_confidence=0.7,
|
||||
current_state=AttachmentState.ACTIVATED,
|
||||
state_intensity=0.6,
|
||||
recent_indicators=["pattern1"],
|
||||
effective_responses=[],
|
||||
)
|
||||
|
||||
modifier = service.get_attachment_prompt_modifier(context, "friend")
|
||||
|
||||
assert "space" in modifier.lower()
|
||||
assert "push" in modifier.lower()
|
||||
|
||||
def test_get_attachment_prompt_modifier_disorganized_activated(self, db_session):
|
||||
"""Test prompt modifier for disorganized activated state."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = AttachmentContext(
|
||||
primary_style=AttachmentStyle.DISORGANIZED,
|
||||
style_confidence=0.7,
|
||||
current_state=AttachmentState.ACTIVATED,
|
||||
state_intensity=0.6,
|
||||
recent_indicators=["pattern1"],
|
||||
effective_responses=[],
|
||||
)
|
||||
|
||||
modifier = service.get_attachment_prompt_modifier(context, "friend")
|
||||
|
||||
assert "steady" in modifier.lower()
|
||||
assert "predictable" in modifier.lower()
|
||||
|
||||
def test_get_attachment_prompt_modifier_close_friend_reflection(self, db_session):
|
||||
"""Test prompt modifier includes reflection at close friend level."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = AttachmentContext(
|
||||
primary_style=AttachmentStyle.ANXIOUS,
|
||||
style_confidence=0.7,
|
||||
current_state=AttachmentState.ACTIVATED,
|
||||
state_intensity=0.6,
|
||||
recent_indicators=["pattern1"],
|
||||
effective_responses=[],
|
||||
)
|
||||
|
||||
modifier = service.get_attachment_prompt_modifier(context, "close_friend")
|
||||
|
||||
assert "pattern" in modifier.lower()
|
||||
|
||||
def test_get_attachment_prompt_modifier_with_effective_responses(self, db_session):
|
||||
"""Test prompt modifier includes effective responses."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = AttachmentContext(
|
||||
primary_style=AttachmentStyle.ANXIOUS,
|
||||
style_confidence=0.7,
|
||||
current_state=AttachmentState.ACTIVATED,
|
||||
state_intensity=0.6,
|
||||
recent_indicators=["pattern1"],
|
||||
effective_responses=["reassurance", "validation"],
|
||||
)
|
||||
|
||||
modifier = service.get_attachment_prompt_modifier(context, "friend")
|
||||
|
||||
assert "helped" in modifier.lower()
|
||||
assert "reassurance" in modifier.lower()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_response_effectiveness_helpful(self, db_session, sample_user):
|
||||
"""Test recording a helpful response."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
await service.record_response_effectiveness(
|
||||
user=sample_user,
|
||||
guild_id=111222333,
|
||||
response_style="reassurance",
|
||||
was_helpful=True,
|
||||
)
|
||||
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
assert "reassurance" in (profile.effective_responses or [])
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_record_response_effectiveness_unhelpful(self, db_session, sample_user):
|
||||
"""Test recording an unhelpful response."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
await service.record_response_effectiveness(
|
||||
user=sample_user,
|
||||
guild_id=111222333,
|
||||
response_style="advice",
|
||||
was_helpful=False,
|
||||
)
|
||||
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
assert "advice" in (profile.ineffective_responses or [])
|
||||
|
||||
def test_default_context(self, db_session):
|
||||
"""Test default context when tracking is disabled."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
context = service._default_context()
|
||||
|
||||
assert context.primary_style == AttachmentStyle.UNKNOWN
|
||||
assert context.current_state == AttachmentState.REGULATED
|
||||
assert context.style_confidence == 0.0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_primary_style_determination(self, db_session, sample_user):
|
||||
"""Test that primary style is determined after enough samples."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
# Send multiple messages with anxious indicators
|
||||
anxious_messages = [
|
||||
"Are you still there?",
|
||||
"Do you still like me?",
|
||||
"Did I do something wrong?",
|
||||
"Please don't leave me",
|
||||
"Are you mad at me?",
|
||||
"I'm scared you'll abandon me",
|
||||
]
|
||||
|
||||
for msg in anxious_messages:
|
||||
await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content=msg,
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
|
||||
# After enough samples, primary style should be determined
|
||||
assert profile.anxious_indicators >= 5
|
||||
assert profile.style_confidence > 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_activation_tracking(self, db_session, sample_user):
|
||||
"""Test that activations are tracked."""
|
||||
service = AttachmentService(db_session)
|
||||
|
||||
await service.analyze_message(
|
||||
user=sample_user,
|
||||
message_content="Are you still there? Do you still like me?",
|
||||
guild_id=111222333,
|
||||
)
|
||||
|
||||
profile = await service.get_or_create_profile(sample_user, guild_id=111222333)
|
||||
|
||||
assert profile.activation_count >= 1
|
||||
assert profile.last_activation_at is not None
|
||||
|
||||
Reference in New Issue
Block a user