Перейти к содержимому
Pre-MVP. Кодовая база 1.0 не является выпущенным продуктом — см. границы продукта 1.0.

Тестирование плагина

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

Поверхность, которая тестируется, описана в Protocol v1, а инварианты, ради защиты которых существуют приведённые ниже случаи, — в Безопасность плагинов.

Это случаи, которые должны быть у плагина. Каждый из них — свойство протокола, а не какого-то конкретного плагина, поэтому каждый из них должен быть в вашем наборе тестов, что бы ни делал ваш плагин.

Случай Что должно произойти
Активация Одноразовый код расходуется ровно один раз; неизвестный, использованный или просроченный код отклоняется; сырой токен хранится в зашифрованном виде и никогда не попадает в логи
Ротация kind: rotate заменяет сохранённый токен; старый перестаёт использоваться; при отклонённой ротации старый токен остаётся на месте
Запуск Код обменивается один раз, через бэкенд плагина; второй обмен завершается ошибкой; код, выпущенный для другой инсталляции, завершается ошибкой
Сессия Собственная браузерная сессия плагина создаётся при обмене, является отдельным cookie и не несёт никаких учётных данных Dee Wan
Закрепление Публикация закрепляет current_version_id, снятие с публикации закрепляет published_version_id, оба закрепляют workflow.revision, а выбранная пара (kind, to) взята из делегированного чтения
Идемпотентность Ключ выводится из записи и её ревизии, одинаков при каждом повторе и меняется, когда запись перепланируется
Не более одного раза Два воркера не могут выполнить одну запись: аренда (lease) с compare-and-set плюс ключ как вторая защита на стороне ядра
Повтор idempotent_replay засчитывается как успех, а не как новый эффект
Терминальные отказы invalid_request, plugin_unauthorized, capability_refused, requester_unauthorized, content_not_found, scheduled_target_stale, workflow_conflict, dependent_content, idempotency_key_reused, transition_unavailable и transition_refused — каждый завершается без повтора, под собственным именем
Повторы rate_limited соблюдает Retry-After в пределах максимума; temporary_failure и транспортные сбои дают экспоненциальную задержку с джиттером; оба прекращаются по лимиту попыток И по лимиту возраста
Недоверенные ответы Статус, расходящийся с outcome.code, тело не в формате конверта, 3xx или неизвестный статус — это терминальный отказ, никогда не успех и никогда не бесконечный повтор
Изоляция Каждая таблица и каждый запрос ограничены инсталляцией; одна инсталляция не может читать, отменять или выполнять записи другой
Отзыв После удаления ожидающая работа отказывает с plugin_unauthorized, и плагин сообщает об этом, а не повторяет попытки
Секреты Ни одна тестовая фикстура, строка лога, тело ошибки или скриншот не содержит токен, код запуска или код активации

Набор тестов эталонного плагина — рабочий пример для каждой строки: plugins/scheduling/test/ (activation, launch, schedules, runner, time, isolation, flow).

Не направляйте свой набор тестов на работающий Dee Wan. Фейковое ядро детерминировано, может внедрять сбои, которые реальное по запросу не воспроизведёт, и работает за миллисекунды.

plugins/scheduling/test/helpers/fake-core.ts — как раз такое ядро и задуманная отправная точка для плагина в другом репозитории. Это реализация fetch, которая:

  • аутентифицирует bearer-токен для каждой инсталляции и в противном случае отвечает plugin_unauthorized;
  • отвечает на /context, /content/:id и /launch/exchange телами в форме протокола, включая блок workflow и переходы, которые разрешает делегирование;
  • проверяет закрепление и ревизию workflow в команде и отвечает scheduled_target_stale или workflow_conflict, когда они больше не выполняются;
  • хранит запись идемпотентности для каждого ключа, так что точный повтор получает ответ idempotent_replay, а повторно использованный ключ с другими входными данными — idempotency_key_reused;
  • расходует код запуска один раз и отклоняет его для другой инсталляции;
  • внедряет сбои по запросу — сетевую ошибку, любой статус, любой outcome.code, заголовок Retry-After или намеренно искажённое тело;
  • записывает каждую полученную команду, чтобы тест мог утверждать, что ничего не было отправлено дважды.

