Перейти к содержимому
Версия 1.0 ещё не выпущена; проект находится на стадии pre-MVP. См. возможности и ограничения версии 1.0.

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

Это руководство объясняет, что нужно проверить перед работой плагина с реальным контентом: как использовать тестовое ядро в своём наборе тестов, запустить оба сервиса на одной машине и намеренно воспроизвести каждый сбой.

Проверяемые операции описаны в Protocol v1, а требования, соблюдение которых подтверждают эти тесты, — в разделе Безопасность плагинов.

Каждый плагин должен проверять следующие сценарии. Они относятся к свойствам протокола, а не к особенностям конкретного плагина, поэтому должны входить в ваш набор тестов независимо от его назначения.

Случай Что должно произойти
Активация Одноразовый код расходуется ровно один раз. Неизвестный, использованный или просроченный код отклоняется. Сырой токен хранится зашифрованным и никогда не логируется. Обработчик отвечает с хорошим запасом до 5 секунд и сам не делает вызовов протокола
Ротация kind: rotate заменяет сохранённый токен. Старый перестаёт использоваться каждым воркером. Отклонённая ротация оставляет старый токен на месте
Запуск Код обменивается один раз, через бэкенд плагина. Второй обмен не удаётся. Код, выпущенный для другой инсталляции, не проходит. Страница управления отправляет Referrer-Policy: no-referrer и убирает dee_wan_launch из URL после обмена
Сессия Браузерная сессия плагина создаётся при обмене, хранится в его собственном cookie и не содержит учётных данных Dee Wan
Закрепление Публикация закрепляет current_version_id, снятие с публикации закрепляет published_version_id, оба закрепляют workflow.revision, а выбранная пара (kind, to) получена из делегированного чтения
Идемпотентность Ключ выводится из записи и её ревизии, одинаков при каждом повторе и меняется, когда запись перепланируется
Не более одного раза Доставку начинает только текущий владелец аренды. Срок выполнения запроса должен истечь раньше срока аренды. Восстановление может повторить запрос с теми же ключом и телом. Ядро применяет эффект не более одного раза в пределах хранения ключа. Таймаут не доказывает остановку удалённого запроса
Повтор 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/429 и temporary_failure/503, ответы 429/500/502/503/504 без outcome и сетевые сбои/таймауты повторяются в пределах лимитов попыток И возраста. Соблюдайте допустимый ограниченный Retry-After. Иначе используйте backoff с джиттером
Недоверенные ответы Присутствующий, но некорректный outcome, неизвестный код, неверный флаг retry или несовпадение статуса и кода терминальны даже на повторяемом статусе. Тело без outcome терминально вне перечисленных резервных статусов. Перенаправления не выполняются
Изоляция Каждая таблица и каждый запрос ограничены инсталляцией. Одна инсталляция не может читать, отменять или выполнять записи другой
Отзыв После удаления ожидающее задание завершается с plugin_unauthorized. Плагин сообщает об отказе и не повторяет запрос
Секреты Ни одна тестовая фикстура, строка журнала, ответ с ошибкой или снимок экрана не содержит токена, кода запуска или кода активации
Запись от имени пользователя Сохранение полей, работа с черновиком, загрузка медиа или создание предложения указывает делегирование пользователя, от имени которого выполняется операция, фиксирует прочитанную версию или ревизию черновика и передаёт Idempotency-Key. Конфликты version_conflict и draft_conflict требуют решения пользователя, а не автоматического повтора

Исходный код плагина Scheduling (plugins/scheduling в репозитории Dee Wan) содержит набор тестов, который является рабочим примером каждой строки, в test/activation.test.ts, launch.test.ts, schedules.test.ts, runner.test.ts, recovery.test.ts, time.test.ts, isolation.test.ts и flow.test.ts. Файл test/conformance.test.ts проверяет плагин на сценариях протокола из @dee-wan/plugin-conformance.

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

@dee-wan/plugin-conformance — устанавливаемый набор тестов протокола v1. Его исходный код — packages/plugin-conformance в репозитории Dee Wan, где он собирается и тестируется. Он ЕЩЁ НЕ опубликован в реестре npm; публикация требует полномочий. Пока это так, плагин в другом репозитории собирает его из checkout Dee Wan и устанавливает tarball:

