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()