SurfSense/surfsense_backend/tests/integration/podcasts/test_regeneration.py

202 lines
6.6 KiB
Python

"""Regeneration: the listen-then-redo loop after the brief gate.
A user who dislikes the finished audio sends the episode back to the brief
gate: the saved brief reopens for tweaks (voices, length, focus) and drafting
only restarts on a fresh approval. The whole redo can also be reverted at any
point before the new render commits, falling back to the still-stored episode.
These pin the READY -> AWAITING_BRIEF -> DRAFTING round trip, the revert
fallback, and the 409s for acting from states that have nothing to redo or
revert.
"""
from __future__ import annotations
import pytest
from app.podcasts.persistence import Podcast, PodcastStatus
from app.podcasts.service import PodcastService
from .conftest import build_transcript
pytestmark = pytest.mark.integration
BASE = "/api/v1/podcasts"
async def test_regenerate_from_ready_reopens_the_brief_gate(
client, db_search_space, make_podcast, captured_tasks
):
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.READY
)
resp = await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "awaiting_brief"
# The prior brief is kept as the starting point for the new take.
assert body["spec"] is not None
# Nothing drafts until the user approves the reopened brief.
assert captured_tasks.draft == []
assert captured_tasks.render == []
async def test_approving_the_reopened_brief_starts_a_fresh_draft(
client, db_search_space, make_podcast, captured_tasks
):
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.READY
)
await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
resp = await client.post(f"{BASE}/{podcast.id}/brief/approve")
assert resp.status_code == 200
assert resp.json()["status"] == "drafting"
assert captured_tasks.draft == [((podcast.id, db_search_space.id), {})]
async def test_regenerate_from_brief_gate_is_rejected(
client, db_search_space, make_podcast, captured_tasks
):
# Nothing has been drafted yet, so there is nothing to regenerate.
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.AWAITING_BRIEF
)
resp = await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
assert resp.status_code == 409
assert captured_tasks.draft == []
async def test_regenerate_from_cancelled_is_rejected(
client, db_search_space, make_podcast, captured_tasks
):
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.AWAITING_BRIEF
)
await client.post(f"{BASE}/{podcast.id}/cancel")
resp = await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
assert resp.status_code == 409
assert captured_tasks.draft == []
async def test_reverting_a_regeneration_restores_the_ready_episode(
client, db_search_space, make_podcast, captured_tasks
):
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.READY
)
await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
resp = await client.post(f"{BASE}/{podcast.id}/regenerate/revert")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "ready"
# The episode the user could already play is untouched.
assert body["has_audio"] is True
assert captured_tasks.draft == []
assert captured_tasks.render == []
async def test_reverting_mid_draft_keeps_the_episode(
client, db_search_space, make_podcast
):
# Changing one's mind is allowed even after the reopened brief was
# approved: the episode survives until a new render replaces it.
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.READY
)
await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
await client.post(f"{BASE}/{podcast.id}/brief/approve")
resp = await client.post(f"{BASE}/{podcast.id}/regenerate/revert")
assert resp.status_code == 200
assert resp.json()["status"] == "ready"
async def test_reverting_mid_render_keeps_the_episode(
client, db_session, db_search_space, make_podcast
):
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.READY
)
service = PodcastService(db_session)
await service.regenerate(podcast)
await service.begin_drafting(podcast)
await service.attach_transcript(podcast, build_transcript())
resp = await client.post(f"{BASE}/{podcast.id}/regenerate/revert")
assert resp.status_code == 200
assert resp.json()["status"] == "ready"
async def test_reverted_episode_can_be_regenerated_again(
client, db_search_space, make_podcast
):
# Reverting must not strand the episode: the user can change their mind
# again immediately.
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.READY
)
await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
await client.post(f"{BASE}/{podcast.id}/regenerate/revert")
resp = await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
assert resp.status_code == 200
assert resp.json()["status"] == "awaiting_brief"
async def test_revert_on_a_fresh_brief_gate_is_rejected(
client, db_search_space, make_podcast
):
# A first-time brief has no regeneration to revert.
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.AWAITING_BRIEF
)
resp = await client.post(f"{BASE}/{podcast.id}/regenerate/revert")
assert resp.status_code == 409
assert resp.json()["detail"]
async def test_revert_when_nothing_was_regenerated_is_rejected(
client, db_search_space, make_podcast
):
podcast = await make_podcast(
search_space_id=db_search_space.id, status=PodcastStatus.READY
)
resp = await client.post(f"{BASE}/{podcast.id}/regenerate/revert")
assert resp.status_code == 409
async def test_regenerate_without_a_brief_is_rejected(
client, db_session, db_search_space, captured_tasks
):
# Legacy episodes finished before briefs existed; reopening a gate with
# nothing to review would strand them there.
podcast = Podcast(
title="Legacy Episode",
search_space_id=db_search_space.id,
status=PodcastStatus.READY,
spec_version=1,
file_location="/var/old/podcast.mp3",
)
db_session.add(podcast)
await db_session.flush()
resp = await client.post(f"{BASE}/{podcast.id}/transcript/regenerate")
assert resp.status_code == 422
assert captured_tasks.draft == []