feat: rebuild AI commands and migrate config to TOML

This commit is contained in:
Azimkin
2026-09-14 13:03:04 +02:00
parent 01d3173254
commit 4421c9cd31
53 changed files with 1840 additions and 3464 deletions

96
tests/test_agent_tools.py Normal file
View File

@@ -0,0 +1,96 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock
from micro_agents_tools import FilesystemTools, PythonTools
from app.agent_tools import AgentToolManager, TelegramTools
class AgentToolPolicyTests(unittest.IsolatedAsyncioTestCase):
async def test_filesystem_and_python_tools_are_owner_only(self) -> None:
telegram_tools = TelegramTools(
SimpleNamespace(),
99,
base_dir=Path.cwd(),
).tools()
manager = AgentToolManager(
[*FilesystemTools().tools(), *PythonTools().tools(), *telegram_tools]
)
guest_names = {
tool.name for tool in await manager.available_tools({"is_owner": False})
}
owner_names = {
tool.name for tool in await manager.available_tools({"is_owner": True})
}
self.assertEqual(guest_names, {"send_message", "send_image", "send_file"})
self.assertIn("read_file", owner_names)
self.assertIn("write_file", owner_names)
self.assertIn("execute_python", owner_names)
class TelegramToolsTests(unittest.IsolatedAsyncioTestCase):
async def test_sends_text_to_current_chat_as_reply_to_command(self) -> None:
client = SimpleNamespace(
send_message=AsyncMock(return_value=SimpleNamespace(id=123))
)
tools = TelegramTools(
client,
99,
base_dir=Path.cwd(),
reply_to_message_id=7,
)
result = await tools.send_message("hello")
client.send_message.assert_awaited_once_with(
99,
"hello",
reply_to_message_id=7,
)
self.assertIn("message_id=123", result)
async def test_sends_image_and_file_from_workspace(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
base_dir = Path(temp_dir)
image = base_dir / "photo.jpg"
document = base_dir / "report.txt"
image.write_bytes(b"image")
document.write_text("report", encoding="utf-8")
client = SimpleNamespace(
send_photo=AsyncMock(return_value=SimpleNamespace(id=1)),
send_document=AsyncMock(return_value=SimpleNamespace(id=2)),
)
tools = TelegramTools(client, 99, base_dir=base_dir)
await tools.send_image("photo.jpg", "caption")
await tools.send_file("report.txt")
client.send_photo.assert_awaited_once_with(
99,
str(image),
caption="caption",
reply_to_message_id=None,
)
client.send_document.assert_awaited_once_with(
99,
str(document),
caption=None,
reply_to_message_id=None,
)
async def test_rejects_path_outside_workspace(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
tools = TelegramTools(SimpleNamespace(), 99, base_dir=Path(temp_dir))
with self.assertRaisesRegex(ValueError, "Absolute paths"):
await tools.send_file(str(Path(temp_dir).resolve() / "secret.txt"))
if __name__ == "__main__":
unittest.main()

306
tests/test_ai_command.py Normal file
View File

@@ -0,0 +1,306 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, MagicMock, patch
from app.agentic import (
AgentSessionHandle,
AgentSessionStore,
AgentSyntaxError,
_build_context_messages,
_download_message_media,
render_prompt_template,
parse_agent_invocation,
run_agent_turn,
strip_prompt_comments,
)
from app.commands.builtins import ai, command_registry
from app.config import AiConfig, AiProviderConfig
class AgentInvocationTests(unittest.TestCase):
def test_prompt_comments_are_removed_without_breaking_urls(self) -> None:
value = strip_prompt_comments(
"// first\nkeep https://example.com // trailing\n/* block\ncomment */\nlast"
)
self.assertNotIn("first", value)
self.assertNotIn("trailing", value)
self.assertNotIn("block", value)
self.assertIn("https://example.com", value)
self.assertIn("last", value)
def test_prompt_template_replaces_nested_values_and_text(self) -> None:
rendered = render_prompt_template(
"caller={{caller_user.id}} owner={owner.name} text={{text}}",
{
"caller_user": {"id": 42},
"owner": {"name": "Owner"},
"text": "old",
"source_text": "new",
},
)
self.assertEqual(rendered, "caller=42 owner=Owner text=new")
def test_parses_context_provider_and_thinking_flags(self) -> None:
invocation = parse_agent_invocation("12 .ds .th explain this", AiConfig())
self.assertEqual(invocation.context_messages, 12)
self.assertTrue(invocation.use_deepseek)
self.assertTrue(invocation.thinking)
self.assertEqual(invocation.prompt, "explain this")
def test_ignores_unknown_leading_dot_arguments(self) -> None:
invocation = parse_agent_invocation(".unknown .ds prompt", AiConfig())
self.assertTrue(invocation.use_deepseek)
self.assertEqual(invocation.prompt, "prompt")
def test_rejects_missing_prompt(self) -> None:
with self.assertRaises(AgentSyntaxError):
parse_agent_invocation("10 .ds .th", AiConfig())
def test_rejects_context_count_above_configured_limit(self) -> None:
with self.assertRaises(AgentSyntaxError):
parse_agent_invocation("101 prompt", AiConfig(max_context_messages=100))
class AgentContextTests(unittest.IsolatedAsyncioTestCase):
async def test_reply_is_injected_once_and_history_is_chronological(self) -> None:
older = SimpleNamespace(id=1, chat=SimpleNamespace(id=99), text="older")
newer = SimpleNamespace(id=2, chat=SimpleNamespace(id=99), text="newer")
command = SimpleNamespace(
id=3,
chat=SimpleNamespace(id=99),
reply_to_message=older,
)
async def history():
yield newer
yield older
client = SimpleNamespace(get_chat_history=lambda *args, **kwargs: history())
invocation = parse_agent_invocation("2 prompt", AiConfig())
with tempfile.TemporaryDirectory() as temp_dir:
messages = await _build_context_messages(
client,
command,
invocation,
AiConfig(),
Path(temp_dir),
user_template="wrapped={{message.is_reply}} {{text}}",
template_context={},
)
self.assertEqual(len(messages), 2)
self.assertIn("wrapped=true", messages[0].message())
self.assertIn("older", messages[0].message())
self.assertIn("newer", messages[1].message())
async def test_media_over_limit_is_not_downloaded(self) -> None:
message = SimpleNamespace(
id=1,
document=SimpleNamespace(file_size=6, mime_type="text/plain"),
)
client = SimpleNamespace(download_media=AsyncMock())
with tempfile.TemporaryDirectory() as temp_dir:
media = await _download_message_media(client, message, Path(temp_dir), 5)
self.assertEqual(media, [])
client.download_media.assert_not_awaited()
async def test_media_within_limit_is_attached(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
path = Path(temp_dir) / "note.txt"
path.write_bytes(b"hello")
message = SimpleNamespace(
id=1,
document=SimpleNamespace(
file_size=5,
mime_type="text/plain",
file_name="note.txt",
),
)
client = SimpleNamespace(download_media=AsyncMock(return_value=str(path)))
media = await _download_message_media(client, message, Path(temp_dir), 5)
self.assertEqual(len(media), 1)
self.assertEqual(media[0].data, b"hello")
self.assertEqual(media[0].kind, "file")
async def test_deepseek_and_reasoning_are_passed_to_agent(self) -> None:
config = AiConfig(
glm=AiProviderConfig("https://glm.example", "glm-key", "glm-model"),
deepseek=AiProviderConfig("https://ds.example", "ds-key", "ds-model"),
brave_api_key="brave-key",
)
invocation = parse_agent_invocation(".ds .th prompt", config)
command = SimpleNamespace(
id=3,
chat=SimpleNamespace(id=99),
from_user=SimpleNamespace(id=42),
reply_to_message=None,
)
account = SimpleNamespace(
me=SimpleNamespace(id=7, full_name="Owner", username="owner"),
config=SimpleNamespace(name="personal", session="personal"),
)
final_message = SimpleNamespace(message=lambda: "answer")
session = SimpleNamespace(
run=AsyncMock(
return_value=SimpleNamespace(
succeeded=True,
final_message=final_message,
error=None,
)
)
)
agent = MagicMock()
agent.create_session.return_value = session
with (
patch("app.agentic.OpenAIBasedLLM") as llm_class,
patch("app.agentic.Aggregator", return_value=agent) as aggregator_class,
patch("app.agentic.BraveSearchTools") as brave_tools,
patch("app.agentic._build_context_messages", AsyncMock(return_value=[])),
):
brave_tools.return_value.tools.return_value = []
result = await run_agent_turn(
SimpleNamespace(),
command,
account,
AgentSessionStore(),
invocation,
config,
)
self.assertEqual(result.answer, "answer")
llm_class.assert_called_once_with(
api_url="https://ds.example",
api_key="ds-key",
model="ds-model",
)
self.assertTrue(aggregator_class.call_args.kwargs["thinking"])
brave_tools.assert_called_once_with(api_key="brave-key", timeout_seconds=10.0)
result.handle.close()
async def test_reply_to_ai_response_reuses_existing_session(self) -> None:
workspace = MagicMock()
final_message = SimpleNamespace(message=lambda: "continued")
session = SimpleNamespace(
context={"old": True},
run=AsyncMock(
return_value=SimpleNamespace(
succeeded=True,
final_message=final_message,
error=None,
)
),
)
handle = AgentSessionHandle(session, workspace, set())
store = AgentSessionStore()
ai_response = SimpleNamespace(id=50, chat=SimpleNamespace(id=99))
store.register_response(handle, ai_response)
command = SimpleNamespace(
id=51,
chat=SimpleNamespace(id=99),
from_user=SimpleNamespace(id=7, full_name="Owner"),
reply_to_message=ai_response,
)
account = SimpleNamespace(
me=SimpleNamespace(id=7, full_name="Owner"),
config=SimpleNamespace(name="personal", session="personal"),
)
result = await run_agent_turn(
SimpleNamespace(),
command,
account,
store,
parse_agent_invocation("continue", AiConfig()),
AiConfig(),
)
self.assertIs(result.handle, handle)
self.assertEqual(result.answer, "continued")
session.run.assert_awaited_once_with("continue")
self.assertTrue(session.context["is_owner"])
store.clear()
workspace.cleanup.assert_called_once()
class AiCommandTests(unittest.IsolatedAsyncioTestCase):
def _ctx(self, args: str = "prompt") -> SimpleNamespace:
sent = SimpleNamespace(id=124, chat=SimpleNamespace(id=99))
message = SimpleNamespace(
id=123,
react=AsyncMock(),
reply_text=AsyncMock(return_value=sent),
)
session_store = MagicMock()
return SimpleNamespace(
args=args,
message=message,
client=SimpleNamespace(),
account=SimpleNamespace(
me=SimpleNamespace(id=7, full_name="Owner"),
config=SimpleNamespace(name="personal", session="personal"),
ai_sessions=session_store,
),
manager=SimpleNamespace(config=SimpleNamespace(ai=AiConfig())),
reply=message.reply_text,
)
def test_ai_is_registered_as_shared(self) -> None:
definition = command_registry.resolve("ai")
self.assertIsNotNone(definition)
self.assertTrue(definition.is_shared)
self.assertEqual(definition.guest_prefix, "..")
async def test_success_replies_with_agent_answer(self) -> None:
ctx = self._ctx()
handle = MagicMock()
with patch(
"app.commands.builtins.run_agent_turn",
AsyncMock(return_value=SimpleNamespace(answer="answer", handle=handle)),
):
await ai(ctx)
ctx.reply.assert_awaited_once_with("answer")
ctx.account.ai_sessions.register_response.assert_called_once_with(
handle,
ctx.reply.return_value,
)
ctx.message.react.assert_not_awaited()
async def test_invalid_syntax_only_dislikes(self) -> None:
ctx = self._ctx(".ds .th")
with self.assertLogs("app.commands.builtins", level="ERROR"):
await ai(ctx)
ctx.message.react.assert_awaited_once_with("👎")
ctx.reply.assert_not_awaited()
async def test_agent_error_only_dislikes(self) -> None:
ctx = self._ctx()
with patch(
"app.commands.builtins.run_agent_turn",
AsyncMock(side_effect=RuntimeError("API failed")),
):
with self.assertLogs("app.commands.builtins", level="ERROR"):
await ai(ctx)
ctx.message.react.assert_awaited_once_with("👎")
ctx.reply.assert_not_awaited()
if __name__ == "__main__":
unittest.main()

View File

@@ -1,92 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
from tempfile import TemporaryDirectory
from types import SimpleNamespace
import unittest
from app.ai.service import AIService
from app.commands import CommandRegistry
from app.config import AIConfig, AIModelConfig, AIPromptConfig, AIProviderConfig
def _tool_response(call_id: str) -> SimpleNamespace:
call = SimpleNamespace(
id=call_id,
function=SimpleNamespace(
name="temporary_command_create",
arguments=json.dumps({"name": "test", "response": "ok"}),
),
)
return SimpleNamespace(
choices=[
SimpleNamespace(
finish_reason="tool_calls",
message=SimpleNamespace(content=None, tool_calls=[call]),
)
]
)
class _FakeCompletions:
def __init__(self, responses: list[SimpleNamespace]) -> None:
self.responses = responses
self.requests: list[dict] = []
async def create(self, **kwargs: object) -> SimpleNamespace:
self.requests.append(dict(kwargs))
return self.responses.pop(0)
class AIToolLoopTests(unittest.IsolatedAsyncioTestCase):
async def test_suppresses_duplicate_tool_call_and_returns_final_answer(self) -> None:
with TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
config = AIConfig(
default_model="test",
context_messages=0,
prompt=AIPromptConfig(root=root),
providers={"provider": AIProviderConfig("provider", "key", "https://example.test")},
models={"test": AIModelConfig("test", "provider", "model")},
workspace_dir=root / "workspace",
)
registry = CommandRegistry()
service = AIService(config, registry)
completions = _FakeCompletions(
[
_tool_response("one"),
_tool_response("two"),
SimpleNamespace(
choices=[
SimpleNamespace(
finish_reason="stop",
message=SimpleNamespace(content="Created .test", tool_calls=[]),
)
]
),
]
)
service._clients["provider"] = SimpleNamespace(
chat=SimpleNamespace(completions=completions)
)
ctx = SimpleNamespace(
is_owner=True,
actor=None,
message=SimpleNamespace(chat=SimpleNamespace(id=1), id=1, text=".ai test"),
args="test",
replied_message=None,
replied_user=None,
)
response = await service.generate(ctx, SimpleNamespace(context_messages=0, model_flags=[], prompt="create test"))
service.close()
self.assertEqual(response.text, "Created .test")
self.assertIsNotNone(registry.resolve("test"))
self.assertEqual(len(completions.requests), 3)
self.assertIn("already been performed", completions.requests[2]["messages"][-1]["content"])
if __name__ == "__main__":
unittest.main()

View File

@@ -1,22 +0,0 @@
from __future__ import annotations
import json
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
from app.ai.transcript import AITranscriptLogger
class AITranscriptLoggerTests(unittest.TestCase):
def test_writes_jsonl_events(self) -> None:
with TemporaryDirectory() as temp_dir:
logger = AITranscriptLogger(Path(temp_dir))
logger.record("request", payload={"model": "test", "messages": []})
logger.record("response", payload={"choices": [{"message": {"content": "ok"}}]})
events = [json.loads(line) for line in logger.path.read_text(encoding="utf-8").splitlines()]
logger.close()
self.assertEqual([event["event"] for event in events], ["request", "response"])
self.assertEqual(events[1]["payload"]["choices"][0]["message"]["content"], "ok")

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
import unittest
from unittest.mock import patch
from app.config import _parse_github_webhook_config
from app.config import _parse_ai_config, _parse_elevenlabs_config, _parse_github_webhook_config
class ConfigGitHubWebhookTests(unittest.TestCase):
@@ -58,5 +58,68 @@ class ConfigGitHubWebhookTests(unittest.TestCase):
self.assertIsNone(config.secret)
class ConfigElevenLabsTests(unittest.TestCase):
def test_elevenlabs_config_parses_values(self) -> None:
with patch.dict("os.environ", {}, clear=True):
config = _parse_elevenlabs_config(
{
"api_key": "config-key",
"model_id": "scribe_v2",
"language_code": "ru",
"request_timeout_seconds": 120,
}
)
self.assertEqual(config.api_key, "config-key")
self.assertEqual(config.model_id, "scribe_v2")
self.assertEqual(config.language_code, "ru")
self.assertEqual(config.request_timeout_seconds, 120)
def test_elevenlabs_env_overrides(self) -> None:
env = {
"ELEVENLABS_API_KEY": "env-key",
"ELEVENLABS_MODEL_ID": "scribe_v2",
"ELEVENLABS_LANGUAGE_CODE": "en",
"ELEVENLABS_REQUEST_TIMEOUT_SECONDS": "45",
}
with patch.dict("os.environ", env, clear=True):
config = _parse_elevenlabs_config({"api_key": "config-key"})
self.assertEqual(config.api_key, "env-key")
self.assertEqual(config.language_code, "en")
self.assertEqual(config.request_timeout_seconds, 45)
class ConfigAiTests(unittest.TestCase):
def test_ai_config_parses_both_providers(self) -> None:
with patch.dict("os.environ", {}, clear=True):
config = _parse_ai_config(
{
"glm": {"api_key": "glm-key", "model": "glm-model"},
"deepseek": {"api_key": "ds-key", "model": "ds-model"},
}
)
self.assertEqual(config.glm.api_key, "glm-key")
self.assertEqual(config.glm.model, "glm-model")
self.assertEqual(config.deepseek.api_key, "ds-key")
self.assertEqual(config.deepseek.model, "ds-model")
def test_ai_provider_env_overrides(self) -> None:
env = {
"GLM_API_KEY": "env-glm",
"GLM_MODEL": "env-glm-model",
"DEEPSEEK_API_KEY": "env-ds",
"DEEPSEEK_MODEL": "env-ds-model",
}
with patch.dict("os.environ", env, clear=True):
config = _parse_ai_config({})
self.assertEqual(config.glm.api_key, "env-glm")
self.assertEqual(config.glm.model, "env-glm-model")
self.assertEqual(config.deepseek.api_key, "env-ds")
self.assertEqual(config.deepseek.model, "env-ds-model")
if __name__ == "__main__":
unittest.main()

44
tests/test_config_toml.py Normal file
View File

@@ -0,0 +1,44 @@
from __future__ import annotations
import tempfile
import unittest
from pathlib import Path
from unittest.mock import patch
from app.config import load_app_config
class TomlConfigTests(unittest.TestCase):
def test_loads_default_toml_config(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
(root / "config.toml").write_text(
"""
api_id = 123456
api_hash = "hash"
session_dir = "sessions"
[[accounts]]
name = "personal"
session = "personal"
[elevenlabs]
api_key = "test-key"
model_id = "scribe_v2"
""".strip(),
encoding="utf-8",
)
with patch.dict("os.environ", {}, clear=True):
config = load_app_config(root)
self.assertEqual(config.api_id, 123456)
self.assertEqual(config.accounts[0].session, "personal")
self.assertEqual(config.elevenlabs.api_key, "test-key")
self.assertEqual(config.session_dir, root / "sessions")
self.assertEqual(config.ai.prompt_dir, root / "prompts")
self.assertEqual(config.ai.base_dir, root)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,104 +0,0 @@
from __future__ import annotations
from pathlib import Path
import unittest
from unittest.mock import patch
from app.config import _parse_ai_config
def _raw_ai_config() -> dict:
return {
"default_model": "test",
"context_messages": 1,
"prompt": {"root": "prompts/ai"},
"providers": {
"test": {
"api_key": "model-key",
"base_url": "https://example.test/v1",
}
},
"models": {
"test": {
"provider": "test",
"model": "test-model",
}
},
"web_search": {
"api_key": "config-key",
"count": 99,
"safesearch": "strict",
},
"page_extract": {
"max_chars": 5000,
"render_dynamic": False,
},
}
class ConfigWebSearchTests(unittest.TestCase):
def test_web_search_config_clamps_count(self) -> None:
with patch.dict("os.environ", {}, clear=True):
config = _parse_ai_config(_raw_ai_config(), root=Path.cwd())
assert config is not None
self.assertEqual(config.web_search.api_key, "config-key")
self.assertEqual(config.web_search.count, 20)
self.assertEqual(config.web_search.safesearch, "strict")
self.assertEqual(config.page_extract.max_chars, 5000)
self.assertFalse(config.page_extract.render_dynamic)
def test_web_search_env_overrides(self) -> None:
env = {
"BRAVE_API_KEY": "env-key",
"BRAVE_SEARCH_COUNT": "3",
"BRAVE_SEARCH_COUNTRY": "pl",
"BRAVE_SEARCH_LANG": "pl",
"BRAVE_SEARCH_UI_LANG": "pl-PL",
"BRAVE_SEARCH_SAFESEARCH": "off",
"PAGE_EXTRACT_MAX_CHARS": "9000",
"PAGE_EXTRACT_RENDER_DYNAMIC": "true",
}
with patch.dict("os.environ", env, clear=True):
config = _parse_ai_config(_raw_ai_config(), root=Path.cwd())
assert config is not None
self.assertEqual(config.web_search.api_key, "env-key")
self.assertEqual(config.web_search.count, 3)
self.assertEqual(config.web_search.country, "PL")
self.assertEqual(config.web_search.search_lang, "pl")
self.assertEqual(config.web_search.ui_lang, "pl-PL")
self.assertEqual(config.web_search.safesearch, "off")
self.assertEqual(config.page_extract.max_chars, 9000)
self.assertTrue(config.page_extract.render_dynamic)
def test_web_search_accepts_brave_search_api_key_env(self) -> None:
env = {"BRAVE_SEARCH_API_KEY": "search-env-key"}
with patch.dict("os.environ", env, clear=True):
config = _parse_ai_config(_raw_ai_config(), root=Path.cwd())
assert config is not None
self.assertEqual(config.web_search.api_key, "search-env-key")
def test_brave_api_key_takes_precedence_over_search_alias(self) -> None:
env = {
"BRAVE_API_KEY": "primary-key",
"BRAVE_SEARCH_API_KEY": "alias-key",
}
with patch.dict("os.environ", env, clear=True):
config = _parse_ai_config(_raw_ai_config(), root=Path.cwd())
assert config is not None
self.assertEqual(config.web_search.api_key, "primary-key")
def test_web_search_strips_bearer_prefix(self) -> None:
env = {"BRAVE_SEARCH_API_KEY": " Bearer token-value "}
with patch.dict("os.environ", env, clear=True):
config = _parse_ai_config(_raw_ai_config(), root=Path.cwd())
assert config is not None
self.assertEqual(config.web_search.api_key, "token-value")
if __name__ == "__main__":
unittest.main()

View File

@@ -1,45 +0,0 @@
from __future__ import annotations
import unittest
from app.ai.page_extract import extract_readable_text, normalize_url, validate_public_url
class PageExtractTests(unittest.TestCase):
def test_extract_readable_text_skips_scripts_and_styles(self) -> None:
title, text = extract_readable_text(
"""
<html>
<head>
<title>Example Page</title>
<style>.hidden { display: none; }</style>
</head>
<body>
<h1>Hello</h1>
<script>alert('nope')</script>
<p>Readable text.</p>
</body>
</html>
"""
)
self.assertEqual(title, "Example Page")
self.assertIn("Hello", text)
self.assertIn("Readable text.", text)
self.assertNotIn("alert", text)
self.assertNotIn("display", text)
def test_normalize_url_rejects_non_web_schemes(self) -> None:
with self.assertRaisesRegex(ValueError, "http and https"):
normalize_url("file:///etc/passwd")
def test_validate_public_url_blocks_localhost(self) -> None:
with self.assertRaisesRegex(ValueError, "blocked"):
validate_public_url("http://127.0.0.1:8000", block_private_hosts=True)
def test_validate_public_url_can_allow_private_hosts(self) -> None:
validate_public_url("http://127.0.0.1:8000", block_private_hosts=False)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,47 +0,0 @@
from __future__ import annotations
from datetime import UTC, datetime
from pathlib import Path
from tempfile import TemporaryDirectory
import unittest
from app.ai.prompts import PromptBook
class PromptBookTests(unittest.TestCase):
def test_renders_builtin_time_placeholders(self) -> None:
prompt_book = PromptBook(Path("."))
rendered = prompt_book._render_template(
(
"{{current_date}} {{current_time}} {{current_datetime}} "
"{{current_weekday}} {{timezone}} {{utc_date}} {{utc_time}} "
"{{utc_datetime}} {{unix_timestamp}}"
),
now=datetime(2026, 5, 19, 12, 34, 56, tzinfo=UTC),
)
self.assertIn("2026-05-19", rendered)
self.assertIn("2026-05-19T12:34:56+00:00", rendered)
self.assertIn("Tuesday", rendered)
self.assertIn("1779194096", rendered)
def test_unknown_placeholders_are_left_unchanged(self) -> None:
prompt_book = PromptBook(Path("."))
self.assertEqual(
prompt_book._render_template("Hello {{unknown_value}}"),
"Hello {{unknown_value}}",
)
def test_load_system_prompt_renders_files(self) -> None:
with TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
(root / "system.md").write_text("Today is {{current_date}}", encoding="utf-8")
rendered = PromptBook(root).load_system_prompt()
self.assertNotIn("{{current_date}}", rendered)
if __name__ == "__main__":
unittest.main()

View File

@@ -1,31 +0,0 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock
from app.commands import CommandRegistry, TemporaryCommandStore
class TemporaryCommandStoreTests(unittest.IsolatedAsyncioTestCase):
async def test_created_command_is_owner_only_and_replies_from_memory(self) -> None:
registry = CommandRegistry()
store = TemporaryCommandStore(registry)
message = store.create("test-command", "Temporary response", "Test command")
command = registry.resolve("test-command")
self.assertIn(".test-command", message)
self.assertIsNotNone(command)
self.assertFalse(command.is_shared)
ctx = SimpleNamespace(reply=AsyncMock())
await command.handler(ctx)
ctx.reply.assert_awaited_once_with("Temporary response")
def test_rejects_existing_command_name(self) -> None:
registry = CommandRegistry()
store = TemporaryCommandStore(registry)
store.create("test", "First")
with self.assertRaisesRegex(ValueError, "already exists"):
store.create("test", "Second")

94
tests/test_txt_command.py Normal file
View File

@@ -0,0 +1,94 @@
from __future__ import annotations
import unittest
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from pyrogram.enums import MessageEntityType
from app.commands.builtins import command_registry, txt
from app.transcription import TranscriptionError
class TxtCommandTests(unittest.IsolatedAsyncioTestCase):
def test_txt_is_registered_as_shared(self) -> None:
definition = command_registry.resolve("txt")
self.assertIsNotNone(definition)
self.assertTrue(definition.is_shared)
self.assertEqual(definition.guest_prefix, "..")
def _ctx(self, *, reply=None, args: str = ""):
message = SimpleNamespace(
id=123,
reply_to_message=reply,
react=AsyncMock(),
reply_text=AsyncMock(),
)
return SimpleNamespace(
args=args,
message=message,
replied_message=reply,
client=SimpleNamespace(download_media=AsyncMock(return_value="downloaded.ogg")),
manager=SimpleNamespace(
config=SimpleNamespace(elevenlabs=SimpleNamespace())
),
reply=message.reply_text,
)
async def test_invalid_usage_only_reacts_with_dislike(self) -> None:
ctx = self._ctx()
await txt(ctx)
ctx.message.react.assert_awaited_once_with("👎")
ctx.reply.assert_not_awaited()
ctx.client.download_media.assert_not_awaited()
async def test_api_error_only_reacts_with_dislike(self) -> None:
media = SimpleNamespace(mime_type="audio/ogg")
reply = SimpleNamespace(voice=media, video_note=None, video=None)
ctx = self._ctx(reply=reply)
with patch(
"app.commands.builtins.transcribe_file",
AsyncMock(side_effect=TranscriptionError("API failed")),
):
with self.assertLogs("app.commands.builtins", level="ERROR"):
await txt(ctx)
ctx.message.react.assert_awaited_once_with("👎")
ctx.reply.assert_not_awaited()
async def test_success_replies_with_expandable_blockquote(self) -> None:
media = SimpleNamespace(mime_type="video/mp4")
reply = SimpleNamespace(voice=None, video_note=media, video=None)
ctx = self._ctx(reply=reply)
with patch(
"app.commands.builtins.transcribe_file",
AsyncMock(return_value="Расшифрованный текст"),
) as transcribe:
await txt(ctx)
ctx.message.react.assert_not_awaited()
ctx.reply.assert_awaited_once()
self.assertEqual(ctx.reply.await_args.args[0], "Расшифрованный текст")
entity = ctx.reply.await_args.kwargs["entities"][0]
self.assertEqual(entity.type, MessageEntityType.BLOCKQUOTE)
self.assertTrue(entity.expandable)
transcribe.assert_awaited_once()
async def test_arguments_make_otherwise_valid_command_invalid(self) -> None:
media = SimpleNamespace(mime_type="video/mp4")
reply = SimpleNamespace(voice=None, video_note=None, video=media)
ctx = self._ctx(reply=reply, args="unexpected")
await txt(ctx)
ctx.message.react.assert_awaited_once_with("👎")
ctx.reply.assert_not_awaited()
if __name__ == "__main__":
unittest.main()

View File

@@ -1,199 +0,0 @@
from __future__ import annotations
import unittest
import httpx
from app.ai.web_search import (
_format_error_response,
BraveWebSearchClient,
build_query,
format_results,
parse_response,
)
from app.config import AIWebSearchConfig
class WebSearchTests(unittest.IsolatedAsyncioTestCase):
def test_build_query_clamps_and_defaults(self) -> None:
config = AIWebSearchConfig(api_key="key", count=5)
query = build_query(
config,
{
"query": " python web framework ",
"count": 99,
"offset": -1,
"freshness": "pw",
},
)
self.assertEqual(query.query, "python web framework")
self.assertEqual(query.count, 20)
self.assertEqual(query.offset, 0)
self.assertEqual(query.freshness, "pw")
self.assertEqual(query.safesearch, "moderate")
def test_build_query_rejects_invalid_freshness(self) -> None:
with self.assertRaisesRegex(ValueError, "freshness"):
build_query(AIWebSearchConfig(), {"query": "news", "freshness": "yesterday"})
def test_build_query_normalizes_unsupported_brave_country_and_ui_lang(self) -> None:
config = AIWebSearchConfig(
api_key="key",
country="UA",
search_lang="ru",
ui_lang="ru-UA",
)
query = build_query(config, {"query": "crimea news"})
self.assertEqual(query.country, "ALL")
self.assertEqual(query.search_lang, "ru")
self.assertEqual(query.ui_lang, "ru-RU")
def test_build_query_falls_back_unknown_ui_lang_to_english(self) -> None:
config = AIWebSearchConfig(api_key="key", ui_lang="zz-ZZ")
query = build_query(config, {"query": "python"})
self.assertEqual(query.ui_lang, "en-US")
def test_parse_and_format_results(self) -> None:
response = parse_response(
"openai",
{
"query": {"altered": "open ai", "more_results_available": True},
"web": {
"results": [
{
"title": "OpenAI",
"url": "https://openai.com",
"description": "AI research and products.",
"age": "May 18, 2026",
"extra_snippets": ["Models and APIs.", ""],
},
{"title": "", "url": ""},
]
},
},
)
self.assertEqual(len(response.results), 1)
self.assertTrue(response.more_results_available)
formatted = format_results(response)
self.assertIn("Search results for: openai", formatted)
self.assertIn("https://openai.com", formatted)
self.assertIn("Models and APIs.", formatted)
def test_format_empty_results(self) -> None:
formatted = format_results(parse_response("nothing", {"web": {"results": []}}))
self.assertIn("No results found.", formatted)
def test_format_error_response_uses_brave_detail(self) -> None:
request = httpx.Request("GET", "https://api.search.brave.com/res/v1/web/search")
response = httpx.Response(
429,
request=request,
json={"error": {"detail": "Rate limit exceeded"}},
)
self.assertIn("Rate limit exceeded", _format_error_response(response))
async def test_search_sends_subscription_token_header(self) -> None:
seen_headers: httpx.Headers | None = None
async def handler(request: httpx.Request) -> httpx.Response:
nonlocal seen_headers
seen_headers = request.headers
return httpx.Response(
200,
request=request,
json={
"query": {"original": "python"},
"web": {"results": []},
},
)
client = BraveWebSearchClient(
AIWebSearchConfig(api_key="test-key"),
transport=httpx.MockTransport(handler),
)
await client.search({"query": "python"})
assert seen_headers is not None
self.assertEqual(seen_headers["x-subscription-token"], "test-key")
async def test_search_logs_success_response_summary(self) -> None:
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
request=request,
json={
"query": {"original": "python"},
"web": {
"results": [
{
"title": "Python",
"url": "https://www.python.org",
}
]
},
},
)
client = BraveWebSearchClient(
AIWebSearchConfig(api_key="test-key"),
transport=httpx.MockTransport(handler),
)
with self.assertLogs("app.ai.web_search", level="INFO") as captured:
await client.search({"query": "python"})
logs = "\n".join(captured.output)
self.assertIn("Brave web search response status=200", logs)
self.assertIn("results=1", logs)
async def test_search_logs_error_response_body(self) -> None:
async def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
422,
request=request,
json={
"error": {
"code": "VALIDATION",
"detail": "Unable to validate request parameter(s)",
}
},
)
client = BraveWebSearchClient(
AIWebSearchConfig(api_key="test-key"),
transport=httpx.MockTransport(handler),
)
with self.assertLogs("app.ai.web_search", level="WARNING") as captured:
with self.assertRaisesRegex(ValueError, "Unable to validate"):
await client.search({"query": "python"})
logs = "\n".join(captured.output)
self.assertIn("Brave web search error status=422", logs)
self.assertIn("VALIDATION", logs)
async def test_search_rejects_missing_subscription_token_before_request(self) -> None:
async def handler(request: httpx.Request) -> httpx.Response:
self.fail("request should not be sent without a subscription token")
client = BraveWebSearchClient(
AIWebSearchConfig(api_key=""),
transport=httpx.MockTransport(handler),
)
with self.assertRaisesRegex(ValueError, "BRAVE_API_KEY or BRAVE_SEARCH_API_KEY"):
await client.search({"query": "python"})
if __name__ == "__main__":
unittest.main()