Files
xiaoxia-saas/tests/unit/test_unify_template_segments.py
T
xiaoxia ef686dde8f
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 5s
CI/CD Pipeline / Check push changed paths (push) Successful in 17s
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 / Production Browser E2E (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 / 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
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 / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Failing after 207h3m47s
CI/CD Pipeline / Deploy Production (push) Failing after 207h3m47s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 207h3m50s
CI/CD Pipeline / Build Production Web Image (push) Failing after 207h3m51s
CI/CD Pipeline / Build Production API Image (push) Failing after 207h3m51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Failing after 207h9m51s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 207h12m1s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 207h12m2s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 207h12m4s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 207h12m22s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 207h12m26s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 207h12m29s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 207h12m32s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 207h13m21s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 207h13m21s
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Failing after 207h13m26s
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Failing after 207h13m26s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 207h13m38s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Validate - Security (pull_request) Failing after 207h13m39s
CI/CD Pipeline / Validate - Style (pull_request) Failing after 207h13m40s
CI/CD Pipeline / Frontend Lint (push) Failing after 207h13m45s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 207h13m48s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 207h13m48s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 207h13m48s
CI/CD Pipeline / PR Build API Image (push) Failing after 207h13m49s
CI/CD Pipeline / Check push changed paths (pull_request) Failing after 207h13m50s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 207h13m53s
CI/CD Pipeline / CI Gate (push) Failing after 207h38m31s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 207h47m7s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 207h47m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 207h48m2s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Failing after 207h48m8s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Failing after 207h48m20s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 207h48m29s
CI/CD Pipeline / PR Build Web Image (push) Failing after 207h48m30s
fix(#1769): 模板列表过滤无片段配置的无效模板,避免前端选中后400 (#1772)
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-09-08 00:10:13 +08:00

256 lines
8.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统一模板 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