feat(bot): Коммит — сообщение, Push, конфликты, публикация в репозиторий загрузок
- Publisher: пуши по одному (lock), fetch → проверка конфликта с base_sha → чистое дерево origin/main → черновик → коммит от tgit-bot с трейлером Telegram-User → push; отказ push → rebase и повтор (до 3 раз); защита от путей вне клона; лимит 20 пушей/час - «💬 Сообщение коммита» (1–200 символов, первая строка ≤ 72) и «🚀 Push» с подтверждением; исходы: запушено (sha, ссылка, ⏳ CI) · конфликт → «Перезаписать» · нечего коммитить · лимит · сбой - запись в pushes (chat_id, message_id — для обновления статуса CI/CD в #11), черновик очищается - ruff: RUF001–003 выключены — кириллица в текстах это норма Refs #9 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from aiogram.methods import EditMessageText, SendMessage
|
||||
from aiogram.types import InlineKeyboardMarkup
|
||||
|
||||
from app.access.service import AccessService
|
||||
from app.bot.routers.commit import Act, CommitCb
|
||||
from app.db.session import SessionMaker
|
||||
from app.main import build_dispatcher
|
||||
from app.repo.git import UploadsRepo
|
||||
from app.texts import Texts
|
||||
from tests.conftest import ADMIN, git
|
||||
from tests.fakes import TgClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tg(texts: Texts, sessions: SessionMaker, access: AccessService, repo: UploadsRepo) -> TgClient:
|
||||
return TgClient(build_dispatcher(texts, sessions, access, repo, "https://example.test/commit/{sha}"))
|
||||
|
||||
|
||||
def cb(action: Act, i: int = 0) -> str:
|
||||
return CommitCb(a=action, i=i).pack()
|
||||
|
||||
|
||||
def labels(markup: object) -> list[str]:
|
||||
assert isinstance(markup, InlineKeyboardMarkup)
|
||||
return [b.text for row in markup.inline_keyboard for b in row]
|
||||
|
||||
|
||||
async def test_message_then_push_lands_in_remote(tg: TgClient, texts: Texts, remote: Path, tmp_path: Path) -> None:
|
||||
t = texts.commit
|
||||
await tg.send(ADMIN, texts.menu.commit)
|
||||
await tg.press(ADMIN, cb(Act.NEW_FILE))
|
||||
await tg.send(ADMIN, "notes.txt")
|
||||
await tg.send(ADMIN, "привет из телеграма")
|
||||
await tg.press(ADMIN, cb(Act.DRAFT))
|
||||
draft = tg.session.sent(EditMessageText)[-1]
|
||||
assert t.message_missing in draft.text and t.push_button not in labels(draft.reply_markup)
|
||||
|
||||
await tg.press(ADMIN, cb(Act.MESSAGE))
|
||||
await tg.send(ADMIN, "x" * 80) # first line too long
|
||||
assert tg.session.sent(SendMessage)[-1].text == t.bad_message
|
||||
await tg.send(ADMIN, "docs: заметки")
|
||||
with_message = tg.session.sent(SendMessage)[-1]
|
||||
assert t.message_current.format(message="docs: заметки") in with_message.text
|
||||
assert t.push_button in labels(with_message.reply_markup)
|
||||
|
||||
await tg.press(ADMIN, cb(Act.PUSH))
|
||||
assert tg.session.sent(EditMessageText)[-1].text.startswith("Запушить в main?")
|
||||
await tg.press(ADMIN, cb(Act.PUSH_GO))
|
||||
|
||||
pushing, done = tg.session.sent(EditMessageText)[-2:]
|
||||
assert pushing.text == t.pushing
|
||||
assert done.text.startswith("✅ Запушено: ") and "docs: заметки" in done.text
|
||||
assert "https://example.test/commit/" in done.text and done.text.endswith(t.ci_pending)
|
||||
|
||||
clone = tmp_path / "verify"
|
||||
git(tmp_path, "clone", "-q", str(remote), str(clone))
|
||||
assert (clone / "notes.txt").read_text(encoding="utf-8") == "привет из телеграма"
|
||||
|
||||
# the draft is gone after a push
|
||||
tg.session.requests.clear()
|
||||
await tg.send(ADMIN, texts.menu.commit)
|
||||
assert not any(label.startswith("🧾") for label in labels(tg.session.sent(SendMessage)[-1].reply_markup))
|
||||
Reference in New Issue
Block a user