from __future__ import annotations import pytest from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine from app.core.db import Base from app.models.studio import ( CameraAngle, CameraMovement, CameraShotType, Chapter, Project, ProjectStyle, ProjectVisualStyle, Shot, ShotDetail, VFXType, ) from app.schemas.skills.script_processing import ScriptDivisionResult, ShotDivision from app.services.studio.script_division import write_division_result_to_chapter async def _build_session() -> tuple[AsyncSession, object]: engine = create_async_engine("sqlite+aiosqlite:///:memory:", future=True) session_local = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) async with engine.begin() as conn: await conn.run_sync(Base.metadata.create_all) return session_local(), engine def _division_result() -> ScriptDivisionResult: return ScriptDivisionResult( shots=[ ShotDivision( index=1, start_line=1, end_line=3, script_excerpt="主角走进房间。", shot_name="进入房间", ), ShotDivision( index=2, start_line=4, end_line=6, script_excerpt="镜头切到窗边。", shot_name="窗边对话", ), ], total_shots=2, notes="test", ) @pytest.mark.asyncio async def test_write_division_result_to_empty_chapter_creates_shots_and_details() -> None: db, engine = await _build_session() async with db: project = Project( id="p1", name="项目一", description="", style=ProjectStyle.real_people_city, visual_style=ProjectVisualStyle.live_action, ) chapter = Chapter(id="c1", project_id="p1", index=1, title="第一章") db.add_all([project, chapter]) await db.commit() await write_division_result_to_chapter(db, chapter_id="c1", result=_division_result()) shots = (await db.execute(select(Shot).order_by(Shot.index.asc()))).scalars().all() details = (await db.execute(select(ShotDetail).order_by(ShotDetail.id.asc()))).scalars().all() assert [shot.title for shot in shots] == ["进入房间", "窗边对话"] assert [shot.index for shot in shots] == [1, 2] assert len(details) == 2 assert all(detail.camera_shot == CameraShotType.ms for detail in details) assert all(detail.angle == CameraAngle.eye_level for detail in details) assert all(detail.movement == CameraMovement.static for detail in details) assert all(detail.duration == 4 for detail in details) assert all(detail.vfx_type == VFXType.none for detail in details) await engine.dispose() @pytest.mark.asyncio async def test_write_division_result_refuses_when_chapter_already_has_shots() -> None: db, engine = await _build_session() async with db: project = Project( id="p1", name="项目一", description="", style=ProjectStyle.real_people_city, visual_style=ProjectVisualStyle.live_action, ) chapter = Chapter(id="c1", project_id="p1", index=1, title="第一章") existing_shot = Shot(id="s1", chapter_id="c1", index=1, title="已有镜头") db.add_all([project, chapter, existing_shot]) await db.commit() with pytest.raises(HTTPException) as exc_info: await write_division_result_to_chapter(db, chapter_id="c1", result=_division_result()) assert exc_info.value.status_code == 400 assert "already has shots" in exc_info.value.detail await engine.dispose()