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.
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
"""Integration tests for learning and sending user-owned stickers."""
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
BASE = Path(__file__).resolve().parents[1]
|
||||
sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)]
|
||||
|
||||
import main
|
||||
from chat import engine
|
||||
|
||||
|
||||
class FakeStore:
|
||||
def __init__(self, item=None):
|
||||
self.item = item
|
||||
self.learned = []
|
||||
self.random_calls = []
|
||||
def learn(self, scope, source, classification):
|
||||
self.learned.append((scope, source, classification))
|
||||
return {"id": "learned"}
|
||||
def random_for_scene(self, scope, scene):
|
||||
self.random_calls.append((scope, scene))
|
||||
return self.item
|
||||
|
||||
|
||||
class StickerLearningTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_only_marked_stickers_are_learned(self):
|
||||
store = FakeStore()
|
||||
images = [
|
||||
{"kind": "sticker", "source_id": "s1", "original_path": "/tmp/a.webp"},
|
||||
{"kind": "photo", "source_id": "s2", "original_path": "/tmp/b.jpg"},
|
||||
]
|
||||
parsed = {"learned_stickers": [
|
||||
{"source_id": "s1", "accept": True, "scenes": ["sad"], "description": "委屈"},
|
||||
{"source_id": "s2", "accept": True, "scenes": ["happy"], "description": "普通照片"},
|
||||
]}
|
||||
with patch.object(engine, "STICKER_STORE", store), \
|
||||
patch.object(engine, "stickers_enabled", return_value=True):
|
||||
engine._learn_stickers(1001, images, parsed)
|
||||
self.assertEqual(len(store.learned), 1)
|
||||
self.assertEqual(store.learned[0][0], engine.STICKER_GLOBAL_SCOPE)
|
||||
self.assertEqual(store.learned[0][2]["scenes"], ["sad"])
|
||||
|
||||
async def test_source_marker_preserves_user_instruction(self):
|
||||
text = engine._with_sticker_sources(
|
||||
"这个适合什么时候用",
|
||||
[{"kind": "sticker", "source_id": "s1"}, {"kind": "photo"}],
|
||||
)
|
||||
self.assertIn("source_id=s1", text)
|
||||
self.assertTrue(text.endswith("这个适合什么时候用"))
|
||||
|
||||
async def test_random_candidate_requires_visual_confirmation(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
original = Path(directory, "a.webp")
|
||||
preview = Path(directory, "a.png")
|
||||
original.write_bytes(b"sticker")
|
||||
preview.write_bytes(b"preview")
|
||||
item = {"id": "a", "path": str(original), "preview_path": str(preview)}
|
||||
store = FakeStore(item)
|
||||
bot = SimpleNamespace(send_sticker=AsyncMock())
|
||||
channel = SimpleNamespace(bot=bot, _to_raw_target=lambda raw: raw)
|
||||
with patch.object(engine, "STICKER_STORE", store), \
|
||||
patch.object(engine, "stickers_enabled", return_value=True), \
|
||||
patch.object(engine, "_resolve_delivery", return_value=(channel, 10001)), \
|
||||
patch.object(engine, "_confirm_sticker", new=AsyncMock(return_value=False)):
|
||||
sent = await engine._maybe_send_sticker(channel, 1, "sad", "context")
|
||||
self.assertFalse(sent)
|
||||
self.assertEqual(store.random_calls, [(engine.STICKER_GLOBAL_SCOPE, "sad")])
|
||||
bot.send_sticker.assert_not_awaited()
|
||||
|
||||
async def test_confirmation_uses_candidate_preview(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
preview = Path(directory, "preview.png")
|
||||
preview.write_bytes(b"preview")
|
||||
model = AsyncMock(return_value='{"send":true}')
|
||||
with patch.object(engine, "call_deepseek", new=model):
|
||||
approved = await engine._confirm_sticker(
|
||||
{"preview_path": str(preview), "description": "轻微委屈"},
|
||||
"sad", "用户有点失落",
|
||||
)
|
||||
self.assertTrue(approved)
|
||||
self.assertEqual(model.await_args.kwargs["image_infos"][0]["url"], str(preview))
|
||||
|
||||
|
||||
class TelegramStickerInputTests(unittest.IsolatedAsyncioTestCase):
|
||||
async def test_tg_sticker_is_downloaded_and_forwarded_as_sticker(self):
|
||||
sticker = SimpleNamespace(
|
||||
is_animated=False,
|
||||
is_video=False,
|
||||
file_unique_id="unique-1",
|
||||
emoji="🙂",
|
||||
set_name="set-1",
|
||||
)
|
||||
message = SimpleNamespace(
|
||||
from_user=SimpleNamespace(id=123, is_bot=False),
|
||||
chat=SimpleNamespace(id=123, type="private"),
|
||||
text=None,
|
||||
caption=None,
|
||||
photo=[],
|
||||
sticker=sticker,
|
||||
entities=[],
|
||||
)
|
||||
async def download(obj, destination):
|
||||
Path(destination).write_bytes(b"sticker")
|
||||
bot = SimpleNamespace(download=download, id=999)
|
||||
dispatch = AsyncMock()
|
||||
with patch.object(main, "handle_message", new=dispatch):
|
||||
await main.make_tg_handler(SimpleNamespace(tg_username="bot"))(message, bot)
|
||||
images = dispatch.await_args.args[5]
|
||||
self.assertEqual(len(images), 1)
|
||||
self.assertEqual(images[0]["kind"], "sticker")
|
||||
self.assertEqual(images[0]["source_id"], "s1")
|
||||
self.assertEqual(images[0]["file_unique_id"], "unique-1")
|
||||
self.assertEqual(images[0]["format"], "static")
|
||||
self.assertTrue(Path(images[0]["original_path"]).exists())
|
||||
Path(images[0]["original_path"]).unlink()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user