Окно терминала
node scripts/install-dependencies.mjs # from the Dee Wan checkout root
npm --prefix packages/plugin-conformance run build
cd packages/plugin-conformance
npm pack # writes dee-wan-plugin-conformance-0.1.0.tgz
cd /path/to/your-plugin
npm install --save-dev /path/to/dee-wan-plugin-conformance-0.1.0.tgz

В пакет включены опубликованные схемы, поэтому при выполнении он ничего не читает из рабочей копии Dee Wan. Он экспортирует:

Экспорт Что это
createFakeCore() Реализация fetch, которая играет роль ядра Dee Wan для одной или нескольких инсталляций
runConformance({ describe, it }, subject) Регистрирует случаи протокола в vitest, node:test или jest
conformanceCases(subject) Те же случаи как обычные асинхронные функции
validateManifest(value), validateShape(name, value) Валидация по опубликованной схеме манифеста и по $defs протокола
OUTCOMES, CAPABILITIES, TRANSITION_KINDS, PROTOCOL_VERSIONS, INBOUND_RETRY_STATUSES, SECRET_PATTERNS Опубликованные данные протокола

Тестовое ядро:

  • аутентифицирует bearer-токен для каждой инсталляции и иначе отвечает plugin_unauthorized
  • отвечает на /context, /content/:id и /launch/exchange телами, соответствующими структуре протокола, включая блок workflow и переходы, которые разрешают возможности инсталляции и делегирование
  • проверяет выданные установке плагина возможности и доступ к моделям и, как ядро, отклоняет делегирование, которое не разрешает доступ к записи
  • отклоняет тело команды, не являющееся опубликованным transition_command, с invalid_request
  • проверяет закрепление и ревизию workflow в команде и отвечает scheduled_target_stale или workflow_conflict, когда они больше не выполняются, и dependent_content для элемента, помеченного как имеющий зависимые
  • хранит запись идемпотентности для каждого ключа, так что точный повтор отвечает idempotent_replay со всем первым результатом, а повторно использованный ключ с другим вводом отвечает idempotency_key_reused
  • сообщает redirect в каждом результате публикации, из поля redirect элемента, и не сообщает при снятии с публикации
  • перемещает один язык, когда команда называет locale, а у инсталляции есть workflow:locale, и перечисляет каждый язык в workflow.locales для этой инсталляции
  • расходует код запуска один раз и отклоняет его для другой инсталляции
  • выпускает конверты вызова через createInvocation и расходует код вызова на /invocations/exchange один раз, только для инсталляции, для которой он выпущен, и до истечения срока
  • обрабатывает маршруты записи — PUT /content/:id, PUT /drafts, POST /drafts/discard, GET /drafts, POST /media, POST и GET /content/:id/proposals, GET /proposals/:id — по опубликованным структурам запросов и ответов; проверяет возможности, выданные права, делегирование, закреплённую версию и ревизию черновика, поддерживает повтор по ключу; значения полей заменяют версию записи, черновик принадлежит делегировавшему пользователю, а предложение ничего не меняет
  • закрепляет публикацию одного языка за текущей версией группы, а снятие с публикации одного языка — за собственной живой версией этого языка, как это делает ядро
  • внедряет сбои по запросу — сетевую ошибку, ответ, потерянный после применения команды, любой статус, любой outcome.code, заголовок Retry-After или намеренно некорректное тело
  • записывает запросы отдельно от применённых эффектов, так что восстановление может повторно отправить тот же ключ, а эффект происходит не более одного раза.

Постраничный список /content оно не обслуживает.

runConformance принимает subject: ваш манифест, вашу копию данных протокола, ваш классификатор ответов и необязательные адаптеры для активации, обмена кодом запуска, доставки, вызова операций (обязателен, если манифест объявляет operations) и вызова источника медиа (mediaSource, обязателен, если манифест объявляет media_sources). Плагин, который не отправляет команд, например источник медиа, не передаёт копию протокола и классификатор. Каждый адаптер запускает свежий экземпляр вашего плагина, подключённый к тестовому ядру. Минимальный файл vitest:

