Cluster · разработчикам
Проверка перед выпуском
Публикация сохраняет архив и описание. Она не проверяет вместо автора поведение команды, перенос данных или работу внешнего сервиса.
По порядку: синтаксис, описание, непубличная установка, права, перезапуск и обновление. Сам процесс публикации находится в STORE.md, разработка в DEVELOPMENT.md.
Синтаксис и архив
В каталоге модуля:
python3 -m py_compile main.py
python3 -m json.tool manifest.json
python3 -m zipfile -l module.zipПервая команда проверяет синтаксис, но не вызывает обработчики и не проверяет имена app и loader. Файлы __pycache__, которые она создаст, в архив не нужны.
Проверь корень ZIP: main.py должен быть доступен после распаковки. Если используются дополнительные Python-файлы, они тоже входят в пакет. Python-зависимости перечисли в requirements и в поле «Пакеты Python» при публикации. Они будут получены из PyPI при сборке. Если прикладываешь свои пакеты, проверь содержимое wheels и совместимость с Python среды исполнения.
Не публикуй архив виртуального окружения. Он не заменяет совместимые зависимости пакета.
Описание и декораторы
Сравни:
- имя класса
Moduleи наличиеmain.py; - ID методов и записи в каждой группе
interface; - алиас, источник и доступ каждой команды;
- поля кнопок, действий, функций, сигналов, задач и входящих запросов;
accessиconfirmобработчиков;Module.configиinterface.config;Module.pagesиinterface.pages;- порядок записей в списках.
Загрузчик обходит методы по имени. Если методы называются echo и ping, в interface.commands сначала указывается echo. Несовпадение порядка тоже вызывает отказ регистрации.
В установленном окружении исходников Cluster можно проверить свой модуль теми же классами, которые использует ядро. Создай рядом файл check.py:
import json
from pathlib import Path
from cluster.manager.modules.catalog import Catalog
from cluster.shared.schema import Schema
from cluster.user.loader import loader
from cluster.user.router import Router
def check(path: Path) -> None:
manifest = Catalog.manifest(json.loads((path / "manifest.json").read_text()))
scope = {"loader": loader, "app": None}
code = compile((path / "main.py").read_text(), str(path / "main.py"), "exec")
exec(code, scope)
module = scope["Module"]()
router = Router(None)
router.inspect(module)
actual = Schema.parse(router.interface)
if actual != manifest["interface"]:
raise ValueError("The description does not match the module")
print("Module description matches")
check(Path("./my-module"))Замени ./my-module путём к своему каталогу. Для этого способа нужно окружение с установленным Cluster и его зависимостями. Порядок установки приведён в DEPLOYMENT.md.
Проверка выполняет верхний код файла и конструктор класса. Используй её для своих исходников. Обращения к ядру должны находиться в init или обработчиках, поэтому здесь app=None.
Для примеров самого репозитория:
python scripts/check_docs.py
python -m pytest -q tests/test_module_sdk.pyПервая команда проверяет документы, маршруты и описания всех каталогов в examples. Вторая проверяет поведение загрузчика и SDK. Эти команды не отправляют настоящие сообщения Telegram.
Проверка обработчика отдельно
Логику небольшой команды можно проверить без Telegram. Например, для учебного Ping. Следующий файл запускается из корня репозитория Cluster:
import asyncio
import runpy
from pathlib import Path
from types import SimpleNamespace
from cluster.user.loader import loader
class Event:
def __init__(self, args: str = "") -> None:
self.args = args
self.source, self.data = "telegram", {"out": True}
self.replies: list[str] = []
self.edits: list[str] = []
async def reply(self, text: str) -> None:
self.replies.append(text)
async def edit(self, text: str) -> None:
self.edits.append(text)
async def check() -> None:
scope = runpy.run_path(str(Path("examples/quickstart/main.py")), init_globals={"loader": loader})
module = scope["Module"]()
ping = Event()
await module.ping(ping)
assert ping.replies == []
assert ping.edits[0] == "Cluster is running."
assert ping.edits[1].startswith("Cluster is running.\nPing: ")
echo = Event("example")
await module.echo(echo)
assert echo.replies == ["example"]
asyncio.run(check())Здесь Event является небольшой подстановкой только для проверки. Это не подмена события в работающем ядре. Проверяются текст и ветвления обработчика, не доставка Telegram и не выданные права.
Успешный такой тест не означает, что модуль запустится в gVisor или что бот сможет отправить сообщение в выбранный чат. Для этого нужна установленная копия.
Непубличная установка
Опубликуй версию без показа в общем каталоге и установи себе.
Проверь, что сборка завершилась, экземпляр находится в running, обязательные поля заполнены. Открой журнал после запуска.
Для каждой команды получи настоящий ответ в Telegram. Если заявлены оба источника, проверь и аккаунт, и личного бота. При работе с кнопками сначала начни диалог с личным ботом, затем отправь форму.
| Возможность | Проверка |
|---|---|
| Команда | Без аргументов, с правильными и неправильными аргументами |
| Обработчик сообщения | Подходящий текст, неподходящий текст, редактирование |
| Кнопка | Нажатие, повтор, истечение формы, нажатие другим человеком |
| Подтверждение | Первое и повторное нажатие, изменение аргументов |
| Конфигурация | Минимум, максимум, пустое обязательное поле, изменение при работе |
| Секрет | Сохранение, замена, очистка, отсутствие значения в странице и журнале |
| Страница | Начальные данные, изменение, повторное открытие |
| Файл | Загрузка, получение, удаление, превышение размера |
| Сеть | Успех, ошибка сервиса, неправильный JSON, недоступный домен |
| Задача | Запуск по времени, повтор, отмена, выключенный экземпляр |
| Зависимость | Один и несколько экземпляров, выключенный получатель |
| Входящий запрос | Верный и неверный ключ, повтор создания адреса |
| Обмен | Одобрение, отказ, отзыв, выключенный получатель |
Проверяй только возможности, которые действительно есть в модуле. Для модуля с одной командой не нужно создавать внешние запросы и расписание ради этой таблицы.
Права
Проверь три разных случая:
- Право заявлено и принято, действие выполняется.
- Право заявлено, но не принято, действие получает отказ.
- Право принято с ограничением, действие вне этого ограничения не проходит.
Отдельно проверь овнера, доверку с нужным действием и постороннего человека. Эти проверки не заменяют друг друга.
Для команды owner доверка не должна стать овнером из-за принятого telegram.send. Для команды public посторонний получает только возможности обработчика в принятых пределах.
Если модуль может работать без необязательного права, отказ должен завершаться предусмотренным результатом. Если без права он работать не может, это указывается в описании и видно при установке.
Для полного API проверь ограничения отдельно. Например, общий Telegram-запрос может получить отказ, если его адресатов нельзя сопоставить с разрешёнными чатами.
Несколько экземпляров
Установи вторую копию с другим именем, например second.
При совпадении команд выбери другой алиас. Проверь, что настройки, счётчик, файлы и страница не смешиваются. Остановка первой копии не должна менять данные второй.
Для вызовов зависимостей выбери нужный экземпляр. Не рассчитывай, что первый установленный навсегда останется выбранным.
Перезапуск и остановка
Измени данные, останови экземпляр и запусти снова.
Проверь, что:
- важные значения получаются из базы;
- страница получает начальные данные;
- задачи и внешние ключи не создаются повторно без необходимости;
- временные Python-поля восстанавливаются;
- цикл обработчиков снова получает команды.
После принудительного завершения stop может не выполниться. Данные, сохранённые только в нём, не считаются надёжно сохранёнными.
Не проверяй аварийный перезапуск всего сервиса на юзерботах других пользователей. Для этого используются отдельное окружение и операторские проверки.
Обновление
Подготовь состояние предыдущей версии: данные, настройки, файлы, расписание. Затем установи новую версию под тем же ID модуля.
Проверь обновление с резервной копией, перенос прежних данных, новые права и повторный запуск после переноса. Также проверь установку новой версии в пустой экземпляр.
Если обновление не удалось, экземпляр должен оставаться отключённым до решения пользователя. Необработанная ошибка не должна скрываться сообщением об успешном переносе.
После восстановления старых данных проверь предыдущую версию. Успешное создание архива ещё не доказывает, что прежний модуль работает с восстановленным состоянием.
Ошибки запуска
| Признак | Причина |
|---|---|
NameError: loader при обычном запуске Python | Файл запущен вне загрузчика |
Declaration does not match the installed version | Отличаются интерфейс, декораторы или порядок |
access_denied | Право, доступ человека или ограничение ресурса |
handler_error | Обработчик завершился исключением |
failed после долгой команды | Ошибка либо превышение времени |
| Нет ответа в боте | Подключение личного бота, диалог, доставка обновлений и права |
| Нет ответа аккаунта | Подключение Telegram, префикс, алиас, чат и права |
| Форма не отправилась | Личный бот не может писать в чат или нет права |
| Модуль выключился сразу после подготовки | Нет обработчиков и постоянной работы в init |
| Зависимость недоступна | Получатель выключен, отсутствует или не выбран экземпляр |
Сначала получи сообщение из журнала и состояние экземпляра. Если изменился код или описание опубликованной версии, выпусти новую версию. Перезапись прежнего номера не является исправлением.
Выпуск
Перед общим доступом проверь соответствие описания фактическому поведению. Укажи обязательного личного бота, внешние службы, нужные чаты, зависимые модули и дополнительные библиотеки.
В changes перечисли изменения текущей версии. Новые права и изменение формата данных должны быть понятны до установки.
Сохрани архив, описание, номер версии и контрольную сумму. Для GitHub сохрани коммит. После этого версию можно открыть в общем каталоге и при необходимости запросить проверку.