mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-20 21:18:13 +02:00
163 lines
5.8 KiB
Python
163 lines
5.8 KiB
Python
"""The podcast lifecycle: the guarantees the rest of the system relies on.
|
|
|
|
These tests drive the aggregate through :class:`PodcastService`'s public
|
|
methods and observe the resulting status and stored brief/transcript — the
|
|
domain's contract. They say nothing about how the service stores or flushes,
|
|
so they survive any refactor that preserves the lifecycle.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.podcasts.persistence import PodcastStatus
|
|
from app.podcasts.service import (
|
|
InvalidTransition,
|
|
PodcastService,
|
|
PreconditionFailed,
|
|
SpecConflict,
|
|
read_spec,
|
|
read_transcript,
|
|
)
|
|
|
|
pytestmark = pytest.mark.unit
|
|
|
|
|
|
async def test_a_podcast_progresses_from_creation_to_ready(
|
|
fake_session, make_spec, make_transcript
|
|
):
|
|
"""The full happy path: create → brief → draft → review → render → ready."""
|
|
service = PodcastService(fake_session)
|
|
|
|
podcast = await service.create(title="Episode 1", search_space_id=7)
|
|
assert podcast.status == PodcastStatus.PENDING
|
|
|
|
spec = make_spec()
|
|
await service.attach_brief(podcast, spec)
|
|
assert podcast.status == PodcastStatus.AWAITING_BRIEF
|
|
assert read_spec(podcast) == spec
|
|
|
|
await service.begin_drafting(podcast)
|
|
assert podcast.status == PodcastStatus.DRAFTING
|
|
|
|
transcript = make_transcript()
|
|
await service.attach_transcript(podcast, transcript)
|
|
assert podcast.status == PodcastStatus.AWAITING_REVIEW
|
|
assert read_transcript(podcast) == transcript
|
|
|
|
await service.approve(podcast)
|
|
assert podcast.status == PodcastStatus.RENDERING
|
|
|
|
await service.attach_audio(
|
|
podcast, storage_backend="local", storage_key="k", duration_seconds=42
|
|
)
|
|
assert podcast.status == PodcastStatus.READY
|
|
assert podcast.duration_seconds == 42
|
|
|
|
|
|
async def test_drafting_requires_an_approved_brief(fake_session):
|
|
"""A brief must exist before drafting can begin."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="No brief", search_space_id=1)
|
|
|
|
with pytest.raises(PreconditionFailed):
|
|
await service.begin_drafting(podcast)
|
|
|
|
|
|
async def test_rendering_requires_a_transcript(fake_session, make_spec):
|
|
"""Approval to render is refused when no transcript has been drafted."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="No transcript", search_space_id=1)
|
|
await service.attach_brief(podcast, make_spec())
|
|
await service.begin_drafting(podcast)
|
|
|
|
with pytest.raises(PreconditionFailed):
|
|
await service.approve(podcast)
|
|
|
|
|
|
async def test_regenerate_returns_a_reviewed_transcript_to_drafting(
|
|
fake_session, make_spec, make_transcript
|
|
):
|
|
"""At the go/no-go gate, rejecting sends the podcast back to drafting."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="Redo", search_space_id=1)
|
|
await service.attach_brief(podcast, make_spec())
|
|
await service.begin_drafting(podcast)
|
|
await service.attach_transcript(podcast, make_transcript())
|
|
|
|
await service.regenerate(podcast)
|
|
|
|
assert podcast.status == PodcastStatus.DRAFTING
|
|
|
|
|
|
async def test_brief_can_be_edited_at_the_gate_and_bumps_its_version(
|
|
fake_session, make_spec
|
|
):
|
|
"""Editing the brief while awaiting review records it and advances version."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="Editable", search_space_id=1)
|
|
await service.attach_brief(podcast, make_spec(language="en"))
|
|
starting_version = podcast.spec_version
|
|
|
|
await service.update_spec(podcast, make_spec(language="fr"), starting_version)
|
|
|
|
assert read_spec(podcast).language == "fr"
|
|
assert podcast.spec_version == starting_version + 1
|
|
|
|
|
|
async def test_editing_a_brief_with_a_stale_version_conflicts(
|
|
fake_session, make_spec
|
|
):
|
|
"""A concurrent edit racing on a stale version is rejected, not silently lost."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="Raced", search_space_id=1)
|
|
await service.attach_brief(podcast, make_spec())
|
|
current = podcast.spec_version
|
|
|
|
with pytest.raises(SpecConflict):
|
|
await service.update_spec(podcast, make_spec(language="es"), current - 1)
|
|
|
|
|
|
async def test_brief_cannot_be_edited_after_the_gate_closes(
|
|
fake_session, make_spec
|
|
):
|
|
"""Once drafting starts, the brief is settled and edits are refused."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="Locked", search_space_id=1)
|
|
await service.attach_brief(podcast, make_spec())
|
|
await service.begin_drafting(podcast)
|
|
|
|
with pytest.raises(InvalidTransition):
|
|
await service.update_spec(podcast, make_spec(language="es"), podcast.spec_version)
|
|
|
|
|
|
async def test_a_podcast_can_be_cancelled_while_in_flight(fake_session, make_spec):
|
|
"""Cancellation is available from a non-terminal state."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="Abort", search_space_id=1)
|
|
await service.attach_brief(podcast, make_spec())
|
|
|
|
await service.cancel(podcast)
|
|
|
|
assert podcast.status == PodcastStatus.CANCELLED
|
|
|
|
|
|
async def test_failure_records_a_reason(fake_session):
|
|
"""Failing a podcast captures a human-readable reason."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="Boom", search_space_id=1)
|
|
|
|
await service.fail(podcast, "tts provider unavailable")
|
|
|
|
assert podcast.status == PodcastStatus.FAILED
|
|
assert podcast.error == "tts provider unavailable"
|
|
|
|
|
|
async def test_terminal_podcasts_reject_further_transitions(fake_session):
|
|
"""A finished podcast cannot be cancelled or otherwise moved."""
|
|
service = PodcastService(fake_session)
|
|
podcast = await service.create(title="Done", search_space_id=1)
|
|
await service.cancel(podcast)
|
|
|
|
with pytest.raises(InvalidTransition):
|
|
await service.fail(podcast, "too late")
|