import { runConformance } from '@dee-wan/plugin-conformance';
import { describe, it } from 'vitest';
import { subject } from './conformance-subject';
runConformance({ describe, it }, subject);

Набор проверяет соответствие опубликованному протоколу: таблицу исходов, HTTP-статусы для повторов, возможности, виды переходов, версию протокола и форматы секретов. Манифест должен проходить валидацию. Каждый код исхода, неверный статус, неверный флаг повтора, некорректный outcome и статус без outcome должны классифицироваться по правилам протокола. Проверяется одноразовость кодов активации и запуска, а также привязка кодов ротации к своей установке плагина.

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

Для каждой объявленной операции плагин должен отвечать только после однократного обмена кодом. Ответ должен соответствовать опубликованной схеме и содержать только объявленные компоненты результата. Ответ completed недопустим для отклонённого кода, кода другой установки плагина или конверта с неизвестным ключом. Для каждого объявленного столбца списка контента проверяется, что обмен не создаёт делегирования, а ответ содержит один блок cells только для запрошенных строк, по одной ячейке на строку. Каждая ячейка отображается в объявленном виде cell: url допустим только у ссылки, tone — только у метки.

Для каждого объявленного источника медиа ответ на просмотр допустим только после обмена. Все URL превью и скачивания должны находиться на origin манифеста, а все URL выдачи файлов — на объявленном origin выдачи. Объявление должно указывать свои modes и содержать delivery_origins тогда и только тогда, когда объявлен reference. Его next_cursor должен вести на страницу без повторных элементов. При запросе элемента из списка должен возвращаться именно этот элемент. Ответ completed недопустим для отклонённого кода или кода другой установки плагина. Полная реализация subject есть в test/conformance.test.ts плагина Scheduling; реализация для источника медиа — в plugins/media-source-reference/test/conformance.test.ts.

У тестового ядра два ограничения: оно не заменяет настоящее ядро и поддерживает только процесс из двух состояний — draft и published. Проверяйте outcome.code и HTTP-статус, а не текст сообщения: сообщения ядра предназначены для людей, и их формулировки могут меняться.

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

Подготовьте локальную ссылку на пакет по инструкции разработки Scheduling, затем запустите набор тестов эталонного плагина:

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

Внутри репозитория Dee Wan его инструменты берутся из установки в корне репозитория (node scripts/install-dependencies.mjs в корне), а @dee-wan/plugin-conformance подключается из packages/plugin-conformance: сначала выполните там npm run build.

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

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

Особенно важно проверять таблицу outcomes. Если плагин хранит собственную копию этой таблицы, сравнивайте её с опубликованной: расхождение статуса или флага повтора должно останавливать сборку, а не проявляться во время работы. Эталонный плагин хранит такую копию в src/protocol.ts, поскольку не может импортировать её из ядра. Проверки «protocol copy» в runConformance выполняют это сравнение.

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

Вам нужны версия Node, закреплённая в .nvmrc в репозитории Dee Wan, openssl и эти порты: 8787 (admin Worker Dee Wan), 5173 (admin UI) и 8788 (плагин).

Порт 8788 — также порт по умолчанию для npm run local:public, публичного Worker чтения Dee Wan. Если вы запускаете и его, дайте одному из них другой порт: npm run local:public -- --port 8789, либо запустите плагин на другом порту и используйте этот порт везде, где ниже встречается 8788.

В backend/.dev.vars в репозитории Dee Wan (создайте его, если он не существует) добавьте origin плагина:

PLUGIN_DEV_ORIGINS=http://localhost:8788

Это единственная настройка, которую вы добавляете. Конфигурация локального admin Worker уже задаёт ENVIRONMENT=development, благодаря чему PLUGIN_DEV_ORIGINS вообще читается, и BACKEND_URL=http://localhost:8787 — именно его ядро отправляет вашему плагину как dee_wan_base_url. PLUGIN_DEV_ORIGINS — список разрешённых точных origin, без шаблонов и без сокращений для приватных диапазонов, — проверяемый для URL манифеста, базового URL, URL активации и управления. Без этой записи установка с http://localhost:8788 отклоняется как url_not_https.

