2026-06-10 18:44:25 +02:00
|
|
|
"""Shared builders for podcast unit tests.
|
|
|
|
|
|
test(podcasts): relocate stateful tests to integration
Move the lifecycle service, Celery task bodies, and mark_failed coverage out of
DB-faking unit tests and into integration tests against a real Postgres, faking
only true externals (broker, object store, TTS, ffmpeg, billing, LLM). Add HTTP
slices for cancel, voices, scoping, and public-chat streaming. The unit tier is
now fake-free pure logic with no session doubles.
2026-06-11 06:27:00 +02:00
|
|
|
These tests exercise pure logic through public interfaces with no test doubles:
|
|
|
|
|
the brief and transcript factories build valid aggregates so each test states
|
|
|
|
|
only the fields it cares about. Stateful, persistence-backed paths (the lifecycle
|
|
|
|
|
service, the Celery task bodies) are covered by the integration suite against a
|
|
|
|
|
real database.
|
2026-06-10 18:44:25 +02:00
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from app.podcasts.schemas import (
|
|
|
|
|
DurationTarget,
|
|
|
|
|
PodcastSpec,
|
|
|
|
|
PodcastStyle,
|
|
|
|
|
SpeakerRole,
|
|
|
|
|
SpeakerSpec,
|
|
|
|
|
Transcript,
|
|
|
|
|
TranscriptTurn,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_spec():
|
|
|
|
|
"""Factory for a valid :class:`PodcastSpec`; override only what matters."""
|
|
|
|
|
|
|
|
|
|
def _make(
|
|
|
|
|
*,
|
|
|
|
|
language: str = "en",
|
|
|
|
|
style: PodcastStyle = PodcastStyle.CONVERSATIONAL,
|
|
|
|
|
speakers: list[SpeakerSpec] | None = None,
|
2026-06-16 23:38:28 +02:00
|
|
|
min_seconds: int = 600,
|
|
|
|
|
max_seconds: int = 1200,
|
2026-06-10 18:44:25 +02:00
|
|
|
focus: str | None = None,
|
|
|
|
|
) -> PodcastSpec:
|
|
|
|
|
if speakers is None:
|
|
|
|
|
speakers = [
|
|
|
|
|
SpeakerSpec(
|
2026-06-11 15:31:43 -07:00
|
|
|
slot=0,
|
|
|
|
|
name="Host",
|
|
|
|
|
role=SpeakerRole.HOST,
|
|
|
|
|
voice_id="kokoro:am_adam",
|
2026-06-10 18:44:25 +02:00
|
|
|
),
|
|
|
|
|
SpeakerSpec(
|
|
|
|
|
slot=1,
|
|
|
|
|
name="Guest",
|
|
|
|
|
role=SpeakerRole.GUEST,
|
|
|
|
|
voice_id="kokoro:af_bella",
|
|
|
|
|
),
|
|
|
|
|
]
|
|
|
|
|
return PodcastSpec(
|
|
|
|
|
language=language,
|
|
|
|
|
style=style,
|
|
|
|
|
speakers=speakers,
|
2026-06-16 23:38:28 +02:00
|
|
|
duration=DurationTarget(min_seconds=min_seconds, max_seconds=max_seconds),
|
2026-06-10 18:44:25 +02:00
|
|
|
focus=focus,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return _make
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def make_transcript():
|
|
|
|
|
"""Factory for a valid :class:`Transcript`."""
|
|
|
|
|
|
|
|
|
|
def _make(turns: list[tuple[int, str]] | None = None) -> Transcript:
|
|
|
|
|
if turns is None:
|
|
|
|
|
turns = [(0, "Welcome to the show."), (1, "Glad to be here.")]
|
|
|
|
|
return Transcript(
|
|
|
|
|
turns=[TranscriptTurn(speaker=slot, text=text) for slot, text in turns]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return _make
|