320b2fcedb
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 6s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2m40s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 3m15s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 3m23s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Failing after 3m40s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 3m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 3m55s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m21s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m3s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
384 lines
14 KiB
Python
384 lines
14 KiB
Python
"""Tests for unified template segments (read from template_clip_configs)."""
|
|
|
|
import uuid
|
|
|
|
<<<<<<< Updated upstream
|
|
from unittest.mock import MagicMock
|
|
|
|
=======
|
|
>>>>>>> Stashed changes
|
|
|
|
import pytest
|
|
from sqlalchemy import create_engine
|
|
from sqlalchemy.orm import Session, sessionmaker
|
|
|
|
from packages.adapters.sqlalchemy_impl.models import Base, TemplateClipConfigModel, TemplateModel, TemplateSegmentModel
|
|
from packages.adapters.sqlalchemy_impl.template_repository import SQLAlchemyTemplateRepository
|
|
from packages.domain.template import Template, TemplateSegment
|
|
|
|
|
|
@pytest.fixture
|
|
def db_session():
|
|
engine = create_engine("sqlite:///:memory:")
|
|
Base.metadata.create_all(engine)
|
|
SessionLocal = sessionmaker(bind=engine)
|
|
session = SessionLocal()
|
|
yield session
|
|
session.close()
|
|
|
|
|
|
@pytest.fixture
|
|
def repo(db_session):
|
|
return SQLAlchemyTemplateRepository(db_session)
|
|
|
|
|
|
def _make_template(user_id: str = "user1", name: str = "test") -> Template:
|
|
<<<<<<< Updated upstream
|
|
return Template(
|
|
id=uuid.uuid4().hex,
|
|
user_id=user_id,
|
|
name=name,
|
|
mode="one_take",
|
|
)
|
|
=======
|
|
return Template(id=uuid.uuid4().hex, user_id=user_id, name=name, mode="one_take")
|
|
>>>>>>> Stashed changes
|
|
|
|
|
|
def _make_segment(template_id: str, order: int, material_type: str = "video") -> TemplateSegment:
|
|
return TemplateSegment(
|
|
<<<<<<< Updated upstream
|
|
id=uuid.uuid4().hex,
|
|
template_id=template_id,
|
|
segment_order=order,
|
|
duration_min=3.0,
|
|
duration_max=5.0,
|
|
material_type=material_type,
|
|
=======
|
|
id=uuid.uuid4().hex, template_id=template_id, segment_order=order,
|
|
duration_min=3.0, duration_max=5.0, material_type=material_type,
|
|
>>>>>>> Stashed changes
|
|
)
|
|
|
|
|
|
class TestCreateSegmentsWritesToClipConfigs:
|
|
<<<<<<< Updated upstream
|
|
"""create_segments 应该写入 template_clip_configs 表"""
|
|
|
|
def test_create_segments_writes_clip_configs(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
=======
|
|
def test_create_segments_writes_clip_configs(self, repo, db_session):
|
|
>>>>>>> Stashed changes
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
seg = _make_segment(tpl.id, 0, "voiceover")
|
|
repo.create_segments([seg])
|
|
<<<<<<< Updated upstream
|
|
|
|
# 验证 template_clip_configs 有记录
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == tpl.id
|
|
).all()
|
|
assert len(clips) == 1
|
|
assert clips[0].order == 0
|
|
assert clips[0].min_duration == 3.0
|
|
assert clips[0].max_duration == 5.0
|
|
assert clips[0].clip_type == "main"
|
|
assert clips[0].config.get("material_type") == "voiceover"
|
|
|
|
def test_create_segments_no_old_table_write(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
"""确保不写入旧 template_segments 表"""
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
seg = _make_segment(tpl.id, 0)
|
|
repo.create_segments([seg])
|
|
|
|
old_rows = db_session.query(TemplateSegmentModel).filter(
|
|
TemplateSegmentModel.template_id == tpl.id
|
|
).all()
|
|
=======
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == tpl.id).all()
|
|
assert len(clips) == 1
|
|
assert clips[0].order == 0
|
|
assert clips[0].config.get("material_type") == "voiceover"
|
|
|
|
def test_create_segments_no_old_table_write(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0)])
|
|
old_rows = db_session.query(TemplateSegmentModel).filter(
|
|
TemplateSegmentModel.template_id == tpl.id).all()
|
|
>>>>>>> Stashed changes
|
|
assert len(old_rows) == 0
|
|
|
|
|
|
class TestReadSegmentsFromClipConfigs:
|
|
<<<<<<< Updated upstream
|
|
"""list_segments 应该从 template_clip_configs 读取"""
|
|
|
|
def test_list_segments_reads_clip_configs(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
seg = _make_segment(tpl.id, 0, "video")
|
|
repo.create_segments([seg])
|
|
|
|
segments = repo.list_segments(tpl.id)
|
|
assert len(segments) == 1
|
|
assert segments[0].segment_order == 0
|
|
assert segments[0].duration_min == 3.0
|
|
assert segments[0].duration_max == 5.0
|
|
assert segments[0].material_type == "video"
|
|
|
|
def test_list_segments_fallback_to_old_table(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
"""如果 template_clip_configs 为空,回退读旧 template_segments"""
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
|
|
# 直接写入旧表(模拟历史数据)
|
|
old_model = TemplateSegmentModel(
|
|
id=uuid.uuid4().hex,
|
|
template_id=tpl.id,
|
|
segment_order=0,
|
|
duration_min=2.0,
|
|
duration_max=4.0,
|
|
material_type="人物",
|
|
)
|
|
db_session.add(old_model)
|
|
db_session.commit()
|
|
|
|
segments = repo.list_segments(tpl.id)
|
|
assert len(segments) == 1
|
|
assert segments[0].material_type == "人物"
|
|
assert segments[0].duration_min == 2.0
|
|
|
|
def test_list_segments_prefers_clip_configs_over_old(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
"""如果两个表都有数据,优先读 template_clip_configs"""
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
|
|
# 写入新表
|
|
seg = _make_segment(tpl.id, 0, "video")
|
|
repo.create_segments([seg])
|
|
|
|
# 也写入旧表
|
|
old_model = TemplateSegmentModel(
|
|
id=uuid.uuid4().hex,
|
|
template_id=tpl.id,
|
|
segment_order=99,
|
|
duration_min=1.0,
|
|
duration_max=2.0,
|
|
material_type="stale_data",
|
|
)
|
|
db_session.add(old_model)
|
|
db_session.commit()
|
|
|
|
segments = repo.list_segments(tpl.id)
|
|
# 应该只返回新表的数据
|
|
assert len(segments) == 1
|
|
assert segments[0].material_type == "video"
|
|
assert segments[0].segment_order == 0
|
|
|
|
|
|
class TestListByUserSegments:
|
|
"""list_by_user 批量加载 segments 应从 template_clip_configs 读取"""
|
|
|
|
def test_batch_load_segments_from_clip_configs(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
=======
|
|
def test_list_segments_reads_clip_configs(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0, "video")])
|
|
segments = repo.list_segments(tpl.id)
|
|
assert len(segments) == 1
|
|
assert segments[0].material_type == "video"
|
|
|
|
def test_list_segments_fallback_to_old_table(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
old_model = TemplateSegmentModel(
|
|
id=uuid.uuid4().hex, template_id=tpl.id, segment_order=0,
|
|
duration_min=2.0, duration_max=4.0, material_type="人物")
|
|
db_session.add(old_model)
|
|
db_session.commit()
|
|
segments = repo.list_segments(tpl.id)
|
|
assert len(segments) == 1
|
|
assert segments[0].material_type == "人物"
|
|
|
|
def test_list_segments_prefers_clip_configs_over_old(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0, "video")])
|
|
old_model = TemplateSegmentModel(
|
|
id=uuid.uuid4().hex, template_id=tpl.id, segment_order=99,
|
|
duration_min=1.0, duration_max=2.0, material_type="stale_data")
|
|
db_session.add(old_model)
|
|
db_session.commit()
|
|
segments = repo.list_segments(tpl.id)
|
|
assert len(segments) == 1
|
|
assert segments[0].material_type == "video"
|
|
|
|
|
|
class TestListByUserSegments:
|
|
def test_batch_load_segments_from_clip_configs(self, repo, db_session):
|
|
>>>>>>> Stashed changes
|
|
tpl1 = _make_template(name="tpl1")
|
|
tpl2 = _make_template(name="tpl2")
|
|
repo.create(tpl1)
|
|
repo.create(tpl2)
|
|
<<<<<<< Updated upstream
|
|
|
|
repo.create_segments([_make_segment(tpl1.id, 0, "video")])
|
|
repo.create_segments([_make_segment(tpl2.id, 0, "voiceover"), _make_segment(tpl2.id, 1, "video")])
|
|
|
|
templates = repo.list_by_user("user1")
|
|
assert len(templates) == 2
|
|
|
|
tpl1_result = next(t for t in templates if t.id == tpl1.id)
|
|
tpl2_result = next(t for t in templates if t.id == tpl2.id)
|
|
|
|
assert len(tpl1_result.segments) == 1
|
|
assert tpl1_result.segments[0].material_type == "video"
|
|
|
|
assert len(tpl2_result.segments) == 2
|
|
assert tpl2_result.segments[0].material_type == "voiceover"
|
|
assert tpl2_result.segments[1].material_type == "video"
|
|
|
|
|
|
class TestDeleteSegments:
|
|
"""delete_segments_by_template 应清理 template_clip_configs"""
|
|
|
|
def test_delete_segments_clears_clip_configs(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0)])
|
|
|
|
count = repo.delete_segments_by_template(tpl.id)
|
|
assert count == 1
|
|
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == tpl.id
|
|
).all()
|
|
assert len(clips) == 0
|
|
|
|
def test_delete_template_clears_both_tables(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0)])
|
|
|
|
# 也写入旧表
|
|
old_model = TemplateSegmentModel(
|
|
id=uuid.uuid4().hex,
|
|
template_id=tpl.id,
|
|
segment_order=0,
|
|
duration_min=1.0,
|
|
duration_max=2.0,
|
|
)
|
|
db_session.add(old_model)
|
|
db_session.commit()
|
|
|
|
repo.delete(tpl.id, "user1")
|
|
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == tpl.id
|
|
).all()
|
|
assert len(clips) == 0
|
|
|
|
old_rows = db_session.query(TemplateSegmentModel).filter(
|
|
TemplateSegmentModel.template_id == tpl.id
|
|
).all()
|
|
=======
|
|
repo.create_segments([_make_segment(tpl1.id, 0, "video")])
|
|
repo.create_segments([_make_segment(tpl2.id, 0, "voiceover"), _make_segment(tpl2.id, 1, "video")])
|
|
templates = repo.list_by_user("user1")
|
|
assert len(templates) == 2
|
|
tpl1_r = next(t for t in templates if t.id == tpl1.id)
|
|
tpl2_r = next(t for t in templates if t.id == tpl2.id)
|
|
assert len(tpl1_r.segments) == 1
|
|
assert len(tpl2_r.segments) == 2
|
|
|
|
|
|
class TestDeleteSegments:
|
|
def test_delete_segments_clears_clip_configs(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0)])
|
|
count = repo.delete_segments_by_template(tpl.id)
|
|
assert count >= 1
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == tpl.id).all()
|
|
assert len(clips) == 0
|
|
|
|
def test_delete_template_clears_both_tables(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0)])
|
|
old_model = TemplateSegmentModel(
|
|
id=uuid.uuid4().hex, template_id=tpl.id, segment_order=0,
|
|
duration_min=1.0, duration_max=2.0)
|
|
db_session.add(old_model)
|
|
db_session.commit()
|
|
repo.delete(tpl.id, "user1")
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == tpl.id).all()
|
|
assert len(clips) == 0
|
|
old_rows = db_session.query(TemplateSegmentModel).filter(
|
|
TemplateSegmentModel.template_id == tpl.id).all()
|
|
>>>>>>> Stashed changes
|
|
assert len(old_rows) == 0
|
|
|
|
|
|
class TestCopyTemplateSegments:
|
|
<<<<<<< Updated upstream
|
|
"""copy_template 应从 template_clip_configs 复制 segments"""
|
|
|
|
def test_copy_preserves_segments(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([
|
|
_make_segment(tpl.id, 0, "video"),
|
|
_make_segment(tpl.id, 1, "voiceover"),
|
|
])
|
|
|
|
copied = repo.copy_template(tpl.id, "user1", "copy")
|
|
assert len(copied.segments) == 2
|
|
assert copied.segments[0].material_type == "video"
|
|
assert copied.segments[1].material_type == "voiceover"
|
|
|
|
# 验证新模板的 clip_configs 有数据
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == copied.id
|
|
).all()
|
|
=======
|
|
def test_copy_preserves_segments(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0, "video"), _make_segment(tpl.id, 1, "voiceover")])
|
|
copied = repo.copy_template(tpl.id, "user1", "copy")
|
|
assert len(copied.segments) == 2
|
|
clips = db_session.query(TemplateClipConfigModel).filter(
|
|
TemplateClipConfigModel.template_id == copied.id).all()
|
|
>>>>>>> Stashed changes
|
|
assert len(clips) == 2
|
|
|
|
|
|
class TestNullMaterialType:
|
|
<<<<<<< Updated upstream
|
|
"""material_type 为 None 时不应报错"""
|
|
|
|
def test_null_material_type(self, repo: SQLAlchemyTemplateRepository, db_session: Session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
seg = _make_segment(tpl.id, 0, None)
|
|
repo.create_segments([seg])
|
|
|
|
=======
|
|
def test_null_material_type(self, repo, db_session):
|
|
tpl = _make_template()
|
|
repo.create(tpl)
|
|
repo.create_segments([_make_segment(tpl.id, 0, None)])
|
|
>>>>>>> Stashed changes
|
|
segments = repo.list_segments(tpl.id)
|
|
assert len(segments) == 1
|
|
assert segments[0].material_type is None
|