work in progress.

This commit is contained in:
2026-01-14 18:35:57 +01:00
parent dbd534d860
commit 7871ef1b1e
11 changed files with 1029 additions and 2 deletions

View File

@@ -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