feat: queue voice session cover generation jobs

This commit is contained in:
2026-04-20 17:05:01 +08:00
parent 4d7072fb66
commit df4f5f4cdd
6 changed files with 254 additions and 21 deletions

View File

@@ -8,7 +8,7 @@ from httpx import ASGITransport, AsyncClient
from sqlalchemy import select
from app.db.database import get_db
from app.db.models import GenerationJob, GenerationJobEvent
from app.db.models import GenerationJob, GenerationJobEvent, Story
from app.main import app
from app.services.adapters import AdapterConfig
from app.services.adapters.storybook.primary import Storybook, StorybookPage
@@ -20,7 +20,7 @@ from app.services.generation_jobs import (
mark_stale_generation_jobs,
record_generation_event,
)
from app.services.story_service import run_generation_job_service
from app.services.story_service import queue_story_asset_generation, run_generation_job_service
pytestmark = pytest.mark.asyncio
@@ -225,6 +225,86 @@ async def test_asset_retry_records_job_events_and_updates_retryable_assets(
app.dependency_overrides.clear()
async def test_queue_story_asset_generation_dispatches_background_job(
db_session,
test_story,
):
task_delay_path = "app.tasks.generation_workflow.run_generation_workflow_task.delay"
with patch(task_delay_path) as mock_delay:
summary = await queue_story_asset_generation(
test_story.id,
test_story.user_id,
["image"],
db_session,
)
assert summary["output_mode"] == "asset_generation"
assert summary["input_type"] == "image"
assert summary["status"] == "running"
assert summary["current_step"] == "request_accepted"
mock_delay.assert_called_once_with(summary["id"])
job = (
await db_session.execute(
select(GenerationJob).where(GenerationJob.id == summary["id"])
)
).scalar_one()
assert job.story_id == test_story.id
assert job.output_mode == "asset_generation"
async def test_asset_generation_job_worker_completes_cover_image(
db_session,
test_story,
):
job = await create_generation_job(
db_session,
user_id=test_story.user_id,
output_mode="asset_generation",
input_type="image",
request_payload={"story_id": test_story.id, "assets": ["image"]},
story_id=test_story.id,
)
with patch(
"app.services.story_service.generate_image",
new_callable=AsyncMock,
) as mock_generate_image:
mock_generate_image.return_value = "https://example.com/async-cover.png"
await run_generation_job_service(job.id, db_session)
refreshed_job = (
await db_session.execute(select(GenerationJob).where(GenerationJob.id == job.id))
).scalar_one()
assert refreshed_job.status == "completed"
assert refreshed_job.current_step == "asset_generation_completed"
assert refreshed_job.result_snapshot["image_status"] == "ready"
story = (
await db_session.execute(
select(Story).where(Story.id == test_story.id)
)
).scalar_one()
assert story.image_url == "https://example.com/async-cover.png"
events = (
await db_session.execute(
select(GenerationJobEvent)
.where(GenerationJobEvent.job_id == job.id)
.order_by(GenerationJobEvent.id)
)
).scalars().all()
assert [event.event_type for event in events] == [
"request_accepted",
"worker_started",
"cover_image_started",
"cover_image_succeeded",
"asset_generation_completed",
]
async def test_storybook_generation_is_queued_then_worker_records_page_image_events(
db_session,
auth_token,

View File

@@ -99,9 +99,9 @@ async def test_voice_session_correct_turn_and_finalize_to_story(
new_callable=AsyncMock,
) as mock_tts,
patch(
"app.services.voice_session_service.generate_story_cover",
"app.services.voice_session_service.queue_story_asset_generation",
new_callable=AsyncMock,
) as mock_generate_cover,
) as mock_queue_asset_generation,
):
mock_generate.side_effect = [
StoryOutput(
@@ -118,7 +118,23 @@ async def test_voice_session_correct_turn_and_finalize_to_story(
),
]
mock_tts.side_effect = [b"turn-1-audio", b"turn-2-audio"]
mock_generate_cover.return_value = "https://example.com/voice-cover.png"
mock_queue_asset_generation.return_value = {
"id": "cover-job-123",
"story_id": 1,
"output_mode": "asset_generation",
"input_type": "image",
"status": "running",
"current_step": "request_accepted",
"progress_percent": 5,
"progress_label": "已接收请求",
"is_terminal": False,
"can_cancel": True,
"can_retry": False,
"result_snapshot": {},
"error_message": None,
"created_at": "2026-04-20T00:00:00Z",
"updated_at": "2026-04-20T00:00:00Z",
}
transport = ASGITransport(app=app)
try:
@@ -156,6 +172,7 @@ async def test_voice_session_correct_turn_and_finalize_to_story(
finalize_data = response.json()
story_id = finalize_data["story_id"]
assert finalize_data["status"] == "completed"
assert finalize_data["generation_job_id"] == "cover-job-123"
response = await client.get(f"/api/stories/{story_id}")
assert response.status_code == 200
@@ -172,7 +189,7 @@ async def test_voice_session_correct_turn_and_finalize_to_story(
assert session_data["final_story_id"] == story_id
assert session_data["can_continue"] is False
assert session_data["story_state"]["final_summary"]
mock_generate_cover.assert_awaited_once()
mock_queue_asset_generation.assert_awaited_once()
finally:
app.dependency_overrides.clear()