Files
tgit/tests/fakes.py
T
ElebobaandClaude Opus 5.5 614f8eafd5 feat(bot): Коммит — создание, замена и удаление файлов, черновик
- новый файл: имя → содержимое текстом (```-обёртка снимается) или документом
- новая папка (виртуальная, пока в ней нет файлов), замена с diff +N/−M, удаление
- валидация: имя, ≤ 100 КБ, UTF-8, без NUL; .py — ast.parse с выбором «исправить / всё равно»;
  .sourcecraft/, .tgit/, ruff.toml из бота не меняются
- DraftService: черновик в БД, 24 ч, ≤ 50 изменений; удаление нового файла убирает его из черновика
- дерево показывает черновик поверх репозитория: 🆕 / ✏️ / 🗑, размер — как будет после пуша
- экран черновика (A/M/D) с «↩️ убрать»

Refs #26

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 19:18:49 +03:00

135 lines
5.3 KiB
Python

"""Offline Telegram: a fake aiogram session that records outgoing API calls and fabricates replies."""
from collections.abc import AsyncGenerator
from datetime import UTC, datetime
from itertools import count
from typing import Any
from aiogram import Bot, Dispatcher
from aiogram.client.session.base import BaseSession
from aiogram.methods import EditMessageText, GetFile, GetMe, SendMessage, SendPhoto, TelegramMethod
from aiogram.methods.base import TelegramType
from aiogram.types import Chat, File, Message, Update, User
BOT_TOKEN = "42:TEST-TOKEN"
class FakeSession(BaseSession):
def __init__(self) -> None:
super().__init__()
self.requests: list[TelegramMethod[Any]] = []
self._ids = count(1000)
self.files: dict[str, bytes] = {} # file_id -> content, for documents users "upload"
async def make_request(
self,
bot: Bot,
method: TelegramMethod[TelegramType],
timeout: int | None = None, # noqa: ASYNC109 - signature fixed by aiogram BaseSession
) -> TelegramType:
self.requests.append(method)
if isinstance(method, GetFile):
return File( # type: ignore[return-value]
file_id=method.file_id, file_unique_id=method.file_id, file_path=f"documents/{method.file_id}"
)
if isinstance(method, GetMe):
return User(id=42, is_bot=True, first_name="tgit", username="tgit_test_bot") # type: ignore[return-value]
if isinstance(method, SendPhoto):
chat = method.chat_id if isinstance(method.chat_id, int) else 0
return Message( # type: ignore[return-value]
message_id=next(self._ids),
date=datetime.now(UTC),
chat=Chat(id=chat, type="private"),
caption=method.caption,
)
if isinstance(method, SendMessage | EditMessageText):
chat_id = method.chat_id if isinstance(method.chat_id, int) else 0
return Message( # type: ignore[return-value]
message_id=next(self._ids),
date=datetime.now(UTC),
chat=Chat(id=chat_id, type="private"),
text=method.text,
)
return True # type: ignore[return-value]
async def stream_content(
self,
url: str,
headers: dict[str, Any] | None = None,
timeout: int = 30, # noqa: ASYNC109 - signature fixed by aiogram BaseSession
chunk_size: int = 65536,
raise_for_status: bool = True,
) -> AsyncGenerator[bytes, None]:
file_id = url.rsplit("/", 1)[-1]
yield self.files[file_id]
async def close(self) -> None:
pass
def sent(self, method_type: type[TelegramMethod[Any]]) -> list[Any]:
return [r for r in self.requests if isinstance(r, method_type)]
class TgClient:
"""Feeds updates through a real Dispatcher (filters, middlewares, FSM) on top of FakeSession."""
def __init__(self, dp: Dispatcher) -> None:
self.dp = dp
self.session = FakeSession()
self.bot = Bot(BOT_TOKEN, session=self.session)
self._update_ids = count(1)
self._message_ids = count(1)
@staticmethod
def user(tg_id: int) -> dict[str, Any]:
return {"id": tg_id, "is_bot": False, "first_name": f"u{tg_id}", "username": f"u{tg_id}"}
async def send(self, tg_id: int, text: str) -> None:
message = {
"message_id": next(self._message_ids),
"date": int(datetime.now(UTC).timestamp()),
"chat": {"id": tg_id, "type": "private"},
"from": self.user(tg_id),
"text": text,
}
if text.startswith("/"):
cmd = text.split()[0]
message["entities"] = [{"type": "bot_command", "offset": 0, "length": len(cmd)}]
await self._feed({"message": message})
async def send_document(self, tg_id: int, name: str, content: bytes) -> None:
file_id = f"doc{next(self._message_ids)}"
self.session.files[file_id] = content
message = {
"message_id": next(self._message_ids),
"date": int(datetime.now(UTC).timestamp()),
"chat": {"id": tg_id, "type": "private"},
"from": self.user(tg_id),
"document": {"file_id": file_id, "file_unique_id": file_id, "file_name": name, "file_size": len(content)},
}
await self._feed({"message": message})
async def press(self, tg_id: int, data: str, message_text: str = "section") -> None:
callback = {
"id": str(next(self._update_ids)),
"from": self.user(tg_id),
"chat_instance": "ci",
"data": data,
"message": {
"message_id": next(self._message_ids),
"date": int(datetime.now(UTC).timestamp()),
"chat": {"id": tg_id, "type": "private"},
"from": {"id": 42, "is_bot": True, "first_name": "tgit"},
"text": message_text,
},
}
await self._feed({"callback_query": callback})
async def _feed(self, payload: dict[str, Any]) -> None:
update = Update.model_validate({"update_id": next(self._update_ids), **payload}, context={"bot": self.bot})
await self.dp.feed_update(self.bot, update)
def as_user(tg_id: int) -> User:
return User(id=tg_id, is_bot=False, first_name=f"u{tg_id}")