mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-06-22 21:28:12 +02:00
202 lines
6.6 KiB
Python
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 == []
|