from __future__ import annotations from sqlalchemy import select from app.characters.memory_generator import GeneratedMemory, build_prompt, generate_and_store from app.db import SessionLocal from app.models.character import Character, CharacterState from app.models.memory import Memory, MemoryScope, MemoryType class FakeLLMClient: """Duck-typed stand-in for `LLMClient` used in tests.""" def __init__(self, memories: list[GeneratedMemory] | Exception): self._result = memories def generate_structured(self, **kwargs): if isinstance(self._result, Exception): raise self._result return self._result def _make_character(session) -> str: char = Character( name="Test Character", short_description="A test", backstory="Born in a test. Lived in a test. Died in a test.", aggression=6, risk_tolerance=6, patience=5, trash_talk=4, target_elo=1600, adaptive=False, opening_preferences=["Ruy Lopez", "Sicilian Najdorf"], voice_descriptor="test voice", quirks="never blinks", state=CharacterState.GENERATING_MEMORIES, ) session.add(char) session.commit() return char.id def _fake_memories(n: int = 30) -> list[GeneratedMemory]: out: list[GeneratedMemory] = [] types = list(MemoryType) for i in range(n): out.append( GeneratedMemory( scope=MemoryScope.CHARACTER_LORE, type=types[i % len(types)], emotional_valence=(i % 5 - 2) / 2.0, # spread -1..1 triggers=[f"trigger_{i}", f"alt_{i}"], narrative_text=f"This is the {i}-th fake memory. It has enough length to pass validation.", relevance_tags=[f"tag_{i % 3}"], ) ) return out def test_build_prompt_contains_character_details(): char = Character( name="Vera", short_description="quiet", backstory="a long backstory here", aggression=9, risk_tolerance=2, patience=3, trash_talk=1, target_elo=2000, adaptive=True, opening_preferences=["Ruy Lopez"], voice_descriptor="calm", quirks="hums", ) prompt = build_prompt(char, target=40, minimum=30, maximum=50) assert "Vera" in prompt assert "a long backstory here" in prompt assert "Ruy Lopez" in prompt assert "2000" in prompt assert "adapts" in prompt.lower() # adaptive line def test_generate_and_store_persists_and_marks_ready(): with SessionLocal() as s: character_id = _make_character(s) fake = FakeLLMClient(_fake_memories(30)) count = generate_and_store(character_id, client=fake) assert count == 30 with SessionLocal() as s: char = s.get(Character, character_id) assert char is not None assert char.state == CharacterState.READY assert char.memory_generation_error is None memories = list(s.execute(select(Memory).where(Memory.character_id == character_id)).scalars()) assert len(memories) == 30 # Spread check — we should see multiple distinct types. assert len({m.type for m in memories}) >= 4 def test_generate_and_store_failure_marks_character_failed(): with SessionLocal() as s: character_id = _make_character(s) fake = FakeLLMClient(RuntimeError("fake Gemini outage")) import pytest with pytest.raises(RuntimeError): generate_and_store(character_id, client=fake) with SessionLocal() as s: char = s.get(Character, character_id) assert char is not None assert char.state == CharacterState.GENERATION_FAILED assert char.memory_generation_error is not None assert "fake Gemini outage" in char.memory_generation_error def test_generate_and_store_rejects_too_few(): """If the LLM returns a suspiciously tiny batch, we fail the character.""" with SessionLocal() as s: character_id = _make_character(s) fake = FakeLLMClient(_fake_memories(3)) # below min//2 import pytest with pytest.raises(Exception): generate_and_store(character_id, client=fake) with SessionLocal() as s: char = s.get(Character, character_id) assert char.state == CharacterState.GENERATION_FAILED