File size: 4,364 Bytes
7fedbc5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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