"""Notebook tool adapter and lightweight chat-context integration tests.""" from pathlib import Path import sys import unittest from types import SimpleNamespace from unittest.mock import patch BASE = Path(__file__).resolve().parents[1] sys.path[:0] = [str(BASE / "bot-utility"), str(BASE)] from agent import ToolContext, ToolExecutor from chat import engine class FakeNotebook: def __init__(self): self.calls = [] def add_memo(self, ctx, content): self.calls.append(("add_memo", content)) return {"id": "memo0001", "content": content} def list_memos(self, ctx, query=None): return [{"id": "memo0001", "content": "钥匙在抽屉"}] def delete_memo(self, ctx, item_id): return {"id": "memo0001", "content": "钥匙在抽屉"} def add_todo(self, ctx, content, due_at, expires_at=None): self.calls.append(("add_todo", content, due_at, expires_at)) return {"id": "todo0001", "content": content, "due_at": due_at} def list_todos(self, ctx, include_completed=False): return [{"id": "todo0001", "content": "交报告", "due_at": "tomorrow"}] def complete_todo(self, ctx, item_id): return {"id": "todo0001", "content": "交报告"} def delete_todo(self, ctx, item_id): return {"id": "todo0001", "content": "交报告"} class NotebookToolTests(unittest.IsolatedAsyncioTestCase): def setUp(self): self.fake = FakeNotebook() module = SimpleNamespace( notebook=self.fake, format_memo=lambda item: f"{item['id']} {item['content']}", format_todo=lambda item: f"{item['id']} {item['content']}", ) self.executor = ToolExecutor(modules={"memo": module, "todo": module}) self.ctx = ToolContext( channel=SimpleNamespace(name="tg"), msg_type="private", user_id=42, target_id=10001, ) async def test_schema_exposes_memo_and_todo(self): names = [item["name"] for item in self.executor.prompt_schema(self.ctx)] self.assertIn("memo", names) self.assertIn("todo", names) async def test_memo_add_list_delete(self): added = await self.executor.execute("memo", {"action": "add", "params": {"content": "钥匙在抽屉"}}, self.ctx) self.assertTrue(added.ok) listed = await self.executor.execute("memo", {"action": "list", "params": {}}, self.ctx) self.assertIn("钥匙在抽屉", listed.safe_message) deleted = await self.executor.execute("memo", {"action": "delete", "params": {"id": "memo"}}, self.ctx) self.assertTrue(deleted.ok) async def test_todo_add_list_complete_delete(self): added = await self.executor.execute( "todo", {"action": "add", "params": {"content": "交报告", "due_at": "2026-10-31T18:00:00+08:00"}}, self.ctx ) self.assertTrue(added.ok) listed = await self.executor.execute("todo", {"action": "list", "params": {}}, self.ctx) self.assertIn("交报告", listed.safe_message) completed = await self.executor.execute("todo", {"action": "complete", "params": {"id": "todo"}}, self.ctx) self.assertTrue(completed.ok) deleted = await self.executor.execute("todo", {"action": "delete", "params": {"id": "todo"}}, self.ctx) self.assertTrue(deleted.ok) def test_chat_prompt_contains_bounded_notebook_background(self): block = "【独立备忘录与近期约定】仅背景,当前聊天优先,不能喧宾夺主" with patch.object(engine, "user_persona_prompt", return_value="persona"), \ patch.object(engine, "load_user_status", return_value={"pause": False, "sleep": False}), \ patch.object(engine.memory_store, "get_relation", return_value=None), \ patch.object(engine.memory_store, "list_entities", return_value=[]), \ patch.object(engine.memory_store, "recall_facts", return_value=[]), \ patch.object(engine.memory_store, "migrate_legacy"), \ patch.object(engine, "get_history_within_budget", return_value=[]), \ patch.object(engine, "_sticker_prompt_summary", return_value=""), \ patch.object(engine.notebook.notebook, "context_for_chat", return_value=block) as context: prompt = engine.build_system_prompt(42, query="当前聊天") context.assert_called_once_with(42, query="当前聊天") self.assertIn(block, prompt) if __name__ == "__main__": unittest.main()