Затем из корня репозитория запустите каждую долгоработающую команду в отдельном терминале:

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

Для эталонного плагина, из корня репозитория:

Окно терминала
cd plugins/scheduling
cp wrangler.example.jsonc wrangler.jsonc

Заглушка database_id в скопированном файле допустима: --local никогда не обращается к Cloudflare, поэтому шага wrangler d1 create нет. Рядом с ним создайте .dev.vars:

ENVIRONMENT=development
PUBLIC_ORIGIN=http://localhost:8788
DEE_WAN_DEV_ORIGINS=http://localhost:8787
ADMIN_SECRET=local-admin-secret-0123456789
TOKEN_KEY=<the output of: openssl rand -base64 32>
  • .dev.vars переопределяет production-значения из wrangler.jsonc для wrangler dev.
  • DEE_WAN_DEV_ORIGINS — зеркальное отражение PLUGIN_DEV_ORIGINS на стороне плагина: единственный базовый URL Dee Wan без HTTPS, который он принимает, и только когда ENVIRONMENT=development.
  • ADMIN_SECRET — bearer оператора для маршрутов плагина /admin/*, длиной не менее 16 символов.
  • TOKEN_KEY — base64 от 32 случайных байт. Шифрует сохранённые токены инсталляций при хранении. Потеряете его — придётся ротировать каждую инсталляцию.
Окно терминала
npx wrangler d1 migrations apply dee-wan-scheduling --local
npx wrangler dev --port 8788 --test-scheduled # leave running
curl http://localhost:8788/dee-wan/manifest.json # the manifest names http://localhost:8788

Выпустите одноразовый код активации:

Окно терминала
curl -X POST -H "Authorization: Bearer local-admin-secret-0123456789" \
http://localhost:8788/admin/activation-codes

Он отвечает 201 с { "code", "purpose", "installation_id", "expires_at" }: purpose равен activate, installation_id равен null, а expires_at — на 15 минут вперёд, в миллисекундах от начала эпохи.

Вашему собственному плагину нужны те же три вещи: отдавайте его на разрешённом вами origin, принимайте http://localhost:8787 как базовый URL только в разработке и имейте способ выпустить код активации.

Войдите в admin UI по адресу http://localhost:5173 с учётной записью, которую вывел local:seed, затем откройте Настройки → Плагины: проверьте http://localhost:8788/dee-wan/manifest.json, выдайте набор возможностей, выберите модели и установите с кодом. Откройте запись модели, к которой плагину предоставлен доступ, и используйте действие, объявленное вашим манифестом.

Устанавливайте плагин через интерфейс: административным маршрутам нужны действительная сессия, заголовок сайта и разрешение site:plugins. Интерфейс уже передаёт всё необходимое.

Две особенности локального запуска:

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

Эталонный плагин и ядро запускаются друг против друга, оба вживую, одной командой. С версией Node, закреплённой в .nvmrc, подготовьте checkout Dee Wan, затем запустите скрипт плагина:

Окно терминала
node scripts/install-dependencies.mjs
npm run build:packages
npm --workspace backend run prisma:generate
cd plugins/scheduling
npm run test:cross-service

Тест живёт в плагине, в test/cross-service/cross-service.test.mjs. Вне репозитория Dee Wan задайте DEE_WAN_CORE_DIR как подготовленный checkout Dee Wan. Ему не нужны аккаунт Cloudflare, учётные данные и сеть за пределами 127.0.0.1. Он занимает две-три минуты, потому что назначает выполнение на конкретную минуту и дожидается её наступления.

Тест запускает следующие сервисы, для каждого выбирает свободный порт, а затем останавливает их и удаляет свой временный каталог:

  • admin Worker Dee Wan под wrangler dev, на свежей локальной D1 с применёнными миграциями, с PLUGIN_DEV_ORIGINS, в котором указан плагин.
  • плагин Scheduling под wrangler dev --test-scheduled, из сгенерированной копии wrangler.example.jsonc, на свежей локальной D1 с применёнными миграциями.
  • HTTP-прокси в процессе теста. На него указывает BACKEND_URL ядра, поэтому каждый вызов плагина к ядру проходит через него. Он пересылает запросы и ответы без изменений, кроме заголовка Host. Он может отбросить запрос до пересылки или отбросить ответ после того, как ядро ответило, и записывает каждую увиденную команду перехода. Это сеть, а не дублёр: каждый ответ приходит от ядра.

Всё проходит через настоящие маршруты: активация установки и вход, вызовы /api/plugins из раздела «Настройки → Плагины», делегированные запуски, собственные HTML-формы плагина и тики /__scheduled. Он проверяет:

Случай Что он проверяет
Активация Установка через ядро активирует плагин. Повторное использование кода активации отклоняется с activation_refused
Делегированный запуск Каждое расписание создаётся через POST /api/plugins/:id/launch, обмен плагина и его форму. Подтверждение показывает локальное время, его часовой пояс и момент в UTC
До срока Тик до назначенной минуты ничего не отправляет
Публикация, ответ потерян Ядро применяет публикацию, прокси отбрасывает ответ, плагин повторяет с тем же ключом и побайтно одинаковым телом, и ядро отвечает idempotent_replay с теми же данными результата, что и в потерянном ответе, включая redirect. Одно событие workflow плагина, приписанное инсталляции и делегировавшему человеку
Публикация, запрос потерян Прокси отбрасывает первый запрос, не переслав его. Плагин повторяет, и ядро отвечает transition_applied
Снятие с публикации Закреплённая опубликованная версия снимается с публикации
scheduled_target_stale Черновик отредактирован после закрепления
workflow_conflict Элемент опубликован и снят с публикации вручную после закрепления
transition_unavailable Элемент опубликован вручную после того, как была закреплена публикация
dependent_content Опубликованный элемент связан с тем, который снимается с публикации
Отмена Отменено в UI плагина. Ядро не вызывается ни разу
Изоляция Сессия одной инсталляции не может прочитать или отменить расписание другой инсталляции
Отзыв Ядро удаляет инсталляцию с ожидающим расписанием. Плагин завершает его с plugin_unauthorized
Завершённое остаётся завершённым Каждый отклонённый или отменённый случай оставляет состояние ядра, ревизию и указатель публикации без изменений, а последующий тик ничего не отправляет

CI Dee Wan его не запускает. Собственный CI плагина, .github/workflows/ci.yml в каталоге плагина, запускает его против checkout Dee Wan, когда у плагина появится свой репозиторий. До тех пор запускайте его вручную, когда изменение затрагивает любую из сторон protocol v1.

frontend/cypress/e2e/real-scheduling-releases.cy.ts проходит страницы релизов плагина в браузере, против настоящего admin Worker и настоящего плагина. Раннер e2e (frontend/scripts/run-e2e.mjs) запускает плагин для прогона с E2E_REAL_BACKEND=1, который может дойти до этого spec: весь набор или --spec, совпадающий с ним. E2E_SCHEDULING_PLUGIN=1 включает его принудительно, 0 выключает.

Окно терминала
E2E_REAL_BACKEND=1 npm --prefix frontend run test:e2e -- --spec cypress/e2e/real-scheduling-releases.cy.ts

Что делает раннер, чтобы вы не повторяли настройку выше вручную:

  • генерирует wrangler.jsonc и .dev.vars плагина во временный каталог, которым владеет сам, из wrangler.example.jsonc, с абсолютными main и migrations_dir и заданными переменными (PUBLIC_ORIGIN, ENVIRONMENT=development, DEE_WAN_DEV_ORIGINS = admin Worker этого прогона). Ничего не пишется в plugins/scheduling/. Каталог удаляется, когда прогон останавливается, а каталог, оставленный убитым прогоном (его сокет владельца /tmp/dwe2e-<suffix>.sock, названный по каталогу, больше никто не слушает), удаляется, когда следующий прогон запускает плагин;
  • применяет миграции плагина к свежей локальной D1 в этом каталоге, затем запускает его через wrangler dev --test-scheduled;
  • запускает admin Worker с PLUGIN_DEV_ORIGINS, в котором указан плагин;
  • генерирует ADMIN_SECRET и TOKEN_KEY для каждого прогона и передаёт spec origin и admin secret как CYPRESS_E2E_PLUGIN_ORIGIN и CYPRESS_E2E_PLUGIN_ADMIN_SECRET. Spec выпускает свой код активации этим секретом, устанавливает через POST /api/plugins и открывает каждую страницу плагина по URL с кодом из POST /api/plugins/:id/launch, то есть тем вызовом, который делают кнопки запуска плагина в настройках и в редакторе записи.

Порт плагина — E2E_PLUGIN_PORT (по умолчанию 43190). Если он занят, прогон сразу отказывает, как и для каждого другого порта, который использует прогон. Раннер следит за плагином: если тот падает во время живого прогона в браузере, прогон останавливается и называет его.

Ниже описано, как намеренно воспроизвести каждый сбой. В колонке тестового ядра указаны действия вашего теста, а в колонке локального Dee Wan — способ получить то же состояние в настоящем ядре для проверки всего сценария.

С ними встречается каждый плагин, что бы он ни делал. Каждый имеет собственный outcome.code с фиксированным HTTP-статусом и флагом повтора.

Исход На тестовом ядре На локальной установке Dee Wan
invalid_request Отправьте неизвестный ключ тела или опустите Idempotency-Key То же
plugin_unauthorized Используйте неизвестный токен Отключите или удалите инсталляцию, затем отправьте команду
capability_refused Уберите возможность у инсталляции Ограничьте выданные права в разделе Настройки → Плагины
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 Внедрите структурированный ответ 503, затем пропустите повтор Трудно вызвать. Ядро также отвечает так, пока исход перенаправления публикации ещё не записан; повтор с тем же ключом возвращает весь результат. Доверьтесь внедрённому случаю

У них нет outcome.code, поэтому протокол фиксирует только правило — никогда не считайте такой сбой успехом и никогда не повторяйте бесконечно. Как их записывать — решение вашего плагина. Последняя колонка — то, что делает эталонный плагин.

Сбой На тестовом ядре На локальной установке Dee Wan Эталонный плагин
временный HTTP без outcome Внедрите 429, 500, 502, 503 и 504 без тела outcome Трудно вызвать. Доверьтесь внедрённым случаям Повторяет с ограниченным backoff или допустимым ограниченным Retry-After
транспортный сбой Внедрите сетевую ошибку и таймаут Остановите Worker Dee Wan, затем дайте плагину отправить команду Повторяет как no_response, с задержкой и в пределах своих лимитов
недоверенный ответ Внедрите 200, чей outcome.code — код 409, тело не JSON и 3xx Не воспроизводится. В этом и смысл внедрения Отказывает как untrusted_response
восстановление аренды Оставьте запись в running с истёкшей арендой Убейте плагин посреди команды и дайте следующему тику её забрать Отправляет повторно с тем же ключом идемпотентности, в пределах лимита опоздания

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

  • Пакет контракта не опубликован. @dee-wan/plugin-conformance собирается и тестируется в репозитории Dee Wan. Публикация в npm требует полномочий. До тех пор плагин в другом репозитории устанавливает tarball, созданный npm pack.
  • Межсервисный прогон покрывает случаи из своей таблицы и ничего больше. Он не проверяет requester_unauthorized, capability_refused, rate_limited, ротацию токена, retry_exhausted, падение плагина посреди команды или cron-триггер, который срабатывает сам. На каждой стороне они проверяются с помощью заглушки либо вручную в локальной среде, описанной выше. CI Dee Wan его не запускает, а собственный CI плагина запустится, только когда у плагина будет свой репозиторий.
  • Ничто здесь не является свидетельством развёртывания. Оба сервиса работают под локальным wrangler dev.

Стабильная версия ещё не выпущена (pre-MVP)
Коммит исходного кода: e207e63239a627fbd2ae9c414ef7c6eb19fcfe5a (с локальными изменениями)
Дата сборки (UTC): 2026-10-21T09:28:24.214Z
Коммит для обновления: не определён: в рабочей копии есть изменения. Примечания к выпуску