diff --git a/tests/unit/test_feature_flags_infra.py b/tests/unit/test_feature_flags_infra.py index d5cf2c3db..5b9e91dbb 100755 --- a/tests/unit/test_feature_flags_infra.py +++ b/tests/unit/test_feature_flags_infra.py @@ -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): diff --git a/tests/unit/test_in_memory_asset_library_repository.py b/tests/unit/test_in_memory_asset_library_repository.py index 523b1b4fe..66443102c 100755 --- a/tests/unit/test_in_memory_asset_library_repository.py +++ b/tests/unit/test_in_memory_asset_library_repository.py @@ -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) diff --git a/tests/unit/test_in_memory_asset_repository.py b/tests/unit/test_in_memory_asset_repository.py index ea1bfec49..9f1d2c625 100755 --- a/tests/unit/test_in_memory_asset_repository.py +++ b/tests/unit/test_in_memory_asset_repository.py @@ -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) diff --git a/tests/unit/test_in_memory_repositories.py b/tests/unit/test_in_memory_repositories.py index a926d1372..c90371b92 100755 --- a/tests/unit/test_in_memory_repositories.py +++ b/tests/unit/test_in_memory_repositories.py @@ -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() diff --git a/tests/unit/test_reverse_engine.py b/tests/unit/test_reverse_engine.py index d3a72e57e..0cb0db7c5 100755 --- a/tests/unit/test_reverse_engine.py +++ b/tests/unit/test_reverse_engine.py @@ -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) == "" diff --git a/tests/unit/test_task_enqueue.py b/tests/unit/test_task_enqueue.py index 9c50f4319..e7046f9ff 100755 --- a/tests/unit/test_task_enqueue.py +++ b/tests/unit/test_task_enqueue.py @@ -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") diff --git a/tests/unit/test_templates_editor_schemas.py b/tests/unit/test_templates_editor_schemas.py index 8679d8d47..19b335f31 100755 --- a/tests/unit/test_templates_editor_schemas.py +++ b/tests/unit/test_templates_editor_schemas.py @@ -8,7 +8,6 @@ from __future__ import annotations import pytest - from app.api.routes.templates_editor.schemas import ( ExportUpdateRequest, )