Тестирование плагина
Что плагин должен доказать, прежде чем его подпустят к реальному контенту, как доказать это против фейкового ядра в вашем собственном наборе тестов, как запустить оба сервиса на одной машине и как намеренно воспроизвести каждый сбой.
Поверхность, которая тестируется, описана в 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/schedulingnpm test # vitestnpm 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:pluginsbackend/test/plugin-docs.test.ts падает, если закоммиченные копии расходятся, поэтому команда нужна
только после изменения протокола.
Локальная конфигурация из двух сервисов
Заголовок раздела «Локальная конфигурация из двух сервисов»Оба сервиса на одной машине, без аккаунта Cloudflare и без реальных учётных данных.
Dee Wan. Из корня репозитория:
node scripts/install-dependencies.mjsnpm run local:migratenpm run local:seed # prints the sign-in it createsnpm 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=developmentPLUGIN_DEV_ORIGINS=http://localhost:8788BACKEND_URL=http://localhost:8787PLUGIN_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/schedulingnpx wrangler d1 migrations apply dee-wan-scheduling --localnpx 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) и относятся к оркестратору, а не к автору плагина.