diff --git a/tests/test_application.py b/tests/test_application.py new file mode 100644 index 0000000..800f4a5 --- /dev/null +++ b/tests/test_application.py @@ -0,0 +1,65 @@ +import tempfile +import unittest +from pathlib import Path +from unittest.mock import Mock, patch +from zoneinfo import ZoneInfo + +from assistant_bot import application as application_module +from assistant_bot.auth import authentication_guard +from assistant_bot.handlers import ( + ask_command, + help_command, + history_command, + inline_query, + model_command, + models_command, + new_conversation_command, + private_text, + private_voice, + start, +) + + +class ApplicationRegistrationTests(unittest.TestCase): + def test_registers_the_current_public_handlers_in_order(self) -> None: + with tempfile.TemporaryDirectory() as directory, patch.multiple( + application_module, + get_assistant_password=Mock(return_value="secret"), + get_bot_token=Mock(return_value="123456:TEST"), + get_db_path=Mock( + return_value=Path(directory) / "assistant.sqlite3" + ), + get_assistant_mode=Mock(return_value="local"), + get_ollama_base_url=Mock(return_value="http://localhost:11434"), + get_whisper_model=Mock(return_value="small"), + get_whisper_device=Mock(return_value="cpu"), + get_whisper_compute_type=Mock(return_value="int8"), + get_whisper_language=Mock(return_value="ru"), + get_local_timezone=Mock(return_value=ZoneInfo("UTC")), + get_voice_max_duration_seconds=Mock(return_value=120), + ): + application = application_module.create_application() + + self.assertEqual( + [handler.callback for handler in application.handlers[-1]], + [authentication_guard], + ) + self.assertEqual( + [handler.callback for handler in application.handlers[0]], + [ + start, + help_command, + ask_command, + models_command, + model_command, + new_conversation_command, + history_command, + inline_query, + private_voice, + private_text, + ], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_core.py b/tests/test_core.py index 49b4aa3..87dd823 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -6,7 +6,11 @@ from datetime import datetime, timezone from pathlib import Path from zoneinfo import ZoneInfo -from assistant_bot.agent import execute_agent_tool, parse_agent_decision +from assistant_bot.agent import ( + build_agent_tool_prompt, + execute_agent_tool, + parse_agent_decision, +) from assistant_bot.reminders import parse_reminder, unit_to_timedelta from assistant_bot.storage import AssistantStorage @@ -52,6 +56,144 @@ class AgentDecisionTests(unittest.TestCase): self.assertTrue(decision.reset_context) +class AgentToolContractTests(unittest.TestCase): + TOOL_NAMES = ( + "get_current_datetime", + "remember", + "list_memory", + "delete_memory", + "create_note", + "list_notes", + "delete_note", + "create_reminder", + "list_reminders", + "cancel_reminder", + "create_status", + "list_statuses", + "update_status", + "delete_status", + "search_conversation", + ) + + def test_prompt_exposes_all_supported_tool_names(self) -> None: + prompt = build_agent_tool_prompt(ZoneInfo("UTC")) + + for tool_name in self.TOOL_NAMES: + with self.subTest(tool_name=tool_name): + self.assertIn(tool_name, prompt) + + def test_crud_tool_result_shapes_remain_stable(self) -> None: + with tempfile.TemporaryDirectory() as directory: + storage = AssistantStorage(Path(directory) / "assistant.sqlite3") + common = { + "storage": storage, + "user_id": 42, + "chat_id": 100, + "tz": ZoneInfo("UTC"), + } + + memory = execute_agent_tool( + "remember", + {"text": "короткие ответы"}, + **common, + ) + self.assertEqual( + memory, + {"ok": True, "id": memory["id"], "text": "короткие ответы"}, + ) + self.assertEqual( + execute_agent_tool("list_memory", {"limit": 10}, **common)[ + "items" + ][0]["id"], + memory["id"], + ) + self.assertEqual( + execute_agent_tool( + "delete_memory", + {"id": memory["id"]}, + **common, + ), + {"ok": True, "id": memory["id"]}, + ) + + note = execute_agent_tool( + "create_note", + {"text": "идея"}, + **common, + ) + self.assertEqual( + note, + {"ok": True, "id": note["id"], "text": "идея"}, + ) + self.assertEqual( + execute_agent_tool("list_notes", {}, **common)["items"][0][ + "id" + ], + note["id"], + ) + self.assertEqual( + execute_agent_tool( + "delete_note", + {"id": note["id"]}, + **common, + ), + {"ok": True, "id": note["id"]}, + ) + + status = execute_agent_tool( + "create_status", + {"title": "паспорт", "status": "ожидание"}, + **common, + ) + self.assertEqual( + status, + { + "ok": True, + "id": status["id"], + "title": "паспорт", + "status": "ожидание", + }, + ) + self.assertEqual( + execute_agent_tool( + "update_status", + {"id": status["id"], "status": "готово"}, + **common, + ), + {"ok": True, "id": status["id"], "status": "готово"}, + ) + self.assertEqual( + execute_agent_tool("list_statuses", {}, **common)["items"][0][ + "status" + ], + "готово", + ) + self.assertEqual( + execute_agent_tool( + "delete_status", + {"id": status["id"]}, + **common, + ), + {"ok": True, "id": status["id"]}, + ) + + def test_unknown_tool_error_remains_stable(self) -> None: + with tempfile.TemporaryDirectory() as directory: + result = execute_agent_tool( + "missing", + {}, + AssistantStorage(Path(directory) / "assistant.sqlite3"), + user_id=42, + chat_id=100, + tz=ZoneInfo("UTC"), + ) + + self.assertEqual( + result, + {"ok": False, "error": "unknown tool: missing"}, + ) + + class StorageTests(unittest.TestCase): def test_user_authorization_is_persisted(self) -> None: with tempfile.TemporaryDirectory() as directory: