ef686dde8f
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / PR Build Worker Image (push) 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 / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 17s
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 / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 31s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 30s
CI/CD Pipeline / Build Staging API Image (push) Successful in 35s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 47s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 58s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 58s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 2m9s
CI/CD Pipeline / Integration Tests (push) Successful in 2m24s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m10s
CI/CD Pipeline / Validate - Style (push) Successful in 2m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 58s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m50s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m41s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m26s
AI Code Review / AI Code Review (pull_request) Successful in 6m24s
CI/CD Pipeline / Validate - Security (push) Successful in 7m3s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 7m39s
CI/CD Pipeline / Unit Tests (push) Successful in 9m55s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
256 lines
8.9 KiB
Python
256 lines
8.9 KiB
Python
"""统一模板 segments 数据源单元测试。
|
||
|
||
验证 template_repository 从 template_clip_configs 读取 segments,
|
||
写入走 template_clip_configs,回退兼容 template_segments。
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import sys
|
||
import uuid
|
||
from pathlib import Path
|
||
|
||
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
|
||
|
||
import pytest
|
||
from sqlalchemy import create_engine
|
||
from sqlalchemy.orm import 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 session():
|
||
engine = create_engine("sqlite:///:memory:")
|
||
Base.metadata.create_all(engine)
|
||
Session = sessionmaker(bind=engine)
|
||
s = Session()
|
||
try:
|
||
yield s
|
||
finally:
|
||
s.close()
|
||
|
||
|
||
@pytest.fixture()
|
||
def repo(session):
|
||
return SQLAlchemyTemplateRepository(session)
|
||
|
||
|
||
def _make_template(template_id=None, user_id="u1", name="测试模板", mode="one_take"):
|
||
tid = template_id or str(uuid.uuid4())
|
||
return Template(
|
||
id=tid,
|
||
user_id=user_id,
|
||
name=name,
|
||
mode=mode,
|
||
category="",
|
||
tags=[],
|
||
estimated_duration=30.0,
|
||
is_active=True,
|
||
segments=[],
|
||
)
|
||
|
||
|
||
def _make_segment(template_id, order=1, material_type=None):
|
||
return TemplateSegment(
|
||
id=str(uuid.uuid4()),
|
||
template_id=template_id,
|
||
segment_order=order,
|
||
duration_min=5.0,
|
||
duration_max=10.0,
|
||
material_type=material_type,
|
||
)
|
||
|
||
|
||
class TestCreateSegments:
|
||
def test_writes_to_clip_configs(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1)
|
||
repo.create_segments([seg])
|
||
clips = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == tpl.id).all()
|
||
assert len(clips) == 1
|
||
assert clips[0].clip_type == "main"
|
||
assert clips[0].order == 1
|
||
assert clips[0].min_duration == 5.0
|
||
|
||
def test_material_type_stored_in_config(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1, material_type="voiceover")
|
||
repo.create_segments([seg])
|
||
clip = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == tpl.id).first()
|
||
assert clip.config["material_type"] == "voiceover"
|
||
|
||
|
||
class TestListSegments:
|
||
def test_reads_from_clip_configs(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1, material_type="voiceover")
|
||
repo.create_segments([seg])
|
||
result = repo.list_segments(tpl.id)
|
||
assert len(result) == 1
|
||
assert result[0].material_type == "voiceover"
|
||
|
||
def test_fallback_to_old_table(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
old = TemplateSegmentModel(
|
||
id=str(uuid.uuid4()),
|
||
template_id=tpl.id,
|
||
segment_order=1,
|
||
duration_min=3.0,
|
||
duration_max=8.0,
|
||
material_type="场景",
|
||
)
|
||
session.add(old)
|
||
session.commit()
|
||
result = repo.list_segments(tpl.id)
|
||
assert len(result) == 1
|
||
assert result[0].material_type == "场景"
|
||
|
||
def test_clip_configs_takes_priority(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1)
|
||
repo.create_segments([seg])
|
||
old = TemplateSegmentModel(
|
||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=1, duration_min=1.0, duration_max=2.0
|
||
)
|
||
session.add(old)
|
||
session.commit()
|
||
result = repo.list_segments(tpl.id)
|
||
assert len(result) == 1
|
||
assert result[0].duration_min == 5.0
|
||
|
||
|
||
class TestListByUser:
|
||
def test_batch_loads_from_clip_configs(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1, material_type="人物")
|
||
repo.create_segments([seg])
|
||
result = repo.list_by_user("u1")
|
||
assert len(result) == 1
|
||
assert len(result[0].segments) == 1
|
||
assert result[0].segments[0].material_type == "人物"
|
||
|
||
def test_fallback_for_old_data(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
old = TemplateSegmentModel(
|
||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=1, duration_min=2.0, duration_max=6.0
|
||
)
|
||
session.add(old)
|
||
session.commit()
|
||
result = repo.list_by_user("u1")
|
||
assert len(result) == 1
|
||
assert len(result[0].segments) == 1
|
||
assert result[0].segments[0].duration_min == 2.0
|
||
|
||
def test_valid_only_filters_templates_without_segments(self, repo, session):
|
||
"""#1769: valid_only=True 时排除两张片段表都没有记录的无效模板."""
|
||
# 有效模板:有 clip_configs
|
||
valid_clip = _make_template(name="有效模板-clip_configs")
|
||
repo.create(valid_clip)
|
||
repo.create_segments([_make_segment(valid_clip.id, order=1)])
|
||
# 有效模板:仅有旧表 template_segments 记录
|
||
valid_old = _make_template(name="有效模板-old_segments")
|
||
repo.create(valid_old)
|
||
old = TemplateSegmentModel(
|
||
id=str(uuid.uuid4()),
|
||
template_id=valid_old.id,
|
||
segment_order=1,
|
||
duration_min=2.0,
|
||
duration_max=6.0,
|
||
)
|
||
session.add(old)
|
||
session.commit()
|
||
# 无效模板:两张表都没有记录
|
||
invalid = _make_template(name="无效模板-无片段")
|
||
repo.create(invalid)
|
||
|
||
# 默认不过滤:编辑器视角能看到全部 3 个模板
|
||
all_templates = repo.list_by_user("u1")
|
||
assert len(all_templates) == 3
|
||
assert repo.count_by_user("u1") == 3
|
||
|
||
# valid_only=True:剪辑页视角只返回 2 个有效模板
|
||
valid_templates = repo.list_by_user("u1", valid_only=True)
|
||
assert {t.name for t in valid_templates} == {"有效模板-clip_configs", "有效模板-old_segments"}
|
||
assert all(len(t.segments) > 0 for t in valid_templates)
|
||
assert repo.count_by_user("u1", valid_only=True) == 2
|
||
|
||
def test_valid_only_with_filters_and_pagination(self, repo, session):
|
||
"""valid_only 与其他过滤/分页条件组合使用."""
|
||
tpl = _make_template(name="口播模板", mode="voice_over")
|
||
repo.create(tpl)
|
||
repo.create_segments([_make_segment(tpl.id, order=1, material_type="人物")])
|
||
_invalid = _make_template(name="口播无效模板", mode="voice_over")
|
||
repo.create(_invalid)
|
||
|
||
result = repo.list_by_user("u1", mode="voice_over", valid_only=True)
|
||
assert len(result) == 1
|
||
assert result[0].name == "口播模板"
|
||
assert repo.count_by_user("u1", mode="voice_over", valid_only=True) == 1
|
||
|
||
|
||
class TestCopyTemplate:
|
||
def test_copy_writes_to_clip_configs(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1, material_type="voiceover")
|
||
repo.create_segments([seg])
|
||
copied = repo.copy_template(tpl.id, "u1", "副本模板")
|
||
assert copied.id != tpl.id
|
||
clips = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == copied.id).all()
|
||
assert len(clips) == 1
|
||
assert clips[0].config["material_type"] == "voiceover"
|
||
|
||
def test_copy_empty_segments(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
copied = repo.copy_template(tpl.id, "u1", "空副本")
|
||
assert len(copied.segments) == 0
|
||
|
||
|
||
class TestDelete:
|
||
def test_delete_cleans_both_tables(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1)
|
||
repo.create_segments([seg])
|
||
old = TemplateSegmentModel(
|
||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=1, duration_min=1.0, duration_max=2.0
|
||
)
|
||
session.add(old)
|
||
session.commit()
|
||
repo.delete(tpl.id, "u1")
|
||
c1 = session.query(TemplateClipConfigModel).filter(TemplateClipConfigModel.template_id == tpl.id).count()
|
||
c2 = session.query(TemplateSegmentModel).filter(TemplateSegmentModel.template_id == tpl.id).count()
|
||
assert c1 == 0
|
||
assert c2 == 0
|
||
|
||
def test_delete_segments_by_template(self, repo, session):
|
||
tpl = _make_template()
|
||
repo.create(tpl)
|
||
seg = _make_segment(tpl.id, order=1)
|
||
repo.create_segments([seg])
|
||
old = TemplateSegmentModel(
|
||
id=str(uuid.uuid4()), template_id=tpl.id, segment_order=2, duration_min=1.0, duration_max=2.0
|
||
)
|
||
session.add(old)
|
||
session.commit()
|
||
count = repo.delete_segments_by_template(tpl.id)
|
||
assert count == 2
|