Files
shirobana-bot/tests/test_agent_loop.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

103 lines
3.9 KiB
Python

import asyncio
import unittest
from pathlib import Path
import sys
from unittest.mock import patch
BASE = Path(__file__).resolve().parents[1]
sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)]
import agent_loop
from agent import ToolResult
class Channel:
name = "tg"
def is_admin(self, uid):
return True
class Store:
instances = []
def __init__(self):
self.steps = []
self.finished = None
Store.instances.append(self)
def start(self, uid, conversation):
return "task"
def record(self, *args):
self.steps.append(args)
def finish(self, task, status, error=""):
self.finished = status
class Executor:
calls = []
def __init__(self):
pass
def prompt_schema(self, ctx):
return []
async def execute(self, name, args, ctx):
Executor.calls.append((name, args))
if name == "setu":
return ToolResult(True, "ok", "图片已获取", artifacts=[{"id": "a1"}])
return ToolResult(True, "ok", "邮件已发送", data={"used": args})
class Engine:
def __init__(self, responses):
self.responses = list(responses)
async def call_deepseek(self, *args, **kwargs):
return self.responses.pop(0)
def parse_json_response(self, raw):
return raw
async def run_tools(self, calls):
return "web result"
class AgentLoopTest(unittest.IsolatedAsyncioTestCase):
async def test_two_round_composition_and_final_response(self):
Executor.calls = []
initial = {"message": [], "tools": [{"name": "setu", "args": {"action": "get", "params": {"tags": ["铃兰"]}}}]}
second = {"message": [], "tools": [{"name": "mail", "args": {"action": "send", "params": {"artifact_ids": ["a1"]}}}]}
final = {"message": [{"content": "完成了"}], "tools": []}
engine = Engine([second, final])
with patch.object(agent_loop, "AgentTaskStore", Store), \
patch.object(agent_loop, "ToolExecutor", Executor):
result = await agent_loop.run_agent_loop(
engine, initial, "system", "user", Channel(), "private", 1, 2)
self.assertEqual(result, final)
self.assertEqual([x[0] for x in Executor.calls], ["setu", "mail"])
self.assertEqual(Store.instances[-1].finished, "done")
self.assertEqual(len(Store.instances[-1].steps), 2)
async def test_not_understood_never_executes(self):
initial = {"understood": False, "message": [], "tools": [{"name": "mail"}]}
with patch.object(agent_loop, "ToolExecutor", Executor):
result = await agent_loop.run_agent_loop(
Engine([]), initial, "s", "u", Channel(), "private", 1, 2)
self.assertEqual(result["tools"], [])
self.assertIn("明白", result["message"][0]["content"])
self.assertEqual(Executor.calls, [])
async def test_model_failure_includes_specific_safe_error(self):
Executor.calls = []
initial = {"message": [], "tools": [{"name": "pixiv", "args": {"action": "get"}}]}
with patch.object(agent_loop, "AgentTaskStore", Store), \
patch.object(agent_loop, "ToolExecutor", Executor):
# Force an observation containing a raw-looking failure, then no model response.
original = Executor.execute
async def failure(self, name, args, ctx):
return ToolResult(False, "send_failed", "多次尝试发送失败。请手动访问 https://example.test/image")
with patch.object(Executor, "execute", failure):
result = await agent_loop.run_agent_loop(
Engine([None]), initial, "s", "u", Channel(), "private", 1, 2)
text = result["message"][0]["content"]
self.assertIn("多次尝试发送失败", text)
self.assertIn("send_failed", text)
self.assertIn("https://example.test/image", text)
if __name__ == "__main__":
unittest.main()