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
|
|
|
"""The task failure safety net (``mark_failed``) against a real database.
|
|
|
|
|
|
|
|
|
|
When a task body raises, ``mark_failed`` records the reason on the row. Its
|
|
|
|
|
contract has two halves worth securing: a still-running podcast moves to FAILED
|
|
|
|
|
with the reason, while one that already reached a terminal state is left exactly
|
|
|
|
|
as it was rather than forced. A missing row is a no-op, never a crash.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
|
|
|
|
from app.podcasts.persistence import PodcastStatus
|
|
|
|
|
from app.podcasts.tasks import runtime
|
|
|
|
|
|
|
|
|
|
pytestmark = pytest.mark.integration
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_marking_failed_records_the_reason_on_a_running_podcast(
|
2026-06-26 18:36:46 +02:00
|
|
|
db_workspace, make_podcast, bind_task_session
|
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
|
|
|
):
|
|
|
|
|
podcast = await make_podcast(
|
2026-06-26 18:36:46 +02:00
|
|
|
workspace_id=db_workspace.id, status=PodcastStatus.DRAFTING
|
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
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await runtime.mark_failed(podcast.id, "tts provider unavailable")
|
|
|
|
|
|
|
|
|
|
assert podcast.status == PodcastStatus.FAILED
|
|
|
|
|
assert podcast.error == "tts provider unavailable"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_marking_failed_leaves_an_already_terminal_podcast_untouched(
|
2026-06-26 18:36:46 +02:00
|
|
|
db_workspace, make_podcast, bind_task_session
|
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
|
|
|
):
|
|
|
|
|
podcast = await make_podcast(
|
2026-06-26 18:36:46 +02:00
|
|
|
workspace_id=db_workspace.id, status=PodcastStatus.READY
|
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
|
|
|
)
|
|
|
|
|
|
|
|
|
|
await runtime.mark_failed(podcast.id, "too late")
|
|
|
|
|
|
|
|
|
|
assert podcast.status == PodcastStatus.READY
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def test_marking_a_missing_podcast_failed_is_a_no_op(bind_task_session):
|
|
|
|
|
await runtime.mark_failed(987654321, "gone") # must not raise
|