test(mutmut): stabilize runs and extend telegram coverage (#157)
This commit is contained in:
+8
-1
@@ -54,6 +54,7 @@ build-backend = "uv_build"
|
|||||||
|
|
||||||
[dependency-groups]
|
[dependency-groups]
|
||||||
dev = [
|
dev = [
|
||||||
|
"mutmut>=3.4.0",
|
||||||
"pytest>=9.0.2",
|
"pytest>=9.0.2",
|
||||||
"pytest-anyio>=0.0.0",
|
"pytest-anyio>=0.0.0",
|
||||||
"pytest-cov>=7.0.0",
|
"pytest-cov>=7.0.0",
|
||||||
@@ -66,9 +67,15 @@ docs = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
addopts = ["--cov=takopi", "--cov-branch", "--cov-report=term-missing", "--cov-fail-under=80"]
|
addopts = ["--cov=takopi", "--cov-branch", "--cov-report=term-missing", "--cov-fail-under=81"]
|
||||||
testpaths = ["tests"]
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[tool.mutmut]
|
||||||
|
paths_to_mutate = ["src/takopi"]
|
||||||
|
tests_dir = ["tests"]
|
||||||
|
pytest_add_cli_args = ["-q", "--no-cov"]
|
||||||
|
do_not_mutate = ["src/takopi/cli/*"]
|
||||||
|
|
||||||
[tool.ruff.lint]
|
[tool.ruff.lint]
|
||||||
extend-select = ["B", "BLE001", "C4", "PERF", "RUF043", "S110", "SIM", "UP"]
|
extend-select = ["B", "BLE001", "C4", "PERF", "RUF043", "S110", "SIM", "UP"]
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from collections.abc import Awaitable, Callable, Iterable
|
from collections.abc import Awaitable, Callable, Iterable
|
||||||
@@ -75,6 +76,9 @@ async def watch_config(
|
|||||||
reserved: Iterable[str] = RESERVED_CHAT_COMMANDS,
|
reserved: Iterable[str] = RESERVED_CHAT_COMMANDS,
|
||||||
on_reload: Callable[[ConfigReload], Awaitable[None]] | None = None,
|
on_reload: Callable[[ConfigReload], Awaitable[None]] | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
|
# Mutmut sets MUTANT_UNDER_TEST; disable watchers to avoid native crashes.
|
||||||
|
if os.environ.get("MUTANT_UNDER_TEST"):
|
||||||
|
return
|
||||||
reserved_tuple = tuple(reserved)
|
reserved_tuple = tuple(reserved)
|
||||||
config_path = config_path.expanduser().resolve()
|
config_path = config_path.expanduser().resolve()
|
||||||
watch_root = config_path.parent
|
watch_root = config_path.parent
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ class SafeWriter(io.TextIOBase):
|
|||||||
|
|
||||||
|
|
||||||
def setup_logging(
|
def setup_logging(
|
||||||
*, debug: bool = False, cache_logger_on_first_use: bool = True
|
*, debug: bool = False, cache_logger_on_first_use: bool = False
|
||||||
) -> None:
|
) -> None:
|
||||||
global _MIN_LEVEL, _PIPELINE_LEVEL_NAME
|
global _MIN_LEVEL, _PIPELINE_LEVEL_NAME
|
||||||
global _log_file_handle
|
global _log_file_handle
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from pathlib import Path
|
|||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from rich.console import Console
|
||||||
from rich.table import Table
|
from rich.table import Table
|
||||||
from rich.text import Text
|
from rich.text import Text
|
||||||
|
|
||||||
@@ -258,6 +259,42 @@ def test_render_engine_table_prints() -> None:
|
|||||||
assert ui.printed
|
assert ui.printed
|
||||||
|
|
||||||
|
|
||||||
|
def test_debug_onboarding_paths_prints_table() -> None:
|
||||||
|
console = Console(record=True, width=120)
|
||||||
|
onboarding.debug_onboarding_paths(console=console)
|
||||||
|
output = console.export_text()
|
||||||
|
assert "onboarding paths (15)" in output
|
||||||
|
assert "workspace" in output
|
||||||
|
assert "assistant" in output
|
||||||
|
assert "handoff" in output
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_confirm_prompt_returns_question_result(monkeypatch) -> None:
|
||||||
|
seen: dict[str, object] = {}
|
||||||
|
|
||||||
|
class _PromptSession:
|
||||||
|
def __init__(self, *args, **kwargs) -> None:
|
||||||
|
seen["args"] = args
|
||||||
|
seen["kwargs"] = kwargs
|
||||||
|
self.app = object()
|
||||||
|
|
||||||
|
class _Question:
|
||||||
|
def __init__(self, app) -> None:
|
||||||
|
seen["app"] = app
|
||||||
|
|
||||||
|
async def ask_async(self) -> bool | None:
|
||||||
|
return True
|
||||||
|
|
||||||
|
monkeypatch.setattr(onboarding, "PromptSession", _PromptSession)
|
||||||
|
monkeypatch.setattr(onboarding, "Question", _Question)
|
||||||
|
|
||||||
|
result = await onboarding.confirm_prompt("continue?", default=False)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert "kwargs" in seen
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_get_bot_info_retries(monkeypatch) -> None:
|
async def test_get_bot_info_retries(monkeypatch) -> None:
|
||||||
class _Bot:
|
class _Bot:
|
||||||
|
|||||||
+136
-14
@@ -3,7 +3,15 @@ from typing import Any
|
|||||||
import anyio
|
import anyio
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from takopi.telegram.api_models import Chat, File, Message, Update, User
|
from takopi.telegram.api_models import (
|
||||||
|
Chat,
|
||||||
|
ChatMember,
|
||||||
|
File,
|
||||||
|
ForumTopic,
|
||||||
|
Message,
|
||||||
|
Update,
|
||||||
|
User,
|
||||||
|
)
|
||||||
from takopi.telegram.client import BotClient, TelegramClient, TelegramRetryAfter
|
from takopi.telegram.client import BotClient, TelegramClient, TelegramRetryAfter
|
||||||
|
|
||||||
|
|
||||||
@@ -13,6 +21,16 @@ class FakeBot(BotClient):
|
|||||||
self.edit_calls: list[str] = []
|
self.edit_calls: list[str] = []
|
||||||
self.delete_calls: list[tuple[int, int]] = []
|
self.delete_calls: list[tuple[int, int]] = []
|
||||||
self.topic_calls: list[tuple[int, int, str]] = []
|
self.topic_calls: list[tuple[int, int, str]] = []
|
||||||
|
self.document_calls: list[
|
||||||
|
tuple[int, str, bytes, int | None, int | None, bool | None, str | None]
|
||||||
|
] = []
|
||||||
|
self.command_calls: list[
|
||||||
|
tuple[list[dict[str, Any]], dict[str, Any] | None, str | None]
|
||||||
|
] = []
|
||||||
|
self.callback_calls: list[tuple[str, str | None, bool | None]] = []
|
||||||
|
self.chat_calls: list[int] = []
|
||||||
|
self.chat_member_calls: list[tuple[int, int]] = []
|
||||||
|
self.create_topic_calls: list[tuple[int, str]] = []
|
||||||
self._edit_attempts = 0
|
self._edit_attempts = 0
|
||||||
self._updates_attempts = 0
|
self._updates_attempts = 0
|
||||||
self.retry_after: float | None = None
|
self.retry_after: float | None = None
|
||||||
@@ -51,16 +69,18 @@ class FakeBot(BotClient):
|
|||||||
disable_notification: bool | None = False,
|
disable_notification: bool | None = False,
|
||||||
caption: str | None = None,
|
caption: str | None = None,
|
||||||
) -> Message | None:
|
) -> Message | None:
|
||||||
_ = (
|
|
||||||
chat_id,
|
|
||||||
filename,
|
|
||||||
content,
|
|
||||||
reply_to_message_id,
|
|
||||||
message_thread_id,
|
|
||||||
disable_notification,
|
|
||||||
caption,
|
|
||||||
)
|
|
||||||
self.calls.append("send_document")
|
self.calls.append("send_document")
|
||||||
|
self.document_calls.append(
|
||||||
|
(
|
||||||
|
chat_id,
|
||||||
|
filename,
|
||||||
|
content,
|
||||||
|
reply_to_message_id,
|
||||||
|
message_thread_id,
|
||||||
|
disable_notification,
|
||||||
|
caption,
|
||||||
|
)
|
||||||
|
)
|
||||||
return Message(message_id=1, chat=Chat(id=chat_id, type="private"))
|
return Message(message_id=1, chat=Chat(id=chat_id, type="private"))
|
||||||
|
|
||||||
async def edit_message_text(
|
async def edit_message_text(
|
||||||
@@ -104,9 +124,8 @@ class FakeBot(BotClient):
|
|||||||
scope: dict[str, Any] | None = None,
|
scope: dict[str, Any] | None = None,
|
||||||
language_code: str | None = None,
|
language_code: str | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
_ = commands
|
self.calls.append("set_my_commands")
|
||||||
_ = scope
|
self.command_calls.append((commands, scope, language_code))
|
||||||
_ = language_code
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def get_updates(
|
async def get_updates(
|
||||||
@@ -144,7 +163,8 @@ class FakeBot(BotClient):
|
|||||||
text: str | None = None,
|
text: str | None = None,
|
||||||
show_alert: bool | None = None,
|
show_alert: bool | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
_ = callback_query_id, text, show_alert
|
self.calls.append("answer_callback_query")
|
||||||
|
self.callback_calls.append((callback_query_id, text, show_alert))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
async def edit_forum_topic(
|
async def edit_forum_topic(
|
||||||
@@ -154,6 +174,21 @@ class FakeBot(BotClient):
|
|||||||
self.topic_calls.append((chat_id, message_thread_id, name))
|
self.topic_calls.append((chat_id, message_thread_id, name))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def get_chat(self, chat_id: int) -> Chat | None:
|
||||||
|
self.calls.append("get_chat")
|
||||||
|
self.chat_calls.append(chat_id)
|
||||||
|
return Chat(id=chat_id, type="private")
|
||||||
|
|
||||||
|
async def get_chat_member(self, chat_id: int, user_id: int) -> ChatMember | None:
|
||||||
|
self.calls.append("get_chat_member")
|
||||||
|
self.chat_member_calls.append((chat_id, user_id))
|
||||||
|
return ChatMember(status="member")
|
||||||
|
|
||||||
|
async def create_forum_topic(self, chat_id: int, name: str) -> ForumTopic | None:
|
||||||
|
self.calls.append("create_forum_topic")
|
||||||
|
self.create_topic_calls.append((chat_id, name))
|
||||||
|
return ForumTopic(message_thread_id=11)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_edit_forum_topic_uses_outbox() -> None:
|
async def test_edit_forum_topic_uses_outbox() -> None:
|
||||||
@@ -169,6 +204,93 @@ async def test_edit_forum_topic_uses_outbox() -> None:
|
|||||||
assert bot.topic_calls == [(7, 42, "takopi @main")]
|
assert bot.topic_calls == [(7, 42, "takopi @main")]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_send_document_uses_outbox() -> None:
|
||||||
|
bot = FakeBot()
|
||||||
|
client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)
|
||||||
|
|
||||||
|
result = await client.send_document(
|
||||||
|
chat_id=5,
|
||||||
|
filename="note.txt",
|
||||||
|
content=b"hello",
|
||||||
|
caption="greetings",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is not None
|
||||||
|
assert bot.calls == ["send_document"]
|
||||||
|
assert bot.document_calls == [
|
||||||
|
(5, "note.txt", b"hello", None, None, False, "greetings")
|
||||||
|
]
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_set_my_commands_uses_outbox() -> None:
|
||||||
|
bot = FakeBot()
|
||||||
|
client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)
|
||||||
|
|
||||||
|
commands = [{"command": "ping", "description": "Ping the bot"}]
|
||||||
|
result = await client.set_my_commands(
|
||||||
|
commands,
|
||||||
|
scope={"type": "default"},
|
||||||
|
language_code="en",
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert bot.calls == ["set_my_commands"]
|
||||||
|
assert bot.command_calls == [(commands, {"type": "default"}, "en")]
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_answer_callback_query_uses_outbox() -> None:
|
||||||
|
bot = FakeBot()
|
||||||
|
client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)
|
||||||
|
|
||||||
|
result = await client.answer_callback_query(
|
||||||
|
callback_query_id="cb-1",
|
||||||
|
text="ok",
|
||||||
|
show_alert=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result is True
|
||||||
|
assert bot.calls == ["answer_callback_query"]
|
||||||
|
assert bot.callback_calls == [("cb-1", "ok", True)]
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_get_chat_and_member_uses_outbox() -> None:
|
||||||
|
bot = FakeBot()
|
||||||
|
client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)
|
||||||
|
|
||||||
|
chat = await client.get_chat(9)
|
||||||
|
member = await client.get_chat_member(9, 42)
|
||||||
|
|
||||||
|
assert chat is not None
|
||||||
|
assert chat.id == 9
|
||||||
|
assert member is not None
|
||||||
|
assert member.status == "member"
|
||||||
|
assert bot.calls == ["get_chat", "get_chat_member"]
|
||||||
|
assert bot.chat_calls == [9]
|
||||||
|
assert bot.chat_member_calls == [(9, 42)]
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_create_forum_topic_uses_outbox() -> None:
|
||||||
|
bot = FakeBot()
|
||||||
|
client = TelegramClient(client=bot, private_chat_rps=0.0, group_chat_rps=0.0)
|
||||||
|
|
||||||
|
topic = await client.create_forum_topic(3, "status updates")
|
||||||
|
|
||||||
|
assert topic is not None
|
||||||
|
assert topic.message_thread_id == 11
|
||||||
|
assert bot.calls == ["create_forum_topic"]
|
||||||
|
assert bot.create_topic_calls == [(3, "status updates")]
|
||||||
|
await client.close()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_edits_coalesce_latest() -> None:
|
async def test_edits_coalesce_latest() -> None:
|
||||||
class _BlockingBot(FakeBot):
|
class _BlockingBot(FakeBot):
|
||||||
|
|||||||
Reference in New Issue
Block a user