diff --git a/packages/domain/entities.py b/packages/domain/entities.py index e55b044de..e79404a69 100644 --- a/packages/domain/entities.py +++ b/packages/domain/entities.py @@ -95,6 +95,7 @@ class Asset: storage_key: str mime_type: str metadata: dict[str, Any] = field(default_factory=dict) + tags: list[str] = field(default_factory=list) created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc)) @classmethod @@ -124,8 +125,23 @@ class Asset: storage_key=storage_key.strip(), mime_type=mime_type.strip(), metadata=metadata or {}, + tags=[], ) + def add_tag(self, tag: str) -> None: + """添加标签。空标签会被忽略,自动去重。""" + clean_tag = tag.strip() + if not clean_tag: + raise ValueError("标签不能为空") + if clean_tag not in self.tags: + self.tags.append(clean_tag) + + def remove_tag(self, tag: str) -> None: + """删除标签。如果标签不存在,不报错(幂等性)。""" + clean_tag = tag.strip() + if clean_tag in self.tags: + self.tags.remove(clean_tag) + @dataclass(slots=True) class IngestJob: diff --git a/tests/integration/test_asset_tags.py b/tests/integration/test_asset_tags.py new file mode 100644 index 000000000..5fc4c5871 --- /dev/null +++ b/tests/integration/test_asset_tags.py @@ -0,0 +1,99 @@ +import pytest + +from packages.domain import Asset + + +def test_add_tag_to_asset(): + """测试添加标签到 Asset。""" + asset = Asset.create( + workspace_id="ws-1", + project_id="proj-1", + library_id="lib-1", + name="video.mp4", + storage_key="uploads/abc/video.mp4", + mime_type="video/mp4", + ) + + asset.add_tag("风景") + asset.add_tag("自然") + + assert len(asset.tags) == 2 + assert "风景" in asset.tags + assert "自然" in asset.tags + + +def test_add_duplicate_tag_should_ignore(): + """测试添加重复标签应自动去重。""" + asset = Asset.create( + workspace_id="ws-1", + project_id="proj-1", + library_id="lib-1", + name="video.mp4", + storage_key="uploads/abc/video.mp4", + mime_type="video/mp4", + ) + + asset.add_tag("风景") + asset.add_tag("风景") # 重复 + + assert len(asset.tags) == 1 + assert asset.tags.count("风景") == 1 + + +def test_add_empty_tag_should_fail(): + """测试添加空标签应失败。""" + asset = Asset.create( + workspace_id="ws-1", + project_id="proj-1", + library_id="lib-1", + name="video.mp4", + storage_key="uploads/abc/video.mp4", + mime_type="video/mp4", + ) + + with pytest.raises(ValueError, match="标签不能为空"): + asset.add_tag("") + + with pytest.raises(ValueError, match="标签不能为空"): + asset.add_tag(" ") # 仅空格 + + +def test_remove_tag_from_asset(): + """测试从 Asset 删除标签。""" + asset = Asset.create( + workspace_id="ws-1", + project_id="proj-1", + library_id="lib-1", + name="video.mp4", + storage_key="uploads/abc/video.mp4", + mime_type="video/mp4", + ) + + asset.add_tag("风景") + asset.add_tag("自然") + + asset.remove_tag("风景") + + assert len(asset.tags) == 1 + assert "风景" not in asset.tags + assert "自然" in asset.tags + + +def test_remove_nonexistent_tag_should_be_idempotent(): + """测试删除不存在的标签应幂等(不报错)。""" + asset = Asset.create( + workspace_id="ws-1", + project_id="proj-1", + library_id="lib-1", + name="video.mp4", + storage_key="uploads/abc/video.mp4", + mime_type="video/mp4", + ) + + asset.add_tag("风景") + + # 删除不存在的标签,不应报错 + asset.remove_tag("不存在的标签") + + assert len(asset.tags) == 1 + assert "风景" in asset.tags