Две вещи, которыми оно НЕ является: оно не ядро, а его строки message — заглушки. Проверяйте outcome.code и HTTP-статус, но никогда — человекочитаемое сообщение: сообщения ядра предназначены для людей и не стабильны.

Рядом с ним plugins/scheduling/test/helpers/d1.ts — 50-строчная прослойка над better-sqlite3 с поверхностью D1 prepare/bind/first/all/run/batch, так что настоящий SQL плагина — включая его захваты через compare-and-set и частичный уникальный индекс — выполняется в наборе тестов.

Окно терминала
cd plugins/scheduling
npm test # vitest
npm run typecheck

Два документа JSON Schema генерируются из определений, по которым валидирует ядро, и хранятся в репозитории:

  • schemas/manifest-v1.schema.json — валидируйте свой манифест в собственном CI, ещё до того, как администратор его запросит.
  • schemas/protocol-v1.schema.json — формы запросов и ответов в $defs, плюс машиночитаемая таблица outcomes (HTTP-статус и флаг повтора для каждого кода), capabilities, installation_states и limits.

Подключать стоит именно таблицу outcomes: плагин, который держит собственную копию (эталонный плагин так делает, в plugins/scheduling/src/protocol.ts, потому что не может импортировать из ядра), должен проверять, что его копия совпадает с опубликованной. backend/test/plugin-contract.test.ts делает ровно это для эталонного плагина и падает, если они когда-либо разойдутся в статусе или флаге повтора.

Оба документа, а также protocol-v1.md, перегенерируются командой:

Окно терминала
npm run docs:plugins

backend/test/plugin-docs.test.ts падает, если закоммиченные копии расходятся, поэтому команда нужна только после изменения протокола.

Оба сервиса на одной машине, без аккаунта Cloudflare и без реальных учётных данных.

Dee Wan. Из корня репозитория:

Окно терминала
node scripts/install-dependencies.mjs
npm run local:migrate
npm run local:seed # prints the sign-in it creates
npm run local:api # admin Worker on http://localhost:8787 (leave running)
npm run local:admin # admin UI on http://localhost:5173 (leave running)

Административному Worker нужны три настройки, прежде чем он станет общаться с плагином по http://, потому что в продакшене любой URL плагина допускается только по HTTPS. В backend/.dev.vars:

ENVIRONMENT=development
PLUGIN_DEV_ORIGINS=http://localhost:8788
BACKEND_URL=http://localhost:8787

PLUGIN_DEV_ORIGINS — это allowlist точных origin, без подстановочных знаков и без упрощений для приватных диапазонов, и он читается только при ENVIRONMENT=development. BACKEND_URL — это то, что ядро отправляет как dee_wan_base_url, чтобы ваш плагин знал, куда обращаться в ответ.

Ваш плагин. Отдавайте его с origin, который вы только что разрешили, например http://localhost:8788, и принимайте в ответ этот базовый URL: у эталонного плагина по той же самой причине есть собственные ENVIRONMENT=development и DEE_WAN_DEV_ORIGINS=http://localhost:8787. Для Worker:

Окно терминала
cd plugins/scheduling
npx wrangler d1 migrations apply dee-wan-scheduling --local
npx wrangler dev --port 8788 --test-scheduled

Затем в админ-интерфейсе: Settings → Plugins → install, с http://localhost:8788/dee-wan/manifest.json и одноразовым кодом, который выпустил ваш плагин. Выдайте набор возможностей и выберите модели. Откройте элемент контента и воспользуйтесь действием, которое объявил ваш манифест.

Выполняйте установку через UI, а не вручную: административные маршруты требуют настоящей сессии, заголовка сайта и site:plugins, а у UI уже есть все три.

Две локальные ловушки, обе реальные:

  • Cookie сессии __Host-/Secure требует HTTPS или ровно localhost — не 127.0.0.1.
  • Cron-триггер не срабатывает в wrangler dev, если вы об этом не попросите. С --test-scheduled команда curl "http://localhost:8788/__scheduled?cron=*+*+*+*+*" выполняет один тик.

