Files
shirobana-bot/tests/test_skill_integration.py
T
RRRosmontis 34089d0042 Initial import of Shirobana bot backend
Include utility modules, shared channels, prompts, read-only skills, wardrobe memory and regression tests. Externalize legacy credentials and exclude private runtime state.
2026-11-11 00:25:30 +08:00

140 lines
6.9 KiB
Python

"""Skill orchestration tests with fake models, isolated skill directories and no sends."""
import json
from pathlib import Path
import sys
import tempfile
from types import SimpleNamespace
import unittest
from unittest.mock import AsyncMock, MagicMock, patch
BASE = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(BASE / 'bot-utility'), str(BASE)]
import skill_registry
import skill_runtime
import help_ui
import notification_render
import notifications
import agent_loop
from chat import engine
class SkillIntegrationTests(unittest.IsolatedAsyncioTestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
base = Path(self.tmp.name)
root = base / 'skills'
self.body = 'SPECIFIC_STYLE_RULE: 用户称呼优先于默认规范。'
for name in ('language', 'result-style'):
folder = root / name
folder.mkdir(parents=True)
(folder / 'SKILL.md').write_text(
f'---\nname: {name}\ndescription: Language style\n---\n' + self.body + name,
encoding='utf-8')
config = base / 'skills_config.yml'
config.write_text(json.dumps({
'enabled': True, 'allowlist': ['language', 'result-style'],
'always': ['language'], 'scenes': {'tool_result': ['result-style']},
}), encoding='utf-8')
self.registry = skill_registry.SkillRegistry(root, config)
self.registry_patch = patch.object(skill_runtime, 'registry', self.registry)
self.registry_patch.start()
def tearDown(self):
self.registry_patch.stop()
self.tmp.cleanup()
async def test_help_keeps_personal_settings_and_unwraps_text(self):
model = AsyncMock(return_value=json.dumps({'text': '主人,可以查邮件。', 'tools': []}))
with patch.object(engine, '_call_deepseek_transport', new=model), \
patch.object(engine, 'user_persona_prompt', return_value='USER_SETTING: 称呼主人'):
result = await help_ui.chat_help(42)
self.assertEqual(result, '主人,可以查邮件。')
prompt = model.await_args.args[0]
self.assertIn(self.body, prompt)
self.assertIn('USER_SETTING: 称呼主人', prompt)
self.assertIn('help', prompt)
self.assertIsNone(skill_runtime.current_turn())
async def test_notification_and_reminder_open_own_scene(self):
scenes = []
async def model(*args, **kwargs):
scenes.append(skill_runtime.current_turn().scene)
return json.dumps({'text': '提醒内容', 'tools': []})
with patch.object(engine, '_call_deepseek_transport', new=model), \
patch.object(engine, 'user_persona_prompt', return_value='USER_SETTING'):
for kind in ('komari', 'reminder'):
event = notifications.Event(kind=kind, dedupe_key='test-' + kind,
uid=42, legacy_text='原文')
self.assertEqual(await notification_render.render_event(event, 'chat', 1), '提醒内容')
self.assertEqual(scenes, ['notification', 'reminder'])
async def test_notification_cannot_execute_business_tool_and_keeps_fallback(self):
response = json.dumps({'text': '不应发送', 'tools': [{'name': 'akaros_mail', 'args': {}}]})
with patch.object(engine, '_call_deepseek_transport', new=AsyncMock(return_value=response)), \
patch.object(engine, 'user_persona_prompt', return_value='base'):
event = notifications.Event(kind='komari', dedupe_key='readonly', uid=42, legacy_text='原始告警')
result = await notification_render.render_event(event, 'chat', 1)
self.assertIn('原始告警', result)
self.assertNotIn('不应发送', result)
async def test_private_and_proactive_sessions_cover_entire_generation(self):
seen = []
async def impl(*args, **kwargs):
seen.append(skill_runtime.current_turn().scene)
with patch.object(engine, '_resolve_delivery', return_value=None), \
patch.object(engine, '_call_model_and_reply_impl', new=impl), \
patch.object(notifications.center, 'begin_conversation'), \
patch.object(notifications.center, 'end_conversation'), \
patch.object(notifications.center, 'deliver_due', new=AsyncMock()):
await engine._call_model_and_reply(None, 101, 'hi', False)
await engine._call_model_and_reply(None, 101, 'hi', True)
self.assertEqual(seen, ['private_chat', 'proactive'])
self.assertIsNone(skill_runtime.current_turn())
async def test_group_has_group_session_not_private_persona(self):
seen = []
async def impl(*args, **kwargs):
seen.append(skill_runtime.current_turn().scene)
with patch.object(engine, '_call_group_model_and_reply_impl', new=impl):
await engine._call_group_model_and_reply(None, 201, 'hi', 42)
self.assertEqual(seen, ['group_chat'])
async def test_agent_followup_binds_result_style_without_repeating_business(self):
final = json.dumps({'message': [{'content': 'done'}], 'tools': []})
model = AsyncMock(return_value=final)
channel = SimpleNamespace(name='tg', is_admin=lambda uid: False)
store = MagicMock()
with skill_runtime.skill_session('private_chat'), \
patch.object(engine, '_call_deepseek_transport', new=model), \
patch.object(engine, 'run_tools', new=AsyncMock(return_value='read result')) as tools, \
patch.object(agent_loop, 'AgentTaskStore', return_value=store), \
patch.object(agent_loop, 'ToolExecutor'):
result = await agent_loop.run_agent_loop(engine,
{'message': [], 'tools': [{'name': 'web_search', 'args': {'query': 'x'}}]},
'system', 'user', channel, 'private', 42, 1)
self.assertEqual(result['tools'], [])
tools.assert_awaited_once()
self.assertIn(self.body + 'result-style', model.await_args.args[0])
async def test_internal_sticker_audit_suspends_skills(self):
image = Path(self.tmp.name) / 'preview.png'
image.write_bytes(b'preview')
model = AsyncMock(return_value='{"send":true}')
with skill_runtime.skill_session('private_chat'), \
patch.object(engine, '_call_deepseek_transport', new=model):
self.assertTrue(await engine._confirm_sticker({'preview_path': str(image)}, 'happy', 'hi'))
self.assertNotIn(self.body, model.await_args.args[0])
def test_usage_logger_contains_counts_not_payload(self):
payload = {'usage': {'prompt_tokens': 100, 'prompt_tokens_details': {'cached_tokens': 80}},
'choices': [{'message': {'content': 'PRIVATE_MESSAGE'}}]}
with patch('builtins.print') as output:
engine._log_model_usage('test', payload)
text = output.call_args.args[0]
self.assertIn('cached_tokens', text)
self.assertNotIn('PRIVATE_MESSAGE', text)
if __name__ == '__main__':
unittest.main()