File size: 4,215 Bytes
5aaf5ba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import os
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
from pathlib import Path
from types import SimpleNamespace

import pytest
from fastapi.testclient import TestClient

os.environ["DATABASE_URL"] = "sqlite:///./data/test_gargi_ai.db"
os.environ["GEMINI_API_KEY"] = "test-key"

from gargi_ai.config import get_settings  # noqa: E402
from gargi_ai.database import Base, engine  # noqa: E402
from gargi_ai.main import app  # noqa: E402
from gargi_ai.providers import LiveProvider, LLMProvider, TTSProvider  # noqa: E402
from gargi_ai.schemas import QuizQuestion, TeachingPayload, WhiteboardBlock  # noqa: E402


class FakeLLM(LLMProvider):
    async def stream_explanation(self, prompt: str) -> AsyncIterator[str]:
        yield "Gravity "
        yield "pulls objects together."

    async def generate_artifact(self, prompt: str) -> TeachingPayload:
        return TeachingPayload(
            spoken_explanation="Gravity pulls objects together.",
            summary="Gravity is an attractive force.",
            whiteboard_blocks=[
                WhiteboardBlock(type="heading", content="Gravity"),
                WhiteboardBlock(type="equation", content="F = Gm1m2/r^2"),
            ],
            quiz=[
                QuizQuestion(
                    id="q1",
                    type="multiple_choice",
                    question="What does gravity do?",
                    options=["Attracts", "Repels"],
                    expected_answer="Attracts",
                    explanation="Masses attract.",
                ),
                QuizQuestion(
                    id="q2",
                    type="multiple_choice",
                    question="Which has gravity?",
                    options=["Mass", "Nothing"],
                    expected_answer="Mass",
                    explanation="All mass gravitates.",
                ),
                QuizQuestion(
                    id="q3",
                    type="short_answer",
                    question="Name the force.",
                    expected_answer="Gravity",
                    explanation="The force is gravity.",
                ),
            ],
            detected_misconceptions=[],
            learner_level_update="beginner",
            suggested_follow_up="Explore orbits.",
        )


class FakeTTS(TTSProvider):
    def __init__(self, audio_dir: Path):
        self.audio_dir = audio_dir

    async def synthesize(self, text: str, voice: str):
        audio_id = "11111111-1111-1111-1111-111111111111"
        output = self.audio_dir / f"{audio_id}.mp3"
        output.parent.mkdir(parents=True, exist_ok=True)
        output.write_bytes(b"fake-mp3")
        return audio_id, output


class FakeLiveSession:
    def __init__(self):
        self.received_audio = False
        self.delivered = False

    async def send_realtime_input(self, **kwargs):
        if kwargs.get("audio"):
            self.received_audio = True

    async def receive(self):
        while not self.received_audio or self.delivered:
            import asyncio

            await asyncio.sleep(0.01)
        self.delivered = True
        part = SimpleNamespace(
            inline_data=SimpleNamespace(data=b"fake-live-pcm")
        )
        content = SimpleNamespace(
            input_transcription=SimpleNamespace(text="Why does gravity work?"),
            output_transcription=SimpleNamespace(
                text="Gravity attracts objects with mass."
            ),
            model_turn=SimpleNamespace(parts=[part]),
            interrupted=False,
            turn_complete=True,
        )
        yield SimpleNamespace(server_content=content)


class FakeLive(LiveProvider):
    @asynccontextmanager
    async def connect(self, system_instruction: str):
        yield FakeLiveSession()


@pytest.fixture
def client():
    settings = get_settings()
    Base.metadata.drop_all(bind=engine)
    settings.audio_dir.mkdir(parents=True, exist_ok=True)
    with TestClient(app) as test_client:
        app.state.llm = FakeLLM()
        app.state.tts = FakeTTS(settings.audio_dir)
        app.state.live = FakeLive()
        yield test_client
    Base.metadata.drop_all(bind=engine)