mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
- notification 域:渠道能力(MessageChannel→NotificationChannel、ChannelCapability* 迁入 notification.py) - message 域:消息收发(Notification→Message、NotificationType→MessageType、CommingMessage→IncomingMessage、NotificationHistoryItem→MessageHistoryItem、NotificationClear*→MessageClear*) - Agent 工具契约:send_notification_message→send_message、notification_callback→message_callback - 源码不保留旧名物理别名,旧导入经 app/runtime/compat/manifest.py SYMBOL_ALIASES 惰性解析 - API 路径与持久化键冻结不变,前端零改动 - 新增兼容守护测试与 docs/rules/07 命名边界规范
778 lines
30 KiB
Python
778 lines
30 KiB
Python
import io
|
|
import tempfile
|
|
import unittest
|
|
import zipfile
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
from app.testing.bootstrap import ensure_optional_stub
|
|
|
|
ensure_optional_stub("qbittorrentapi", TorrentFilesList=list)
|
|
ensure_optional_stub("transmission_rpc", File=object)
|
|
ensure_optional_stub("psutil")
|
|
ensure_optional_stub("aioshutil")
|
|
ensure_optional_stub("pyquery", PyQuery=object)
|
|
|
|
from app.chain.message import MessageChain
|
|
from app.application.messaging.interaction import InteractionContext
|
|
from app.application.messaging.skill import SkillInteractionHandler
|
|
from app.application.messaging.skill import skill_interaction_manager
|
|
from app.agent.skills.registry import (
|
|
SkillHelper,
|
|
SkillInfo,
|
|
SkillMarketSource,
|
|
settings as skill_settings,
|
|
)
|
|
from app.schemas.types import NotificationChannel
|
|
|
|
|
|
def _build_skill_zip(skill_dir: str, skill_name: str) -> bytes:
|
|
buf = io.BytesIO()
|
|
with zipfile.ZipFile(buf, "w") as zf:
|
|
zf.writestr(
|
|
f"demo-main/{skill_dir}/SKILL.md",
|
|
(
|
|
f"---\n"
|
|
f"name: {skill_name}\n"
|
|
f"version: 1\n"
|
|
f"description: demo skill\n"
|
|
f"---\n\n"
|
|
f"# {skill_name}\n"
|
|
),
|
|
)
|
|
zf.writestr(f"demo-main/{skill_dir}/scripts/example.py", "print('ok')\n")
|
|
return buf.getvalue()
|
|
|
|
|
|
class _FakeResponse:
|
|
def __init__(self, payload=None, content: bytes = b"", status_code: int = 200):
|
|
self._payload = payload
|
|
self.content = content
|
|
self.status_code = status_code
|
|
|
|
def json(self):
|
|
return self._payload
|
|
|
|
|
|
class TestSkillsCommand(unittest.TestCase):
|
|
def tearDown(self):
|
|
skill_interaction_manager.clear()
|
|
|
|
def test_message_routes_text_reply_to_skills_interaction_before_ai(self):
|
|
chain = MessageChain()
|
|
skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Wechat,
|
|
source="wechat-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(chain, "_record_user_message"), patch(
|
|
"app.chain.message.SkillInteractionHandler.handle_text_interaction",
|
|
return_value=True,
|
|
) as handle_text, patch.object(chain, "_handle_ai_message") as handle_ai:
|
|
chain.handle_message(
|
|
channel=NotificationChannel.Wechat,
|
|
source="wechat-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="2",
|
|
)
|
|
|
|
handle_text.assert_called_once()
|
|
handle_ai.assert_not_called()
|
|
|
|
def test_skills_text_exit_skips_notification_history(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(chain._messenger, "post_message") as post_message:
|
|
handled = chain.handle_text_interaction(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="退出",
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
notification = post_message.call_args.args[0]
|
|
self.assertEqual(notification.title, "技能交互已结束")
|
|
self.assertFalse(notification.save_history)
|
|
self.assertIsNone(skill_interaction_manager.get_by_user("10001"))
|
|
|
|
def test_callback_routes_to_skills_chain(self):
|
|
chain = MessageChain()
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch(
|
|
"app.chain.message.SkillInteractionHandler.handle_callback_interaction",
|
|
return_value=True,
|
|
) as handle_callback:
|
|
chain._handle_callback(
|
|
callback_data=f"skills:{request.request_id}:market",
|
|
context=InteractionContext(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
user_id="10001",
|
|
username="tester",
|
|
),
|
|
)
|
|
|
|
handle_callback.assert_called_once()
|
|
|
|
def test_skillhelper_install_and_remove_market_skill(self):
|
|
helper = SkillHelper()
|
|
skill = SkillInfo(
|
|
id="demo-skill",
|
|
name="demo-skill",
|
|
description="demo",
|
|
source_type="market",
|
|
source_label="市场 · acme/demo",
|
|
repo_url="https://github.com/acme/demo",
|
|
repo_name="acme/demo",
|
|
skill_path="skills/demo-skill",
|
|
)
|
|
zip_bytes = _build_skill_zip("skills/demo-skill", "demo-skill")
|
|
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
user_root = Path(tempdir) / "user-skills"
|
|
bundled_root = Path(tempdir) / "bundled-skills"
|
|
user_root.mkdir(parents=True, exist_ok=True)
|
|
bundled_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
with patch.object(
|
|
SkillHelper, "get_user_skills_dir", return_value=user_root
|
|
), patch.object(
|
|
SkillHelper, "get_bundled_skills_dir", return_value=bundled_root
|
|
), patch.object(
|
|
helper, "_download_repo_archive", return_value=zip_bytes
|
|
):
|
|
success, message = helper.install_market_skill(skill)
|
|
self.assertTrue(success, message)
|
|
self.assertTrue((user_root / "demo-skill" / "SKILL.md").exists())
|
|
self.assertTrue(
|
|
(user_root / "demo-skill" / ".moviepilot-skill-source.json").exists()
|
|
)
|
|
|
|
local_skills = helper.list_local_skills()
|
|
self.assertEqual(len(local_skills), 1)
|
|
self.assertEqual(local_skills[0].source_type, "market")
|
|
self.assertTrue(local_skills[0].removable)
|
|
|
|
removed, remove_message = helper.remove_local_skill("demo-skill")
|
|
self.assertTrue(removed, remove_message)
|
|
self.assertFalse((user_root / "demo-skill").exists())
|
|
|
|
bundled_skill_dir = bundled_root / "builtin-skill"
|
|
bundled_skill_dir.mkdir(parents=True, exist_ok=True)
|
|
(bundled_skill_dir / "SKILL.md").write_text(
|
|
"---\nname: builtin-skill\ndescription: builtin\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
installed_builtin = user_root / "builtin-skill"
|
|
installed_builtin.mkdir(parents=True, exist_ok=True)
|
|
(installed_builtin / "SKILL.md").write_text(
|
|
"---\nname: builtin-skill\ndescription: builtin\n---\n",
|
|
encoding="utf-8",
|
|
)
|
|
|
|
removed, remove_message = helper.remove_local_skill("builtin-skill")
|
|
self.assertFalse(removed)
|
|
self.assertIn("内置技能", remove_message)
|
|
|
|
def test_skillhelper_lists_clawhub_registry_skills(self):
|
|
helper = SkillHelper()
|
|
response = _FakeResponse(
|
|
payload={
|
|
"status": "success",
|
|
"value": {
|
|
"hasMore": False,
|
|
"nextCursor": None,
|
|
"page": [
|
|
{
|
|
"ownerHandle": "openclaw",
|
|
"skill": {
|
|
"slug": "weather-forecast",
|
|
"displayName": "Weather Forecast",
|
|
"summary": "Forecast weather from ClawHub",
|
|
},
|
|
}
|
|
],
|
|
},
|
|
}
|
|
)
|
|
|
|
with patch.object(
|
|
helper,
|
|
"_discover_clawhub_runtime_env",
|
|
return_value={"convex_url": "https://wry-manatee-359.convex.cloud"},
|
|
), patch.object(helper, "_request_convex_query", return_value=response):
|
|
skills = helper._list_market_source_skills("https://clawhub.ai")
|
|
|
|
self.assertEqual(len(skills), 1)
|
|
self.assertEqual(skills[0].id, "weather-forecast")
|
|
self.assertEqual(skills[0].name, "Weather Forecast")
|
|
self.assertEqual(skills[0].source_type, "registry")
|
|
self.assertEqual(skills[0].registry_name, "ClawHub")
|
|
self.assertEqual(skills[0].source_label, "社区注册表 · ClawHub")
|
|
self.assertIn("/openclaw/weather-forecast", skills[0].path)
|
|
|
|
def test_skillhelper_filters_market_skills_by_query(self):
|
|
helper = SkillHelper()
|
|
skills = [
|
|
SkillInfo(
|
|
id="weather-forecast",
|
|
name="Weather Forecast",
|
|
description="Forecast weather from ClawHub",
|
|
source_label="社区注册表 · ClawHub",
|
|
),
|
|
SkillInfo(
|
|
id="github-tools",
|
|
name="GitHub Tools",
|
|
description="Manage pull requests",
|
|
source_label="官方仓库 · openai/skills",
|
|
),
|
|
]
|
|
|
|
filtered = helper.filter_market_skills(skills=skills, query="weather clawhub")
|
|
|
|
self.assertEqual(len(filtered), 1)
|
|
self.assertEqual(filtered[0].id, "weather-forecast")
|
|
|
|
def test_skillhelper_falls_back_to_rest_registry_listing_when_runtime_missing(self):
|
|
helper = SkillHelper()
|
|
response = _FakeResponse(
|
|
payload={
|
|
"items": [
|
|
{
|
|
"slug": "weather-forecast",
|
|
"name": "Weather Forecast",
|
|
"summary": "Forecast weather from ClawHub",
|
|
"owner": {"handle": "openclaw"},
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
with patch.object(
|
|
helper, "_discover_clawhub_runtime_env", return_value=None
|
|
), patch.object(helper, "_request_registry", return_value=response):
|
|
skills = helper._list_market_source_skills("https://clawhub.ai")
|
|
|
|
self.assertEqual(len(skills), 1)
|
|
self.assertEqual(skills[0].id, "weather-forecast")
|
|
self.assertEqual(skills[0].source_type, "registry")
|
|
self.assertEqual(skills[0].registry_name, "ClawHub")
|
|
self.assertEqual(skills[0].source_label, "社区注册表 · ClawHub")
|
|
self.assertIn("/openclaw/weather-forecast", skills[0].path)
|
|
|
|
def test_skillhelper_installs_registry_skill(self):
|
|
helper = SkillHelper()
|
|
skill = SkillInfo(
|
|
id="registry-demo",
|
|
name="Registry Demo",
|
|
description="registry demo",
|
|
source_type="registry",
|
|
source_label="注册表 · ClawHub",
|
|
registry_url="https://clawhub.ai",
|
|
registry_name="ClawHub",
|
|
registry_slug="registry-demo",
|
|
download_url="https://clawhub.ai/api/v1/download?slug=registry-demo",
|
|
)
|
|
zip_bytes = _build_skill_zip("package", "registry-demo")
|
|
|
|
with tempfile.TemporaryDirectory() as tempdir:
|
|
user_root = Path(tempdir) / "user-skills"
|
|
bundled_root = Path(tempdir) / "bundled-skills"
|
|
user_root.mkdir(parents=True, exist_ok=True)
|
|
bundled_root.mkdir(parents=True, exist_ok=True)
|
|
|
|
with patch.object(
|
|
SkillHelper, "get_user_skills_dir", return_value=user_root
|
|
), patch.object(
|
|
SkillHelper, "get_bundled_skills_dir", return_value=bundled_root
|
|
), patch.object(
|
|
helper, "_request_registry", return_value=_FakeResponse(content=zip_bytes)
|
|
):
|
|
success, message = helper.install_market_skill(skill)
|
|
self.assertTrue(success, message)
|
|
self.assertTrue((user_root / "registry-demo" / "SKILL.md").exists())
|
|
self.assertTrue(
|
|
(
|
|
user_root
|
|
/ "registry-demo"
|
|
/ ".moviepilot-skill-source.json"
|
|
).exists()
|
|
)
|
|
|
|
local_skills = helper.list_local_skills()
|
|
self.assertEqual(len(local_skills), 1)
|
|
self.assertEqual(local_skills[0].source_type, "registry")
|
|
self.assertEqual(local_skills[0].registry_name, "ClawHub")
|
|
self.assertEqual(local_skills[0].source_label, "社区注册表 · ClawHub")
|
|
|
|
def test_skillhelper_lists_market_sources_and_marks_custom_entries(self):
|
|
helper = SkillHelper()
|
|
|
|
with patch.object(
|
|
helper,
|
|
"get_market_sources",
|
|
return_value=[
|
|
"https://clawhub.ai",
|
|
"https://github.com/openai/skills",
|
|
"https://github.com/acme/custom-skills",
|
|
],
|
|
), patch.object(
|
|
helper,
|
|
"get_default_market_sources",
|
|
return_value=[
|
|
"https://clawhub.ai",
|
|
"https://github.com/openai/skills",
|
|
],
|
|
):
|
|
sources = helper.list_market_source_entries()
|
|
|
|
self.assertEqual(len(sources), 3)
|
|
self.assertTrue(sources[0].builtin)
|
|
self.assertTrue(sources[1].builtin)
|
|
self.assertFalse(sources[2].builtin)
|
|
self.assertTrue(sources[2].removable)
|
|
self.assertEqual(sources[2].label, "仓库来源 · acme/custom-skills")
|
|
|
|
def test_skillhelper_add_custom_market_source_updates_setting(self):
|
|
helper = SkillHelper()
|
|
|
|
with patch.object(
|
|
helper,
|
|
"get_market_sources",
|
|
return_value=["https://github.com/openai/skills"],
|
|
), patch.object(
|
|
type(skill_settings),
|
|
"update_setting",
|
|
return_value=(True, ""),
|
|
) as update_setting:
|
|
success, message = helper.add_custom_market_source("acme/custom-skills")
|
|
|
|
self.assertTrue(success)
|
|
self.assertIn("acme/custom-skills", message)
|
|
update_setting.assert_called_once_with(
|
|
key="SKILL_MARKET",
|
|
value="https://github.com/openai/skills,https://github.com/acme/custom-skills",
|
|
)
|
|
|
|
def test_skillhelper_remove_custom_market_source_blocks_builtin(self):
|
|
helper = SkillHelper()
|
|
|
|
with patch.object(
|
|
helper,
|
|
"get_default_market_sources",
|
|
return_value=["https://github.com/openai/skills"],
|
|
):
|
|
success, message = helper.remove_custom_market_source(
|
|
"https://github.com/openai/skills"
|
|
)
|
|
|
|
self.assertFalse(success)
|
|
self.assertIn("内置默认源", message)
|
|
|
|
def test_skills_chain_market_view_marks_clawhub_as_community_source(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
request.view = "market"
|
|
|
|
with patch.object(
|
|
chain.skillhelper,
|
|
"list_market_skills",
|
|
return_value=[
|
|
SkillInfo(
|
|
id="weather-forecast",
|
|
name="Weather Forecast",
|
|
description="Forecast weather from ClawHub",
|
|
source_type="registry",
|
|
source_label="社区注册表 · ClawHub",
|
|
registry_name="ClawHub",
|
|
registry_url="https://clawhub.ai",
|
|
registry_slug="weather-forecast",
|
|
)
|
|
],
|
|
):
|
|
title, text, _buttons = chain._build_market_view(request=request)
|
|
|
|
self.assertEqual(title, "技能市场")
|
|
self.assertIn("社区注册表 · ClawHub", text)
|
|
self.assertIn("社区源,安装前请自行甄别安全性", text)
|
|
self.assertIn("ClawHub 属于社区注册表", text)
|
|
|
|
def test_skills_chain_market_view_filters_by_search_query(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
request.view = "market"
|
|
request.market_query = "weather"
|
|
|
|
with patch.object(
|
|
chain.skillhelper,
|
|
"list_market_skills",
|
|
return_value=[
|
|
SkillInfo(
|
|
id="weather-forecast",
|
|
name="Weather Forecast",
|
|
description="Forecast weather from ClawHub",
|
|
source_type="registry",
|
|
source_label="社区注册表 · ClawHub",
|
|
registry_name="ClawHub",
|
|
registry_url="https://clawhub.ai",
|
|
registry_slug="weather-forecast",
|
|
),
|
|
SkillInfo(
|
|
id="github-tools",
|
|
name="GitHub Tools",
|
|
description="Manage pull requests",
|
|
source_type="market",
|
|
source_label="官方仓库 · openai/skills",
|
|
repo_name="openai/skills",
|
|
),
|
|
],
|
|
):
|
|
title, text, buttons = chain._build_market_view(request=request)
|
|
|
|
self.assertEqual(title, "技能市场")
|
|
self.assertIn("当前搜索:weather", text)
|
|
self.assertIn("weather-forecast", text)
|
|
self.assertNotIn("github-tools", text)
|
|
self.assertTrue(buttons)
|
|
self.assertEqual(buttons[0][0]["callback_data"], f"skills:{request.request_id}:clear-search")
|
|
|
|
def test_skills_chain_root_view_uses_friendly_source_labels(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(chain.skillhelper, "list_local_skills", return_value=[]), patch.object(
|
|
chain.skillhelper, "list_market_skills", return_value=[]
|
|
), patch.object(
|
|
chain.skillhelper,
|
|
"list_market_source_entries",
|
|
return_value=[
|
|
SkillMarketSource(
|
|
source="https://clawhub.ai",
|
|
label="社区注册表 · ClawHub",
|
|
builtin=True,
|
|
removable=False,
|
|
),
|
|
SkillMarketSource(
|
|
source="https://github.com/openai/skills",
|
|
label="官方仓库 · openai/skills",
|
|
builtin=True,
|
|
removable=False,
|
|
),
|
|
SkillMarketSource(
|
|
source="https://github.com/acme/custom-skills",
|
|
label="仓库来源 · acme/custom-skills",
|
|
builtin=False,
|
|
removable=True,
|
|
),
|
|
],
|
|
):
|
|
title, text, _buttons = chain._build_root_view(request=request)
|
|
|
|
self.assertEqual(title, "技能管理")
|
|
self.assertIn("社区注册表 · ClawHub", text)
|
|
self.assertIn("官方仓库 · openai/skills", text)
|
|
self.assertIn("仓库来源 · acme/custom-skills", text)
|
|
self.assertIn("3. 管理技能源", text)
|
|
|
|
def test_skills_chain_installed_view_builds_remove_buttons(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.WebAgent,
|
|
source="web-agent",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(
|
|
chain.skillhelper,
|
|
"list_local_skills",
|
|
return_value=[
|
|
SkillInfo(
|
|
id="builtin",
|
|
name="Builtin",
|
|
description="Built in skill",
|
|
source_type="builtin",
|
|
source_label="内置",
|
|
removable=False,
|
|
),
|
|
SkillInfo(
|
|
id="custom",
|
|
name="Custom",
|
|
description="Custom skill",
|
|
source_type="local",
|
|
source_label="本地",
|
|
removable=True,
|
|
),
|
|
],
|
|
):
|
|
title, text, buttons = chain._build_installed_view(request=request)
|
|
|
|
self.assertEqual(title, "已安装技能")
|
|
self.assertIn("builtin", text)
|
|
self.assertIn("custom", text)
|
|
self.assertTrue(buttons)
|
|
self.assertIn(
|
|
{"text": "删除 2", "callback_data": f"skills:{request.request_id}:remove:2"},
|
|
[button for row in buttons for button in row],
|
|
)
|
|
|
|
def test_skills_chain_callback_enters_search_input_mode(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(chain, "_render_interaction") as render:
|
|
handled = chain.handle_callback_interaction(
|
|
callback_data=f"skills:{request.request_id}:search",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
self.assertEqual(request.view, "market")
|
|
self.assertEqual(request.awaiting_input, "market-search")
|
|
render.assert_called_once()
|
|
|
|
def test_skills_chain_text_search_updates_market_query(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
request.view = "market"
|
|
|
|
with patch.object(chain, "_render_interaction") as render:
|
|
handled = chain.handle_text_interaction(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="搜索 weather",
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
self.assertEqual(request.market_query, "weather")
|
|
self.assertEqual(request.market_page, 0)
|
|
self.assertIsNone(request.awaiting_input)
|
|
render.assert_called_once()
|
|
|
|
def test_skills_chain_followup_text_applies_search_when_awaiting_input(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
request.view = "market"
|
|
request.awaiting_input = "market-search"
|
|
|
|
with patch.object(chain, "_render_interaction") as render:
|
|
handled = chain.handle_text_interaction(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="calendar",
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
self.assertEqual(request.market_query, "calendar")
|
|
self.assertIsNone(request.awaiting_input)
|
|
render.assert_called_once()
|
|
|
|
def test_skills_chain_callback_enters_source_add_mode(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(chain, "_render_interaction") as render:
|
|
handled = chain.handle_callback_interaction(
|
|
callback_data=f"skills:{request.request_id}:source-add",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
self.assertEqual(request.view, "sources")
|
|
self.assertEqual(request.awaiting_input, "source-add")
|
|
render.assert_called_once()
|
|
|
|
def test_skills_chain_followup_text_adds_custom_market_source(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
request.view = "sources"
|
|
request.awaiting_input = "source-add"
|
|
|
|
with patch.object(
|
|
chain.skillhelper,
|
|
"add_custom_market_source",
|
|
return_value=(True, "已添加技能源:仓库来源 · acme/custom-skills"),
|
|
) as add_source, patch.object(chain, "_render_interaction") as render, patch.object(
|
|
chain._messenger, "post_message"
|
|
) as post_message:
|
|
handled = chain.handle_text_interaction(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="acme/custom-skills",
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
self.assertIsNone(request.awaiting_input)
|
|
add_source.assert_called_once_with("acme/custom-skills")
|
|
post_message.assert_called_once()
|
|
render.assert_called_once()
|
|
|
|
def test_skills_chain_text_removes_custom_market_source_by_index(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
|
|
with patch.object(
|
|
chain,
|
|
"_remove_market_source",
|
|
return_value=(True, "已删除技能源:仓库来源 · acme/custom-skills"),
|
|
) as remove_source, patch.object(chain, "_render_interaction") as render, patch.object(
|
|
chain._messenger, "post_message"
|
|
) as post_message:
|
|
handled = chain.handle_text_interaction(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
text="删除源 3",
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
self.assertEqual(request.view, "sources")
|
|
remove_source.assert_called_once_with(page_index=3)
|
|
post_message.assert_called_once()
|
|
render.assert_called_once()
|
|
|
|
def test_skills_chain_source_view_lists_custom_sources(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
request = skill_interaction_manager.create_or_replace(
|
|
user_id="10001",
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
username="tester",
|
|
)
|
|
request.view = "sources"
|
|
|
|
with patch.object(
|
|
chain.skillhelper,
|
|
"list_market_source_entries",
|
|
return_value=[
|
|
SkillMarketSource(
|
|
source="https://clawhub.ai",
|
|
label="社区注册表 · ClawHub",
|
|
builtin=True,
|
|
removable=False,
|
|
),
|
|
SkillMarketSource(
|
|
source="https://github.com/acme/custom-skills",
|
|
label="仓库来源 · acme/custom-skills",
|
|
builtin=False,
|
|
removable=True,
|
|
),
|
|
],
|
|
):
|
|
title, text, buttons = chain._build_sources_view(request=request)
|
|
|
|
self.assertEqual(title, "技能源管理")
|
|
self.assertIn("社区注册表 · ClawHub", text)
|
|
self.assertIn("仓库来源 · acme/custom-skills", text)
|
|
self.assertIn("删除自定义源", text)
|
|
self.assertTrue(buttons)
|
|
self.assertEqual(
|
|
buttons[1][0]["callback_data"],
|
|
f"skills:{request.request_id}:source-remove:2",
|
|
)
|
|
|
|
def test_skills_chain_updates_buttons_via_edit_message(self):
|
|
chain = SkillInteractionHandler(messenger=MessageChain())
|
|
buttons = [[{"text": "安装 1", "callback_data": "skills:req:install:1"}]]
|
|
|
|
with patch.object(chain._messenger, "edit_message", return_value=True) as edit_message, patch.object(
|
|
chain._messenger, "post_message"
|
|
) as post_message:
|
|
chain._update_or_post_message(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
userid="10001",
|
|
username="tester",
|
|
title="技能市场",
|
|
text="请选择技能",
|
|
buttons=buttons,
|
|
original_message_id=123,
|
|
original_chat_id="456",
|
|
)
|
|
|
|
edit_message.assert_called_once_with(
|
|
channel=NotificationChannel.Telegram,
|
|
source="telegram-test",
|
|
message_id=123,
|
|
chat_id="456",
|
|
title="技能市场",
|
|
text="请选择技能",
|
|
buttons=buttons,
|
|
)
|
|
post_message.assert_not_called()
|