"""素材批量操作单元测试:软删除、批量打标签、批量分类、批量智能视图标记。""" import sys from pathlib import Path sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api")) from packages.adapters.in_memory.asset_repository import InMemoryAssetRepository from packages.domain import Asset, AssetStatus class TestBatchSoftDelete: """batch_delete 软删除测试。""" def _make_assets(self, repo: InMemoryAssetRepository, count: int = 5) -> list[Asset]: assets = [] for i in range(count): asset = Asset.create( project_id="proj-1", library_id="lib-1", name=f"asset_{i}.mp4", storage_key=f"uploads/asset_{i}.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) repo.create(asset) assets.append(asset) return assets def test_batch_soft_delete_marks_status_deleted(self): """软删除:status 变为 deleted,记录仍然存在。""" repo = InMemoryAssetRepository() assets = self._make_assets(repo, 3) ids_to_delete = [assets[0].id, assets[2].id] count = repo.batch_delete(ids_to_delete) assert count == 2 # 记录仍在,只是 status 变了 assert repo.get(assets[0].id) is not None assert repo.get(assets[0].id).status == AssetStatus.DELETED assert repo.get(assets[2].id).status == AssetStatus.DELETED # 未删除的保持原样 assert repo.get(assets[1].id).status == AssetStatus.READY def test_batch_soft_delete_idempotent(self): """重复删除已删除的素材,计数不增加。""" repo = InMemoryAssetRepository() assets = self._make_assets(repo, 2) count1 = repo.batch_delete([assets[0].id]) count2 = repo.batch_delete([assets[0].id]) assert count1 == 1 assert count2 == 0 assert repo.get(assets[0].id).status == AssetStatus.DELETED def test_batch_soft_delete_empty_list(self): repo = InMemoryAssetRepository() self._make_assets(repo, 3) assert repo.batch_delete([]) == 0 def test_batch_soft_delete_nonexistent_ids(self): repo = InMemoryAssetRepository() self._make_assets(repo, 3) assert repo.batch_delete(["nonexistent-1", "nonexistent-2"]) == 0 class TestBatchUpdateMetadata: """batch_update_metadata 批量更新 metadata 测试。""" def test_batch_update_category(self): """批量修改分类(metadata.category)。""" repo = InMemoryAssetRepository() assets = [] for i in range(3): asset = Asset.create( project_id="proj-1", library_id="lib-1", name=f"v{i}.mp4", storage_key=f"v{i}.mp4", mime_type="video/mp4", metadata={"existing_key": "existing_value"}, status=AssetStatus.READY, ) repo.create(asset) assets.append(asset) ids = [a.id for a in assets] count = repo.batch_update_metadata(ids, {"category": "person"}) assert count == 3 for a in assets: updated = repo.get(a.id) assert updated.metadata["category"] == "person" assert updated.metadata["existing_key"] == "existing_value" # 合并而非覆盖 def test_batch_update_smart_view(self): """批量设置智能视图标记。""" repo = InMemoryAssetRepository() assets = [] for i in range(4): asset = Asset.create( project_id="proj-1", library_id="lib-1", name=f"v{i}.mp4", storage_key=f"v{i}.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) repo.create(asset) assets.append(asset) # 标记前2个为 recommended count = repo.batch_update_metadata([assets[0].id, assets[1].id], {"smart_view": "recommended"}) assert count == 2 assert repo.get(assets[0].id).metadata["smart_view"] == "recommended" assert repo.get(assets[1].id).metadata["smart_view"] == "recommended" # 其余不变 assert "smart_view" not in repo.get(assets[2].id).metadata # 再标记后2个为 high_risk count2 = repo.batch_update_metadata([assets[2].id, assets[3].id], {"smart_view": "high_risk"}) assert count2 == 2 assert repo.get(assets[2].id).metadata["smart_view"] == "high_risk" assert repo.get(assets[3].id).metadata["smart_view"] == "high_risk" def test_batch_update_metadata_partial_existing(self): """部分素材存在时,只更新存在的。""" repo = InMemoryAssetRepository() asset = Asset.create( project_id="proj-1", library_id="lib-1", name="v.mp4", storage_key="v.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) repo.create(asset) count = repo.batch_update_metadata([asset.id, "nonexistent"], {"category": "scenic"}) assert count == 1 assert repo.get(asset.id).metadata["category"] == "scenic" def test_batch_update_metadata_empty_list(self): repo = InMemoryAssetRepository() assert repo.batch_update_metadata([], {"category": "x"}) == 0 class TestBatchAddTags: """batch_add_tags 批量添加标签测试。""" def test_batch_add_tags_merges_and_dedups(self): """添加模式:合并去重,已有标签不重复添加。""" repo = InMemoryAssetRepository() assets = [] for i in range(3): asset = Asset.create( project_id="proj-1", library_id="lib-1", name=f"v{i}.mp4", storage_key=f"v{i}.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) asset.add_tag("tag-existing") repo.create(asset) assets.append(asset) ids = [a.id for a in assets] count = repo.batch_add_tags(ids, ["tag-1", "tag-2", "tag-existing"]) assert count == 3 # 都有新增标签,所以都算变更 for a in assets: updated = repo.get(a.id) assert set(updated.tag_ids) == {"tag-existing", "tag-1", "tag-2"} def test_batch_add_tags_no_change_when_all_exist(self): """所有标签都已存在时,返回0。""" repo = InMemoryAssetRepository() asset = Asset.create( project_id="proj-1", library_id="lib-1", name="v.mp4", storage_key="v.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) asset.add_tag("tag-a") asset.add_tag("tag-b") repo.create(asset) count = repo.batch_add_tags([asset.id], ["tag-a", "tag-b"]) assert count == 0 def test_batch_add_tags_empty_input(self): repo = InMemoryAssetRepository() assert repo.batch_add_tags([], ["tag-1"]) == 0 assert repo.batch_add_tags(["aid"], []) == 0 class TestBatchReplaceTags: """batch_replace_tags 批量替换标签测试。""" def test_batch_replace_tags_full_override(self): """替换模式:全量覆盖原有标签。""" repo = InMemoryAssetRepository() assets = [] for i in range(3): asset = Asset.create( project_id="proj-1", library_id="lib-1", name=f"v{i}.mp4", storage_key=f"v{i}.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) asset.add_tag(f"old-{i}") asset.add_tag("old-common") repo.create(asset) assets.append(asset) ids = [a.id for a in assets] count = repo.batch_replace_tags(ids, ["new-1", "new-2"]) assert count == 3 for a in assets: updated = repo.get(a.id) assert set(updated.tag_ids) == {"new-1", "new-2"} def test_batch_replace_tags_empty_tags_clears_all(self): """替换为空列表:清空所有标签。""" repo = InMemoryAssetRepository() asset = Asset.create( project_id="proj-1", library_id="lib-1", name="v.mp4", storage_key="v.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) asset.add_tag("tag-a") asset.add_tag("tag-b") repo.create(asset) count = repo.batch_replace_tags([asset.id], []) assert count == 1 assert repo.get(asset.id).tag_ids == [] def test_batch_replace_tags_empty_assets(self): repo = InMemoryAssetRepository() assert repo.batch_replace_tags([], ["tag-1"]) == 0 class TestBatchOperationLimits: """批量操作上限与边界测试。""" def test_large_batch_operations(self): """大量素材的批量操作(验证性能基本可用)。""" repo = InMemoryAssetRepository() assets = [] for i in range(50): asset = Asset.create( project_id="proj-1", library_id="lib-1", name=f"v{i}.mp4", storage_key=f"v{i}.mp4", mime_type="video/mp4", status=AssetStatus.READY, ) repo.create(asset) assets.append(asset) ids = [a.id for a in assets] # 批量打标签 count = repo.batch_add_tags(ids, ["bulk-tag"]) assert count == 50 # 批量分类 count = repo.batch_update_metadata(ids, {"category": "scenic"}) assert count == 50 # 批量软删除 count = repo.batch_delete(ids) assert count == 50 for a in assets: assert repo.get(a.id).status == AssetStatus.DELETED