style: auto-format with black + isort + prettier
CI/CD Pipeline / Check if frontend-only change (push) 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 / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m42s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m44s
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 / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m26s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m22s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 4m29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m16s
CI/CD Pipeline / Unit Tests (push) Failing after 5m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m58s
CI/CD Pipeline / Check if frontend-only change (push) 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 / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m42s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m44s
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 / Build Staging Web Image (push) Successful in 1m42s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m26s
CI/CD Pipeline / Validate - Code Quality (push) Failing after 3m13s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m22s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Successful in 4m29s
CI/CD Pipeline / Integration Tests (push) Successful in 2m16s
CI/CD Pipeline / Unit Tests (push) Failing after 5m46s
CI/CD Pipeline / Build Staging API Image (push) Successful in 11m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 3m18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 30s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 2m13s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 5m58s
This commit is contained in:
@@ -201,11 +201,14 @@ class TestFeatureFlagsManager:
|
||||
|
||||
def test_set_user_override(self, ff):
|
||||
ff.set_user_override(FeatureScope.BATCH_EXPORT, "user_42", True)
|
||||
assert ff.is_enabled(
|
||||
FeatureScope.BATCH_EXPORT,
|
||||
user_plan="free",
|
||||
user_id="user_42",
|
||||
) is True
|
||||
assert (
|
||||
ff.is_enabled(
|
||||
FeatureScope.BATCH_EXPORT,
|
||||
user_plan="free",
|
||||
user_id="user_42",
|
||||
)
|
||||
is True
|
||||
)
|
||||
|
||||
def test_set_user_override_nonexistent_raises(self, ff):
|
||||
with pytest.raises(KeyError):
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
"""InMemoryAssetLibraryRepository 单测 — 素材库仓储内存实现."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain import AssetLibrary, AssetLibraryKind
|
||||
from packages.adapters.in_memory.asset_library_repository import (
|
||||
InMemoryAssetLibraryRepository,
|
||||
)
|
||||
|
||||
from packages.domain import AssetLibrary, AssetLibraryKind
|
||||
|
||||
# ── Fixtures ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -21,11 +21,13 @@ def repo():
|
||||
def sample_libraries(repo):
|
||||
"""创建几个测试素材库."""
|
||||
libs = []
|
||||
for i, kind in enumerate([
|
||||
AssetLibraryKind.VIDEO,
|
||||
AssetLibraryKind.VOICE,
|
||||
AssetLibraryKind.IMAGE,
|
||||
]):
|
||||
for i, kind in enumerate(
|
||||
[
|
||||
AssetLibraryKind.VIDEO,
|
||||
AssetLibraryKind.VOICE,
|
||||
AssetLibraryKind.IMAGE,
|
||||
]
|
||||
):
|
||||
lib = AssetLibrary.create(
|
||||
project_id="proj-1",
|
||||
name=f"Library {i}",
|
||||
@@ -49,9 +51,7 @@ class TestAssetLibraryRepoCRUD:
|
||||
"""基本 CRUD 操作."""
|
||||
|
||||
def test_create_and_get(self, repo):
|
||||
lib = AssetLibrary.create(
|
||||
project_id="p1", name="Test Lib", kind=AssetLibraryKind.VIDEO
|
||||
)
|
||||
lib = AssetLibrary.create(project_id="p1", name="Test Lib", kind=AssetLibraryKind.VIDEO)
|
||||
created = repo.create(lib)
|
||||
assert created.id == lib.id
|
||||
assert created.name == "Test Lib"
|
||||
@@ -64,16 +64,12 @@ class TestAssetLibraryRepoCRUD:
|
||||
assert repo.get("nonexistent") is None
|
||||
|
||||
def test_find_by_id_alias(self, repo):
|
||||
lib = AssetLibrary.create(
|
||||
project_id="p1", name="Test", kind=AssetLibraryKind.VOICE
|
||||
)
|
||||
lib = AssetLibrary.create(project_id="p1", name="Test", kind=AssetLibraryKind.VOICE)
|
||||
repo.create(lib)
|
||||
assert repo.find_by_id(lib.id).id == lib.id
|
||||
|
||||
def test_update(self, repo):
|
||||
lib = AssetLibrary.create(
|
||||
project_id="p1", name="Old Name", kind=AssetLibraryKind.IMAGE
|
||||
)
|
||||
lib = AssetLibrary.create(project_id="p1", name="Old Name", kind=AssetLibraryKind.IMAGE)
|
||||
repo.create(lib)
|
||||
|
||||
lib.name = "New Name"
|
||||
@@ -84,9 +80,7 @@ class TestAssetLibraryRepoCRUD:
|
||||
assert fetched.name == "New Name"
|
||||
|
||||
def test_delete_existing(self, repo):
|
||||
lib = AssetLibrary.create(
|
||||
project_id="p1", name="To Delete", kind=AssetLibraryKind.VIDEO
|
||||
)
|
||||
lib = AssetLibrary.create(project_id="p1", name="To Delete", kind=AssetLibraryKind.VIDEO)
|
||||
repo.create(lib)
|
||||
|
||||
result = repo.delete(lib.id)
|
||||
@@ -129,9 +123,7 @@ class TestAssetLibraryRepoCounting:
|
||||
"""素材计数相关方法."""
|
||||
|
||||
def test_increment_asset_count(self, repo):
|
||||
lib = AssetLibrary.create(
|
||||
project_id="p1", name="Test", kind=AssetLibraryKind.VIDEO
|
||||
)
|
||||
lib = AssetLibrary.create(project_id="p1", name="Test", kind=AssetLibraryKind.VIDEO)
|
||||
repo.create(lib)
|
||||
assert lib.asset_count == 0
|
||||
assert lib.total_size == 0
|
||||
@@ -147,9 +139,7 @@ class TestAssetLibraryRepoCounting:
|
||||
assert fetched.total_size == 3072
|
||||
|
||||
def test_decrement_asset_count(self, repo):
|
||||
lib = AssetLibrary.create(
|
||||
project_id="p1", name="Test", kind=AssetLibraryKind.VIDEO
|
||||
)
|
||||
lib = AssetLibrary.create(project_id="p1", name="Test", kind=AssetLibraryKind.VIDEO)
|
||||
lib.asset_count = 3
|
||||
lib.total_size = 3000
|
||||
repo.create(lib)
|
||||
@@ -161,9 +151,7 @@ class TestAssetLibraryRepoCounting:
|
||||
|
||||
def test_decrement_not_below_zero(self, repo):
|
||||
"""计数和大小不会减到负数."""
|
||||
lib = AssetLibrary.create(
|
||||
project_id="p1", name="Test", kind=AssetLibraryKind.VIDEO
|
||||
)
|
||||
lib = AssetLibrary.create(project_id="p1", name="Test", kind=AssetLibraryKind.VIDEO)
|
||||
lib.asset_count = 1
|
||||
lib.total_size = 100
|
||||
repo.create(lib)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"""InMemoryAssetRepository 单测 — 素材仓储内存实现."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain import Asset, AssetStatus
|
||||
from packages.adapters.in_memory.asset_repository import InMemoryAssetRepository
|
||||
|
||||
from packages.domain import Asset, AssetStatus
|
||||
|
||||
# ── Fixtures ───────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -112,16 +112,25 @@ class TestAssetRepoQueries:
|
||||
|
||||
def test_find_by_library_and_file_type_video(self, repo):
|
||||
video = Asset.create(
|
||||
project_id="p1", library_id="lib-1", name="v.mp4",
|
||||
storage_key="v.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="lib-1",
|
||||
name="v.mp4",
|
||||
storage_key="v.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
audio = Asset.create(
|
||||
project_id="p1", library_id="lib-1", name="a.mp3",
|
||||
storage_key="a.mp3", mime_type="audio/mp3",
|
||||
project_id="p1",
|
||||
library_id="lib-1",
|
||||
name="a.mp3",
|
||||
storage_key="a.mp3",
|
||||
mime_type="audio/mp3",
|
||||
)
|
||||
image = Asset.create(
|
||||
project_id="p1", library_id="lib-1", name="i.jpg",
|
||||
storage_key="i.jpg", mime_type="image/jpeg",
|
||||
project_id="p1",
|
||||
library_id="lib-1",
|
||||
name="i.jpg",
|
||||
storage_key="i.jpg",
|
||||
mime_type="image/jpeg",
|
||||
)
|
||||
repo.create(video)
|
||||
repo.create(audio)
|
||||
@@ -144,18 +153,27 @@ class TestAssetRepoQueries:
|
||||
|
||||
def test_find_by_tag_ids_single_tag(self, repo):
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a1.tag_ids = ["tag1", "tag2"]
|
||||
a2 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a2.mp4",
|
||||
storage_key="a2.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a2.mp4",
|
||||
storage_key="a2.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a2.tag_ids = ["tag1"]
|
||||
a3 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a3.mp4",
|
||||
storage_key="a3.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a3.mp4",
|
||||
storage_key="a3.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a3.tag_ids = ["tag3"]
|
||||
repo.create(a1)
|
||||
@@ -168,13 +186,19 @@ class TestAssetRepoQueries:
|
||||
def test_find_by_tag_ids_multiple_tags_all_match(self, repo):
|
||||
"""必须包含所有指定标签(AND 逻辑)."""
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a1.tag_ids = ["tag1", "tag2"]
|
||||
a2 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a2.mp4",
|
||||
storage_key="a2.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a2.mp4",
|
||||
storage_key="a2.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a2.tag_ids = ["tag1"]
|
||||
repo.create(a1)
|
||||
@@ -212,12 +236,18 @@ class TestAssetRepoBatchOperations:
|
||||
|
||||
def test_batch_delete_marks_deleted(self, repo):
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a2 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a2.mp4",
|
||||
storage_key="a2.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a2.mp4",
|
||||
storage_key="a2.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
repo.create(a1)
|
||||
repo.create(a2)
|
||||
@@ -231,14 +261,20 @@ class TestAssetRepoBatchOperations:
|
||||
|
||||
def test_batch_delete_skip_already_deleted(self, repo):
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a1.status = AssetStatus.DELETED
|
||||
repo.create(a1)
|
||||
a2 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a2.mp4",
|
||||
storage_key="a2.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a2.mp4",
|
||||
storage_key="a2.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
repo.create(a2)
|
||||
|
||||
@@ -251,20 +287,24 @@ class TestAssetRepoBatchOperations:
|
||||
|
||||
def test_batch_update_metadata(self, repo):
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a1.metadata = {"key1": "val1"}
|
||||
a2 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a2.mp4",
|
||||
storage_key="a2.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a2.mp4",
|
||||
storage_key="a2.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
repo.create(a1)
|
||||
repo.create(a2)
|
||||
|
||||
count = repo.batch_update_metadata(
|
||||
[a1.id, a2.id], {"key2": "val2"}
|
||||
)
|
||||
count = repo.batch_update_metadata([a1.id, a2.id], {"key2": "val2"})
|
||||
assert count == 2
|
||||
|
||||
# 合并而非覆盖
|
||||
@@ -274,8 +314,11 @@ class TestAssetRepoBatchOperations:
|
||||
|
||||
def test_batch_add_tags(self, repo):
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a1.tag_ids = ["existing"]
|
||||
repo.create(a1)
|
||||
@@ -291,8 +334,11 @@ class TestAssetRepoBatchOperations:
|
||||
def test_batch_add_tags_dedup(self, repo):
|
||||
"""添加已存在的标签不会重复."""
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a1.tag_ids = ["tag1"]
|
||||
repo.create(a1)
|
||||
@@ -305,8 +351,11 @@ class TestAssetRepoBatchOperations:
|
||||
|
||||
def test_batch_replace_tags(self, repo):
|
||||
a1 = Asset.create(
|
||||
project_id="p1", library_id="l1", name="a1.mp4",
|
||||
storage_key="a1.mp4", mime_type="video/mp4",
|
||||
project_id="p1",
|
||||
library_id="l1",
|
||||
name="a1.mp4",
|
||||
storage_key="a1.mp4",
|
||||
mime_type="video/mp4",
|
||||
)
|
||||
a1.tag_ids = ["old1", "old2"]
|
||||
repo.create(a1)
|
||||
|
||||
@@ -8,14 +8,14 @@ from datetime import datetime
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.adapters.in_memory.project_repository import InMemoryProjectRepository
|
||||
from packages.adapters.in_memory.classification_job_repository import InMemoryClassificationJobRepository
|
||||
from packages.adapters.in_memory.ingest_job_repository import InMemoryIngestJobRepository
|
||||
from packages.domain import Project, ClassificationJob, IngestJob
|
||||
|
||||
from packages.adapters.in_memory.project_repository import InMemoryProjectRepository
|
||||
from packages.domain import ClassificationJob, IngestJob, Project
|
||||
|
||||
# ── Project Repository ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def project_repo():
|
||||
return InMemoryProjectRepository()
|
||||
@@ -97,6 +97,7 @@ class TestInMemoryProjectRepository:
|
||||
|
||||
# ── Classification Job Repository ──────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def classif_repo():
|
||||
return InMemoryClassificationJobRepository()
|
||||
@@ -149,6 +150,7 @@ class TestInMemoryClassificationJobRepository:
|
||||
|
||||
# ── Ingest Job Repository ──────────────────────────────────────────────────
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ingest_repo():
|
||||
return InMemoryIngestJobRepository()
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
"""ReverseEngine 单测 — 倒放引擎配置解析 + 滤镜构建."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from video_processing.reverse_engine import ReverseConfig, ReverseEngine
|
||||
|
||||
|
||||
# ── ReverseConfig.from_dict ────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -34,31 +33,37 @@ class TestReverseConfigFromDict:
|
||||
assert config.reverse_audio is True
|
||||
|
||||
def test_disable_video_only(self):
|
||||
config = ReverseConfig.from_dict({
|
||||
"enabled": True,
|
||||
"reverse_video": False,
|
||||
"reverse_audio": True,
|
||||
})
|
||||
config = ReverseConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"reverse_video": False,
|
||||
"reverse_audio": True,
|
||||
}
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.reverse_video is False
|
||||
assert config.reverse_audio is True
|
||||
|
||||
def test_disable_audio_only(self):
|
||||
config = ReverseConfig.from_dict({
|
||||
"enabled": True,
|
||||
"reverse_video": True,
|
||||
"reverse_audio": False,
|
||||
})
|
||||
config = ReverseConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"reverse_video": True,
|
||||
"reverse_audio": False,
|
||||
}
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.reverse_video is True
|
||||
assert config.reverse_audio is False
|
||||
|
||||
def test_both_disabled(self):
|
||||
config = ReverseConfig.from_dict({
|
||||
"enabled": True,
|
||||
"reverse_video": False,
|
||||
"reverse_audio": False,
|
||||
})
|
||||
config = ReverseConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"reverse_video": False,
|
||||
"reverse_audio": False,
|
||||
}
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.reverse_video is False
|
||||
assert config.reverse_audio is False
|
||||
@@ -75,22 +80,26 @@ class TestReverseConfigFromDict:
|
||||
|
||||
def test_truthy_values(self):
|
||||
"""非布尔真值也能被 bool() 转换."""
|
||||
config = ReverseConfig.from_dict({
|
||||
"enabled": 1,
|
||||
"reverse_video": 1,
|
||||
"reverse_audio": "yes",
|
||||
})
|
||||
config = ReverseConfig.from_dict(
|
||||
{
|
||||
"enabled": 1,
|
||||
"reverse_video": 1,
|
||||
"reverse_audio": "yes",
|
||||
}
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.reverse_video is True
|
||||
assert config.reverse_audio is True
|
||||
|
||||
def test_falsy_values(self):
|
||||
"""非布尔假值也能被 bool() 转换."""
|
||||
config = ReverseConfig.from_dict({
|
||||
"enabled": True,
|
||||
"reverse_video": 0,
|
||||
"reverse_audio": "",
|
||||
})
|
||||
config = ReverseConfig.from_dict(
|
||||
{
|
||||
"enabled": True,
|
||||
"reverse_video": 0,
|
||||
"reverse_audio": "",
|
||||
}
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.reverse_video is False
|
||||
assert config.reverse_audio is False
|
||||
@@ -125,17 +134,13 @@ class TestBuildVideoFilter:
|
||||
def test_exactly_max_safe_duration(self):
|
||||
"""刚好等于上限,允许."""
|
||||
config = ReverseConfig(enabled=True)
|
||||
result = ReverseEngine.build_video_filter(
|
||||
config, duration=ReverseEngine.MAX_SAFE_DURATION
|
||||
)
|
||||
result = ReverseEngine.build_video_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION)
|
||||
assert result == "reverse"
|
||||
|
||||
def test_exceeds_max_safe_duration_skips(self):
|
||||
"""超过上限,跳过倒放,返回空字符串."""
|
||||
config = ReverseConfig(enabled=True)
|
||||
result = ReverseEngine.build_video_filter(
|
||||
config, duration=ReverseEngine.MAX_SAFE_DURATION + 1
|
||||
)
|
||||
result = ReverseEngine.build_video_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION + 1)
|
||||
assert result == ""
|
||||
|
||||
def test_negative_duration(self):
|
||||
@@ -173,22 +178,16 @@ class TestBuildAudioFilter:
|
||||
|
||||
def test_exactly_max_safe_duration(self):
|
||||
config = ReverseConfig(enabled=True)
|
||||
result = ReverseEngine.build_audio_filter(
|
||||
config, duration=ReverseEngine.MAX_SAFE_DURATION
|
||||
)
|
||||
result = ReverseEngine.build_audio_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION)
|
||||
assert result == "areverse"
|
||||
|
||||
def test_exceeds_max_safe_duration_skips(self):
|
||||
config = ReverseConfig(enabled=True)
|
||||
result = ReverseEngine.build_audio_filter(
|
||||
config, duration=ReverseEngine.MAX_SAFE_DURATION + 1
|
||||
)
|
||||
result = ReverseEngine.build_audio_filter(config, duration=ReverseEngine.MAX_SAFE_DURATION + 1)
|
||||
assert result == ""
|
||||
|
||||
def test_both_video_and_audio_disabled(self):
|
||||
"""两个都关,两个滤镜都为空."""
|
||||
config = ReverseConfig(
|
||||
enabled=True, reverse_video=False, reverse_audio=False
|
||||
)
|
||||
config = ReverseConfig(enabled=True, reverse_video=False, reverse_audio=False)
|
||||
assert ReverseEngine.build_video_filter(config, 10) == ""
|
||||
assert ReverseEngine.build_audio_filter(config, 10) == ""
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
"""task_enqueue 单测 — 队列限流 + 安全入队逻辑."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from app.core.task_enqueue import (
|
||||
USER_PENDING_LIMIT,
|
||||
GLOBAL_PENDING_LIMIT,
|
||||
UserPendingLimitExceeded,
|
||||
USER_PENDING_LIMIT,
|
||||
GlobalQueueFull,
|
||||
UserPendingLimitExceeded,
|
||||
check_queue_limits,
|
||||
safe_enqueue_generation_task,
|
||||
)
|
||||
|
||||
|
||||
# ── Fixtures / Helpers ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -57,18 +56,14 @@ class TestCheckQueueLimits:
|
||||
|
||||
def test_global_at_limit_raises(self):
|
||||
"""达到全局上限即拒绝."""
|
||||
repo = MockRepository(
|
||||
global_count=GLOBAL_PENDING_LIMIT, user_count=1
|
||||
)
|
||||
repo = MockRepository(global_count=GLOBAL_PENDING_LIMIT, user_count=1)
|
||||
with pytest.raises(GlobalQueueFull) as exc_info:
|
||||
check_queue_limits("user-1", repo)
|
||||
assert exc_info.value.pending_count == GLOBAL_PENDING_LIMIT
|
||||
assert exc_info.value.limit == GLOBAL_PENDING_LIMIT
|
||||
|
||||
def test_global_over_limit_raises(self):
|
||||
repo = MockRepository(
|
||||
global_count=GLOBAL_PENDING_LIMIT + 1, user_count=1
|
||||
)
|
||||
repo = MockRepository(global_count=GLOBAL_PENDING_LIMIT + 1, user_count=1)
|
||||
with pytest.raises(GlobalQueueFull):
|
||||
check_queue_limits("user-1", repo)
|
||||
|
||||
@@ -124,14 +119,10 @@ class TestSafeEnqueueGenerationTask:
|
||||
repo = MockRepository(global_count=1, user_count=1)
|
||||
task = make_mock_task()
|
||||
|
||||
result = safe_enqueue_generation_task(
|
||||
task, repo, user_id="user-1"
|
||||
)
|
||||
result = safe_enqueue_generation_task(task, repo, user_id="user-1")
|
||||
|
||||
assert result is True
|
||||
mock_celery.send_task.assert_called_once_with(
|
||||
"worker.generate_video", args=[task.id]
|
||||
)
|
||||
mock_celery.send_task.assert_called_once_with("worker.generate_video", args=[task.id])
|
||||
task.mark_failed.assert_not_called()
|
||||
|
||||
@patch("app.core.task_enqueue.celery_app")
|
||||
@@ -146,9 +137,7 @@ class TestSafeEnqueueGenerationTask:
|
||||
@patch("app.core.task_enqueue.celery_app")
|
||||
def test_precheck_global_over_marks_failed(self, mock_celery):
|
||||
"""入队前全局超限:标记 failed,抛异常."""
|
||||
repo = MockRepository(
|
||||
global_count=GLOBAL_PENDING_LIMIT + 1, user_count=0
|
||||
)
|
||||
repo = MockRepository(global_count=GLOBAL_PENDING_LIMIT + 1, user_count=0)
|
||||
task = make_mock_task()
|
||||
|
||||
with pytest.raises(GlobalQueueFull):
|
||||
@@ -161,9 +150,7 @@ class TestSafeEnqueueGenerationTask:
|
||||
@patch("app.core.task_enqueue.celery_app")
|
||||
def test_precheck_user_over_marks_failed(self, mock_celery):
|
||||
"""入队前用户超限:标记 failed,抛异常."""
|
||||
repo = MockRepository(
|
||||
global_count=5, user_count=USER_PENDING_LIMIT + 1
|
||||
)
|
||||
repo = MockRepository(global_count=5, user_count=USER_PENDING_LIMIT + 1)
|
||||
task = make_mock_task()
|
||||
|
||||
with pytest.raises(UserPendingLimitExceeded):
|
||||
@@ -211,9 +198,7 @@ class TestSafeEnqueueGenerationTask:
|
||||
return GLOBAL_PENDING_LIMIT + 1 # 入队后再查,超限
|
||||
|
||||
repo = MockRepository(global_count=GLOBAL_PENDING_LIMIT, user_count=0)
|
||||
repo.count_pending_total = MagicMock(
|
||||
side_effect=count_pending_total_side_effect
|
||||
)
|
||||
repo.count_pending_total = MagicMock(side_effect=count_pending_total_side_effect)
|
||||
task = make_mock_task()
|
||||
|
||||
with pytest.raises(GlobalQueueFull):
|
||||
@@ -227,9 +212,7 @@ class TestSafeEnqueueGenerationTask:
|
||||
@patch("app.core.task_enqueue.celery_app")
|
||||
def test_postcheck_user_over_rollback(self, mock_celery):
|
||||
"""入队后用户超限:回滚标记 failed,抛异常."""
|
||||
repo = MockRepository(
|
||||
global_count=5, user_count=USER_PENDING_LIMIT
|
||||
)
|
||||
repo = MockRepository(global_count=5, user_count=USER_PENDING_LIMIT)
|
||||
# 入队前用 > 判断,等于上限通过;入队后模拟并发超限
|
||||
original_user_count = repo.count_pending_by_user
|
||||
|
||||
@@ -241,9 +224,7 @@ class TestSafeEnqueueGenerationTask:
|
||||
return USER_PENDING_LIMIT # 用 > 判断,等于时通过
|
||||
return USER_PENDING_LIMIT + 1 # 入队后,超限
|
||||
|
||||
repo.count_pending_by_user = MagicMock(
|
||||
side_effect=count_by_user_side_effect
|
||||
)
|
||||
repo.count_pending_by_user = MagicMock(side_effect=count_by_user_side_effect)
|
||||
task = make_mock_task()
|
||||
|
||||
with pytest.raises(UserPendingLimitExceeded):
|
||||
@@ -257,9 +238,7 @@ class TestSafeEnqueueGenerationTask:
|
||||
repo = MockRepository(global_count=1, user_count=1)
|
||||
task = make_mock_task()
|
||||
|
||||
result = safe_enqueue_generation_task(
|
||||
task, repo, user_id="user-1", log_task_status=True
|
||||
)
|
||||
result = safe_enqueue_generation_task(task, repo, user_id="user-1", log_task_status=True)
|
||||
assert result is True
|
||||
|
||||
@patch("app.core.task_enqueue.celery_app")
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from app.api.routes.templates_editor.schemas import (
|
||||
ExportUpdateRequest,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user