test: wave217 title_library use cases +29单测(8个UC含智能选标题) #1189

Merged
xiaoxia merged 1 commits from test/wave217-title-library-use-cases into develop 2026-07-30 08:45:19 +08:00
+342 -279
View File
@@ -1,4 +1,4 @@
"""标题库 UseCase 单元测试."""
"""标题库 Use Cases 单元测试 — wave217"""
from __future__ import annotations
@@ -24,384 +24,447 @@ from packages.application.title_library.use_cases import (
from packages.domain.exceptions import NotFoundError, QuotaExceededError
from packages.domain.title_library import TitleLibraryItem
# ── helpers ──────────────────────────────────────────────────────────────────
def _make_item(id: str, name: str, text: str, usage_count: int = 0, category: str = "default") -> TitleLibraryItem:
def _make_item(
item_id="t1",
user_id="u1",
name="标题A",
text="这是一个标题",
category="default",
usage_count=0,
is_active=True,
description="",
tags=None,
metadata_=None,
):
return TitleLibraryItem(
id=id,
user_id="user_1",
id=item_id,
user_id=user_id,
name=name,
text=text,
category=category,
description="",
tags=[],
description=description,
tags=tags or [],
usage_count=usage_count,
is_active=True,
metadata_={},
is_active=is_active,
metadata_=metadata_ or {},
)
@pytest.fixture
def mock_repo():
return MagicMock()
@pytest.fixture
def sample_item():
return _make_item("title_1", "爆款标题", "这是一个爆款标题文案", usage_count=5)
# ── ListTitleLibraryUseCase ─────────────────────────────────────────────────
class TestListTitleLibraryUseCase:
"""ListTitleLibraryUseCase 测试"""
def test_list_default_params(self):
items = [_make_item("t1"), _make_item("t2")]
repo = MagicMock()
repo.list_by_user.return_value = items
def test_list_returns_results(self, mock_repo, sample_item):
"""正常返回标题列表"""
mock_repo.list_by_user.return_value = [sample_item]
use_case = ListTitleLibraryUseCase(mock_repo)
uc = ListTitleLibraryUseCase(repo)
result = uc.execute("u1")
result = use_case.execute("user_1")
assert len(result) == 2
repo.list_by_user.assert_called_once_with("u1", category=None, skip=0, limit=50)
assert len(result) == 1
assert result[0].id == "title_1"
mock_repo.list_by_user.assert_called_once_with("user_1", category=None, skip=0, limit=50)
def test_list_with_category(self):
repo = MagicMock()
repo.list_by_user.return_value = []
def test_list_with_category(self, mock_repo, sample_item):
"""按分类过滤"""
mock_repo.list_by_user.return_value = [sample_item]
use_case = ListTitleLibraryUseCase(mock_repo)
use_case.execute("user_1", category="电商")
mock_repo.list_by_user.assert_called_once_with("user_1", category="电商", skip=0, limit=50)
def test_list_with_pagination(self, mock_repo, sample_item):
"""带分页参数"""
mock_repo.list_by_user.return_value = [sample_item]
use_case = ListTitleLibraryUseCase(mock_repo)
use_case.execute("user_1", skip=10, limit=20)
mock_repo.list_by_user.assert_called_once_with("user_1", category=None, skip=10, limit=20)
def test_empty_list(self, mock_repo):
"""空列表"""
mock_repo.list_by_user.return_value = []
use_case = ListTitleLibraryUseCase(mock_repo)
result = use_case.execute("user_1")
uc = ListTitleLibraryUseCase(repo)
result = uc.execute("u1", category="marketing")
repo.list_by_user.assert_called_once_with("u1", category="marketing", skip=0, limit=50)
assert result == []
def test_list_pagination(self):
repo = MagicMock()
repo.list_by_user.return_value = []
uc = ListTitleLibraryUseCase(repo)
uc.execute("u1", skip=10, limit=20)
repo.list_by_user.assert_called_once_with("u1", category=None, skip=10, limit=20)
# ── GetTitleLibraryUseCase ──────────────────────────────────────────────────
class TestGetTitleLibraryUseCase:
"""GetTitleLibraryUseCase 测试"""
def test_get_found(self):
item = _make_item()
repo = MagicMock()
repo.get.return_value = item
def test_get_existing(self, mock_repo, sample_item):
"""获取存在的标题"""
mock_repo.get.return_value = sample_item
use_case = GetTitleLibraryUseCase(mock_repo)
uc = GetTitleLibraryUseCase(repo)
result = uc.execute("t1", "u1")
assert result.id == "t1"
repo.get.assert_called_once_with("t1", "u1")
result = use_case.execute("title_1", "user_1")
assert result is not None
assert result.id == "title_1"
mock_repo.get.assert_called_once_with("title_1", "user_1")
def test_get_nonexistent_returns_none(self, mock_repo):
"""获取不存在的标题返回 None"""
mock_repo.get.return_value = None
use_case = GetTitleLibraryUseCase(mock_repo)
result = use_case.execute("nonexistent", "user_1")
def test_get_not_found(self):
repo = MagicMock()
repo.get.return_value = None
uc = GetTitleLibraryUseCase(repo)
result = uc.execute("t999", "u1")
assert result is None
# ── CreateTitleLibraryUseCase ───────────────────────────────────────────────
class TestCreateTitleLibraryUseCase:
"""CreateTitleLibraryUseCase 测试"""
def test_create_success_free_plan_within_quota(self):
repo = MagicMock()
repo.count_by_user.return_value = 0 # 已用数量
repo.create.side_effect = lambda x: x
def test_create_success(self, mock_repo, sample_item):
"""创建成功"""
mock_repo.count_by_user.return_value = 0
mock_repo.create.return_value = sample_item
use_case = CreateTitleLibraryUseCase(mock_repo)
uc = CreateTitleLibraryUseCase(repo)
cmd = CreateTitleLibraryCommand(user_id="u1", name="好标题", text="这是一个好标题的内容")
command = CreateTitleLibraryCommand(
user_id="user_1",
name="新标题",
text="新标题文案",
category="default",
description="",
tags=[],
metadata_={},
with patch("packages.application.title_library.use_cases.quota_checker") as mock_qc:
mock_result = MagicMock()
mock_result.allowed = True
mock_result.limit = 10
mock_result.used = 0
mock_qc.check.return_value = mock_result
result = uc.execute(cmd, plan_name="free")
assert result.name == "好标题"
assert result.user_id == "u1"
repo.create.assert_called_once()
def test_create_quota_exceeded_raises(self):
repo = MagicMock()
repo.count_by_user.return_value = 100
uc = CreateTitleLibraryUseCase(repo)
cmd = CreateTitleLibraryCommand(user_id="u1", name="超了", text="配额超限了")
with patch("packages.application.title_library.use_cases.quota_checker") as mock_qc:
mock_result = MagicMock()
mock_result.allowed = False
mock_result.limit = 5
mock_result.used = 5
mock_qc.check.return_value = mock_result
with pytest.raises(QuotaExceededError):
uc.execute(cmd, plan_name="free")
def test_create_with_tags_and_metadata(self):
repo = MagicMock()
repo.count_by_user.return_value = 0
repo.create.side_effect = lambda x: x
uc = CreateTitleLibraryUseCase(repo)
cmd = CreateTitleLibraryCommand(
user_id="u1",
name="标题",
text="内容",
category="marketing",
description="描述",
tags=["tag1", "tag2"],
metadata_={"key": "value"},
)
result = use_case.execute(command, plan_name="free")
assert result.id == "title_1"
mock_repo.count_by_user.assert_called_once_with("user_1")
mock_repo.create.assert_called_once()
with patch("packages.application.title_library.use_cases.quota_checker") as mock_qc:
mock_result = MagicMock()
mock_result.allowed = True
mock_result.limit = 100
mock_result.used = 0
mock_qc.check.return_value = mock_result
def test_create_quota_exceeded(self, mock_repo):
"""超过配额时抛出 QuotaExceededError"""
mock_repo.count_by_user.return_value = 9999
use_case = CreateTitleLibraryUseCase(mock_repo)
result = uc.execute(cmd)
command = CreateTitleLibraryCommand(
user_id="user_1",
name="新标题",
text="文案",
category="default",
description="",
tags=[],
metadata_={},
)
with pytest.raises(QuotaExceededError):
use_case.execute(command, plan_name="free")
assert result.category == "marketing"
assert result.description == "描述"
assert result.tags == ["tag1", "tag2"]
assert result.metadata_ == {"key": "value"}
mock_repo.create.assert_not_called()
def test_create_with_tags_and_metadata(self, mock_repo, sample_item):
"""创建时带 tags 和 metadata_"""
mock_repo.count_by_user.return_value = 0
mock_repo.create.return_value = sample_item
use_case = CreateTitleLibraryUseCase(mock_repo)
command = CreateTitleLibraryCommand(
user_id="user_1",
name="带标签标题",
text="文案",
category="电商",
description="测试描述",
tags=["爆款", "促销"],
metadata_={"source": "manual"},
)
use_case.execute(command, plan_name="premium")
created = mock_repo.create.call_args[0][0]
assert isinstance(created, TitleLibraryItem)
assert created.name == "带标签标题"
assert created.category == "电商"
assert created.tags == ["爆款", "促销"]
assert created.metadata_ == {"source": "manual"}
# ── UpdateTitleLibraryUseCase ───────────────────────────────────────────────
class TestUpdateTitleLibraryUseCase:
"""UpdateTitleLibraryUseCase 测试"""
def test_update_name(self):
existing = _make_item(name="old")
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
def test_update_name(self, mock_repo, sample_item):
"""更新标题名称"""
mock_repo.get.return_value = sample_item
mock_repo.update.side_effect = lambda x: x
use_case = UpdateTitleLibraryUseCase(mock_repo)
uc = UpdateTitleLibraryUseCase(repo)
cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", name="new")
result = uc.execute(cmd)
command = UpdateTitleLibraryCommand(title_id="title_1", user_id="user_1", name="新名称")
result = use_case.execute(command)
assert result.name == "new"
repo.update.assert_called_once()
assert result.name == "新名称"
# 其他字段不变
assert result.text == "这是一个爆款标题文案"
mock_repo.get.assert_called_once_with("title_1", "user_1")
mock_repo.update.assert_called_once()
def test_update_text(self):
existing = _make_item(text="old")
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
def test_update_multiple_fields(self, mock_repo, sample_item):
"""同时更新多个字段"""
mock_repo.get.return_value = sample_item
mock_repo.update.side_effect = lambda x: x
use_case = UpdateTitleLibraryUseCase(mock_repo)
uc = UpdateTitleLibraryUseCase(repo)
cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", text="new text")
result = uc.execute(cmd)
assert result.text == "new text"
command = UpdateTitleLibraryCommand(
title_id="title_1",
user_id="user_1",
text="新文案内容",
category="美食",
is_active=False,
)
result = use_case.execute(command)
def test_update_category(self):
existing = _make_item(category="old")
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
assert result.text == "新文案内容"
assert result.category == "美食"
uc = UpdateTitleLibraryUseCase(repo)
cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", category="new_cat")
result = uc.execute(cmd)
assert result.category == "new_cat"
def test_update_tags(self):
existing = _make_item(tags=["old"])
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
uc = UpdateTitleLibraryUseCase(repo)
cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", tags=["a", "b"])
result = uc.execute(cmd)
assert result.tags == ["a", "b"]
def test_update_is_active(self):
existing = _make_item(is_active=True)
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
uc = UpdateTitleLibraryUseCase(repo)
cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1", is_active=False)
result = uc.execute(cmd)
assert result.is_active is False
def test_update_nonexistent_raises(self, mock_repo):
"""更新不存在的标题抛出 NotFoundError"""
mock_repo.get.return_value = None
use_case = UpdateTitleLibraryUseCase(mock_repo)
def test_update_not_found_raises(self):
repo = MagicMock()
repo.get.return_value = None
command = UpdateTitleLibraryCommand(title_id="noexist", user_id="user_1", name="新名称")
with pytest.raises(NotFoundError, match="not found"):
use_case.execute(command)
uc = UpdateTitleLibraryUseCase(repo)
cmd = UpdateTitleLibraryCommand(title_id="t999", user_id="u1", name="x")
with pytest.raises(NotFoundError):
uc.execute(cmd)
mock_repo.update.assert_not_called()
def test_update_none_fields_not_modified(self):
existing = _make_item(name="keep", category="keep_cat", description="keep_desc")
repo = MagicMock()
repo.get.return_value = existing
repo.update.return_value = existing
uc = UpdateTitleLibraryUseCase(repo)
cmd = UpdateTitleLibraryCommand(title_id="t1", user_id="u1") # 全None
result = uc.execute(cmd)
assert result.name == "keep"
assert result.category == "keep_cat"
assert result.description == "keep_desc"
# ── DeleteTitleLibraryUseCase ───────────────────────────────────────────────
class TestDeleteTitleLibraryUseCase:
"""DeleteTitleLibraryUseCase 测试"""
def test_delete_success(self, mock_repo):
"""删除成功"""
mock_repo.delete.return_value = True
use_case = DeleteTitleLibraryUseCase(mock_repo)
result = use_case.execute("title_1", "user_1")
def test_delete_success(self):
repo = MagicMock()
repo.delete.return_value = True
uc = DeleteTitleLibraryUseCase(repo)
result = uc.execute("t1", "u1")
assert result is True
mock_repo.delete.assert_called_once_with("title_1", "user_1")
repo.delete.assert_called_once_with("t1", "u1")
def test_delete_nonexistent_returns_false(self, mock_repo):
"""删除不存在的返回 False"""
mock_repo.delete.return_value = False
use_case = DeleteTitleLibraryUseCase(mock_repo)
result = use_case.execute("noexist", "user_1")
def test_delete_not_found(self):
repo = MagicMock()
repo.delete.return_value = False
uc = DeleteTitleLibraryUseCase(repo)
result = uc.execute("t999", "u1")
assert result is False
# ── IncrementTitleUsageUseCase ──────────────────────────────────────────────
class TestIncrementTitleUsageUseCase:
"""IncrementTitleUsageUseCase 测试"""
def test_increment_default_1(self):
repo = MagicMock()
repo.increment_usage_count.return_value = True
def test_increment_positive(self, mock_repo):
"""正增量时调用 repository"""
mock_repo.increment_usage_count.return_value = True
use_case = IncrementTitleUsageUseCase(mock_repo)
command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=1)
result = use_case.execute(command)
uc = IncrementTitleUsageUseCase(repo)
cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1")
result = uc.execute(cmd)
assert result is True
mock_repo.increment_usage_count.assert_called_once_with("title_1", "user_1", increment=1)
repo.increment_usage_count.assert_called_once_with("t1", "u1", increment=1)
def test_increment_zero_returns_false(self, mock_repo):
"""增量为0返回False,不调用repository"""
use_case = IncrementTitleUsageUseCase(mock_repo)
def test_increment_custom_amount(self):
repo = MagicMock()
repo.increment_usage_count.return_value = True
command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=0)
result = use_case.execute(command)
uc = IncrementTitleUsageUseCase(repo)
cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1", increment=5)
result = uc.execute(cmd)
repo.increment_usage_count.assert_called_once_with("t1", "u1", increment=5)
def test_increment_zero_returns_false(self):
repo = MagicMock()
uc = IncrementTitleUsageUseCase(repo)
cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1", increment=0)
result = uc.execute(cmd)
assert result is False
mock_repo.increment_usage_count.assert_not_called()
repo.increment_usage_count.assert_not_called()
def test_increment_negative_returns_false(self, mock_repo):
"""负增量返回False"""
use_case = IncrementTitleUsageUseCase(mock_repo)
def test_increment_negative_returns_false(self):
repo = MagicMock()
command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=-1)
result = use_case.execute(command)
uc = IncrementTitleUsageUseCase(repo)
cmd = IncrementTitleUsageCommand(title_id="t1", user_id="u1", increment=-1)
result = uc.execute(cmd)
assert result is False
mock_repo.increment_usage_count.assert_not_called()
repo.increment_usage_count.assert_not_called()
def test_increment_large_number(self, mock_repo):
"""大增量值"""
mock_repo.increment_usage_count.return_value = True
use_case = IncrementTitleUsageUseCase(mock_repo)
command = IncrementTitleUsageCommand(title_id="title_1", user_id="user_1", increment=10)
use_case.execute(command)
mock_repo.increment_usage_count.assert_called_once_with("title_1", "user_1", increment=10)
# ── PickTitleUseCase ────────────────────────────────────────────────────────
class TestPickTitleUseCase:
"""PickTitleUseCase 智能选标题测试"""
def test_pick_from_multiple_returns_least_used_in_pool(self):
items = [
_make_item("t1", usage_count=10),
_make_item("t2", usage_count=1), # 最少
_make_item("t3", usage_count=5),
_make_item("t4", usage_count=3),
_make_item("t5", usage_count=8),
_make_item("t6", usage_count=2),
]
repo = MagicMock()
repo.list_by_user.return_value = items
def test_pick_from_multiple(self, mock_repo):
"""从多个标题中选一个(最少使用的前5个中随机)"""
items = [_make_item(f"t{i}", f"标题{i}", f"文案{i}", usage_count=i) for i in range(10)]
mock_repo.list_by_user.return_value = items
use_case = PickTitleUseCase(mock_repo)
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1")
command = PickTitleCommand(user_id="user_1")
result = use_case.execute(command)
# 由于有随机性,多次验证都在候选池(最少使用的5个)中
for _ in range(10):
result = uc.execute(cmd)
assert result is not None
# 最少使用的5个是: t2(1), t6(2), t4(3), t3(5), t5(8)
assert result.id in {"t1", "t2", "t3", "t4", "t5", "t6"}
# 选中的一定是使用次数最少的5个之一 (usage_count <= 8)
assert result.usage_count <= 8
assert result is not None
assert isinstance(result, TitleLibraryItem)
# 选出的应该是使用次数最少的前5个之一(0-4)
assert result.usage_count <= 4
mock_repo.list_by_user.assert_called_once()
# 验证查询参数
repo.list_by_user.assert_called()
call_args = repo.list_by_user.call_args
assert call_args[0][0] == "u1"
assert call_args[1]["is_active"] is True
def test_pick_empty_returns_none(self, mock_repo):
"""空标题库返回 None"""
mock_repo.list_by_user.return_value = []
use_case = PickTitleUseCase(mock_repo)
command = PickTitleCommand(user_id="user_1")
result = use_case.execute(command)
def test_pick_empty_returns_none(self):
repo = MagicMock()
repo.list_by_user.return_value = []
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1")
result = uc.execute(cmd)
assert result is None
def test_pick_with_category(self, mock_repo):
"""按分类选标题"""
items = [_make_item("t1", "标题1", "文案1", category="美食")]
mock_repo.list_by_user.return_value = items
use_case = PickTitleUseCase(mock_repo)
def test_pick_single_item(self):
item = _make_item("t1")
repo = MagicMock()
repo.list_by_user.return_value = [item]
command = PickTitleCommand(user_id="user_1", category="美食")
result = use_case.execute(command)
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1")
result = uc.execute(cmd)
assert result.id == "t1"
def test_pick_with_category_filter(self):
repo = MagicMock()
repo.list_by_user.return_value = [_make_item("t1", category="marketing")]
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1", category="marketing")
result = uc.execute(cmd)
assert result is not None
call_kwargs = mock_repo.list_by_user.call_args[1]
assert call_kwargs["category"] == "美食"
assert call_kwargs["is_active"] is True
repo.list_by_user.assert_called_once()
assert repo.list_by_user.call_args[1]["category"] == "marketing"
def test_pick_exclude_ids(self, mock_repo):
"""排除指定ID"""
def test_pick_exclude_ids(self):
items = [
_make_item("t1", "标题1", "文案1", usage_count=1),
_make_item("t2", "标题2", "文案2", usage_count=2),
_make_item("t3", "标题3", "文案3", usage_count=3),
_make_item("t1", usage_count=1),
_make_item("t2", usage_count=2),
_make_item("t3", usage_count=3),
]
mock_repo.list_by_user.return_value = items
use_case = PickTitleUseCase(mock_repo)
repo = MagicMock()
repo.list_by_user.return_value = items
command = PickTitleCommand(user_id="user_1", exclude_ids=["t1", "t2"])
result = use_case.execute(command)
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1", exclude_ids=["t1", "t2"])
# 排除两个后只剩t3
# 排除 t1, t2 后只剩 t3
result = uc.execute(cmd)
assert result.id == "t3"
def test_pick_exclude_all_falls_back(self, mock_repo):
"""排除全部时从所有标题中选"""
def test_pick_exclude_all_fallback_to_all(self):
items = [
_make_item("t1", "标题1", "文案1", usage_count=1),
_make_item("t2", "标题2", "文案2", usage_count=2),
_make_item("t1", usage_count=1),
_make_item("t2", usage_count=2),
]
mock_repo.list_by_user.return_value = items
use_case = PickTitleUseCase(mock_repo)
repo = MagicMock()
repo.list_by_user.return_value = items
command = PickTitleCommand(user_id="user_1", exclude_ids=["t1", "t2"])
result = use_case.execute(command)
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1", exclude_ids=["t1", "t2"])
# 排除全部后fallback到全部,所以还是能选出一个
# 排除后没了,回退到从全部选
result = uc.execute(cmd)
assert result is not None
assert result.id in ("t1", "t2")
assert result.id in {"t1", "t2"}
def test_pick_single_item(self, mock_repo):
"""只有一个标题时选它"""
item = _make_item("only", "唯一标题", "唯一文案", usage_count=10)
mock_repo.list_by_user.return_value = [item]
use_case = PickTitleUseCase(mock_repo)
def test_pick_pool_size_is_5(self):
# 10个标题,使用次数从 1~10
items = [_make_item(f"t{i}", usage_count=i) for i in range(1, 11)]
repo = MagicMock()
repo.list_by_user.return_value = items
command = PickTitleCommand(user_id="user_1")
result = use_case.execute(command)
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1")
assert result.id == "only"
def test_pick_prefers_less_used(self, mock_repo):
"""倾向于选择使用次数少的"""
items = [
_make_item("t_used", "常用", "常用", usage_count=100),
_make_item("t_fresh", "新的", "新的", usage_count=0),
]
mock_repo.list_by_user.return_value = items
use_case = PickTitleUseCase(mock_repo)
# 跑多次,验证使用少的出现在候选池里
results = set()
# 运行多次,确保选中的都在前5个使用最少的里(t1~t5, usage 1~5
for _ in range(20):
command = PickTitleCommand(user_id="user_1")
r = use_case.execute(command)
if r:
results.add(r.id)
result = uc.execute(cmd)
assert int(result.id[1:]) <= 5 # 只从前5个里选
# 两个都在候选池(少于5个),所以都可能被选中
assert "t_used" in results or "t_fresh" in results
def test_pick_fewer_than_pool_size(self):
# 只有3个标题,不足5个池大小
items = [
_make_item("t1", usage_count=3),
_make_item("t2", usage_count=1),
_make_item("t3", usage_count=2),
]
repo = MagicMock()
repo.list_by_user.return_value = items
uc = PickTitleUseCase(repo)
cmd = PickTitleCommand(user_id="u1")
results = set()
for _ in range(30):
result = uc.execute(cmd)
results.add(result.id)
# 3个都有可能被选中(随机性+少量样本,大概率至少出现2个)
assert len(results) >= 1
assert results.issubset({"t1", "t2", "t3"})