Как намеренно получить каждый исход. Левый столбец — что ваш тест делает с фейковым ядром; правый — как то же состояние возникает с реальным, когда вы хотите подтвердить его от начала до конца.

Исход С фейковым ядром С локальным Dee Wan
invalid_request Отправить неизвестный ключ в теле или опустить Idempotency-Key То же самое
plugin_unauthorized Использовать неизвестный токен Отключить или удалить инсталляцию, затем дождаться наступления срока записи
capability_refused Убрать возможность из инсталляции Сузить грант в Settings → Plugins
requester_unauthorized Пометить человека из делегирования как больше не имеющего прав Убрать роль человека или деактивировать его после создания записи
content_not_found Запросить неизвестный экземпляр Использовать id с другого сайта или убрать модель из гранта
scheduled_target_stale Изменить current_version_id контента Отредактировать и сохранить черновик после создания записи
workflow_conflict Увеличить revision контента Опубликовать или снять с публикации элемент вручную после создания записи
dependent_content Ответить dependent_content через внедрение Связать с ним другой опубликованный элемент, затем попытаться снять с публикации
idempotency_key_reused Повторно использовать ключ с другим телом То же самое
transition_unavailable Запросить to, которого состояние не предлагает Попросить снять с публикации то, что не опубликовано
rate_limited Внедрить 429 с Retry-After — в секундах и в виде HTTP-даты 120 запросов за минуту для одной инсталляции
temporary_failure Внедрить 500, 502, 503 и 504 Трудно вызвать; доверьтесь внедрённому случаю
транспортный сбой Внедрить сетевую ошибку и таймаут Остановить Worker Dee Wan посреди выполнения
недоверенный ответ Внедрить 200, у которого outcome.code — код для 409, тело не в JSON и 3xx Не воспроизводится; в этом и смысл внедрения
восстановление аренды Оставить запись в running с истёкшей арендой Убить плагин посреди команды и дать следующему тику забрать её

Для каждого отказа проверяйте две вещи: запись завершилась под правильным именем, и ничего не было отправлено дважды — второе доказывается списком записанных команд.

Что покрывает собственный набор тестов ядра

Заголовок раздела «Что покрывает собственный набор тестов ядра»

Запуск из backend/:

Окно терминала
npx vitest run test/plugin-manifest.test.ts test/plugin-installation.test.ts \
test/plugin-transition.test.ts test/plugin-schema.test.ts test/plugin-contract.test.ts \
test/plugin-docs.test.ts
Файл Что он фиксирует
plugin-manifest.test.ts Парсер манифеста, политику URL, лимит размера тела, форму токена и хеширование
plugin-installation.test.ts Установку, сбой активации, отказ при редиректе, машину состояний, перекрытие и истечение при ротации, сужение гранта, границу запроса, rate limit, однократное использование и истечение кода запуска
plugin-transition.test.ts Публикацию на всю группу, атрибуцию плагину, повтор, конкурентность, повторное чтение полномочий и каждый именованный терминальный отказ
plugin-schema.test.ts Опубликованные схемы согласуются с написанными вручную проверками времени выполнения, случай за случаем
plugin-contract.test.ts Копия протокола в эталонном плагине согласуется с копией ядра
plugin-docs.test.ts Эти четыре документа: сгенерированный файл совпадает, и каждый пример — это то, что отвечают живые маршруты
  • Опубликованного пакета контракта нет. SPEC-PLUGINS.md §14 фиксирует тот же пробел. Сегодня существуют две JSON Schema, фейковое ядро в plugins/scheduling/test/helpers/ и тест на стороне ядра, который держит таблицу протокола эталонного плагина честной. Плагин в другом репозитории копирует фейковое ядро и валидирует по схемам; пока он не может выполнить npm install набора тестов на соответствие.
  • Межсервисный приёмочный прогон из §16 здесь не автоматизирован. Набор тестов эталонного плагина и набор тестов ядра используют дублёр для другой стороны. Запуск обоих процессов друг против друга — это описанная выше локальная конфигурация, управляемая вручную.
  • В эти файлы не входит ни один браузерный сценарий. Экранные гейты для UI Plugins ядра — это политика репозитория (SPEC.md §12 D-S) и относятся к оркестратору, а не к автору плагина.