test: lock down application and agent contracts
This commit is contained in:
65
tests/test_application.py
Normal file
65
tests/test_application.py
Normal file
@@ -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()
|
||||||
@@ -6,7 +6,11 @@ from datetime import datetime, timezone
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from zoneinfo import ZoneInfo
|
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.reminders import parse_reminder, unit_to_timedelta
|
||||||
from assistant_bot.storage import AssistantStorage
|
from assistant_bot.storage import AssistantStorage
|
||||||
|
|
||||||
@@ -52,6 +56,144 @@ class AgentDecisionTests(unittest.TestCase):
|
|||||||
self.assertTrue(decision.reset_context)
|
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):
|
class StorageTests(unittest.TestCase):
|
||||||
def test_user_authorization_is_persisted(self) -> None:
|
def test_user_authorization_is_persisted(self) -> None:
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
with tempfile.TemporaryDirectory() as directory:
|
||||||
|
|||||||
Reference in New Issue
Block a user