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 среды исполнения.

Не публикуй архив виртуального окружения. Он не заменяет совместимые зависимости пакета.

Описание и декораторы

Сравни:

Загрузчик обходит методы по имени. Если методы называются 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, недоступный домен
ЗадачаЗапуск по времени, повтор, отмена, выключенный экземпляр
ЗависимостьОдин и несколько экземпляров, выключенный получатель
Входящий запросВерный и неверный ключ, повтор создания адреса
ОбменОдобрение, отказ, отзыв, выключенный получатель

Проверяй только возможности, которые действительно есть в модуле. Для модуля с одной командой не нужно создавать внешние запросы и расписание ради этой таблицы.

Права

Проверь три разных случая:

  1. Право заявлено и принято, действие выполняется.
  2. Право заявлено, но не принято, действие получает отказ.
  3. Право принято с ограничением, действие вне этого ограничения не проходит.

Отдельно проверь овнера, доверку с нужным действием и постороннего человека. Эти проверки не заменяют друг друга.

Для команды owner доверка не должна стать овнером из-за принятого telegram.send. Для команды public посторонний получает только возможности обработчика в принятых пределах.

Если модуль может работать без необязательного права, отказ должен завершаться предусмотренным результатом. Если без права он работать не может, это указывается в описании и видно при установке.

Для полного API проверь ограничения отдельно. Например, общий Telegram-запрос может получить отказ, если его адресатов нельзя сопоставить с разрешёнными чатами.

Несколько экземпляров

Установи вторую копию с другим именем, например second.

При совпадении команд выбери другой алиас. Проверь, что настройки, счётчик, файлы и страница не смешиваются. Остановка первой копии не должна менять данные второй.

Для вызовов зависимостей выбери нужный экземпляр. Не рассчитывай, что первый установленный навсегда останется выбранным.

Перезапуск и остановка

Измени данные, останови экземпляр и запусти снова.

Проверь, что:

После принудительного завершения stop может не выполниться. Данные, сохранённые только в нём, не считаются надёжно сохранёнными.

Не проверяй аварийный перезапуск всего сервиса на юзерботах других пользователей. Для этого используются отдельное окружение и операторские проверки.

Обновление

Подготовь состояние предыдущей версии: данные, настройки, файлы, расписание. Затем установи новую версию под тем же ID модуля.

Проверь обновление с резервной копией, перенос прежних данных, новые права и повторный запуск после переноса. Также проверь установку новой версии в пустой экземпляр.

Если обновление не удалось, экземпляр должен оставаться отключённым до решения пользователя. Необработанная ошибка не должна скрываться сообщением об успешном переносе.

После восстановления старых данных проверь предыдущую версию. Успешное создание архива ещё не доказывает, что прежний модуль работает с восстановленным состоянием.

Ошибки запуска

ПризнакПричина
NameError: loader при обычном запуске PythonФайл запущен вне загрузчика
Declaration does not match the installed versionОтличаются интерфейс, декораторы или порядок
access_deniedПраво, доступ человека или ограничение ресурса
handler_errorОбработчик завершился исключением
failed после долгой командыОшибка либо превышение времени
Нет ответа в ботеПодключение личного бота, диалог, доставка обновлений и права
Нет ответа аккаунтаПодключение Telegram, префикс, алиас, чат и права
Форма не отправиласьЛичный бот не может писать в чат или нет права
Модуль выключился сразу после подготовкиНет обработчиков и постоянной работы в init
Зависимость недоступнаПолучатель выключен, отсутствует или не выбран экземпляр

Сначала получи сообщение из журнала и состояние экземпляра. Если изменился код или описание опубликованной версии, выпусти новую версию. Перезапись прежнего номера не является исправлением.

Выпуск

Перед общим доступом проверь соответствие описания фактическому поведению. Укажи обязательного личного бота, внешние службы, нужные чаты, зависимые модули и дополнительные библиотеки.

В changes перечисли изменения текущей версии. Новые права и изменение формата данных должны быть понятны до установки.

Сохрани архив, описание, номер версии и контрольную сумму. Для GitHub сохрани коммит. После этого версию можно открыть в общем каталоге и при необходимости запросить проверку.