claude plugin test. Тест вызывает события, которые обрабатывают ваши hooks, и проверяет, что сделали hooks, чтобы вы поймали проблему до того, как она попадёт в сеанс. Первый пример тестирует мод из Create a mod.
Напишите тест
Тест загружает ваш мод, отправляет события через его hooks так, как это делал бы Claude Code, и проверяет, что сделали hooks, без сеанса, входа или сети. Вы запускаете тесты из своей оболочки с помощьюclaude plugin test, и каждый файл теста импортирует набор для тестирования, библиотеку тестирования в модуле claude-code/testing.
Дайте каждому файлу теста имя, заканчивающееся на .test.ts, например first-mod.test.ts, и сохраните его где угодно в директории плагина. Каждый файл теста должен содержать по крайней мере один test(), иначе запуск завершится с ошибкой declares no test(): nothing ran. Файл теста может импортировать собственные файлы вашего мода и вспомогательные файлы .ts соседних уровней, поэтому вы можете модульно тестировать простые функции, такие как правила игры, без набора.
Этот тест вызывает два tool call, запускает команду /tally из Create a mod, и проверяет, что ответ считает оба. Его первая строка — это stub, который отвечает на tool call в место Claude Code. Сохраните его как first-mod/tests/first-mod.test.ts:
first-mod/tests/first-mod.test.ts
first-mod:
$.tool.call прошёл через hook tool.call вашего мода, который добавил один к его счётчику и передал вызов дальше stub. Никакой ls не запустился и никакой файл не был прочитан. $.command.run затем перешёл к hook command.run вашего мода, и answer — это объект, который вернул этот hook.
Команда выходит со статусом 1, когда тест не пройден, поэтому она работает в CI. Если ваши собственные моды не могут загружаться в оболочке, которая её запускает, она выводит строку, начинающуюся с claude plugin test: hooks modules are turned off с причиной и выходит со статусом 1.
Stub что Claude Code ответит бы
В тесте не запускаются модель, хранилище или инструмент, поэтому везде, где ваш мод ожидает ответ от Claude Code, тест предоставляет ответ с помощью stub. Функция теста получает два аргумента для этого:$: собственный$теста, который стоит на месте Claude Code. Это не mods API, который получает hook. Каждый из его методов вызывает событие с тем же именем, отправляет его через hooks вашего мода и разрешается в результат:$.tool.call({ tool: 'Bash', command: 'ls' })вызываетtool.call.$.command.run,$.prompt.submit,$.session.startи$.turn.completeработают так же, и$.classic.Stopи другие методы$.classicвызывают settings hook event. Тест не может напрямую вызвать mods API, такой какui.close. Вызовите его через ваш мод, например нажав кнопку, которая закрывает панель.on: вызовите её для регистрации stub, которые являются hooks, отвечающими в место Claude Code. Назовите stub для вызова mods API без$., поэтому stub, зарегистрированный какstore.get, отвечает на$.store.getвашего мода. Когда ваш мод вызывает$.model.completeили$.store.get, stub предоставляет ответ.
grader и обрабатывает команду /grade, которая отправляет предложение модели и сообщает, начинается ли ответ с PASS. Файл содержит только hook под тестом, поэтому моду также нужны plugin.json и hooks.json, как в Create a mod. Чтобы ввести /grade в сеансе, моду также нужно зарегистрировать команду:
grader/hooks/register.js
grader/tests/grader.test.ts
reply hook — это объект под value, чей text начинается с PASS. Чтобы проверить другую ветвь, добавьте второй тест, чей stub возвращает text, начинающийся с FAIL, и ожидайте Try again.
Stub для вызова mods API возвращает объект с полем value, которое содержит то, на что разрешается вызов в вашем моде: { value: 7 } делает $.store.get разрешённым в 7. Stub для одного из событий Claude Code, такого как turn.step или tool.call, возвращает результат этого события, такой как { result: 'ok' }. $.session.send и $.prompt.fill также принимают результат события, как показано в таблице. Look up what a stub returns показывает, какую форму принимает каждое общее имя. Две ошибки означают, что stub неправильный или отсутствует. Вывод неудачного теста включает блок с заголовком the engine reported:, и каждая ошибка появляется там:
returned neither { value } nor { deny }: stub для вызова mods API вернул простое значениеno implementation forс последующим именем: ваш мод сделал этот вызов и никакой stub не отвечает на него
mock.clock(on) отвечает на $.clock, mock.store(on, { count: 7 }) отвечает на $.store из хранилища, которое начинается с этих записей, и mock.env(on, { CI: 'true' }) отвечает на $.env.get из этих переменных. mock.clock возвращает mock часы, которые ваш тест продвигает, поэтому тест таймера не ждёт. mock.store ничего не возвращает, поэтому чтобы проверить, что сохранил ваш мод, напишите два store stub сами, как это делает drawing test.
Следуйте правилам набора для тестирования
Набор для тестирования имеет несколько собственных правил, и нарушение одного из них производит ошибки, которые встречают первые авторы тестов:-
Зарегистрируйте каждый stub перед первым вызовом теста на
$. Вызовonпосле этого выбрасывает ошибку, такую какon("ui.render") after the test first called $. -
session.startне запускается сам по себе. Каждый тест начинается с вашего модуля, свежезагруженного и ни один из его hooks не вызван, поэтому переменные уровня модуля содержат свои начальные значения. Если hook зависит от того, что устанавливаетsession.start, вызовите его первым:Второй stub отвечает на вызов$.command.register, который делает hooksession.start, такой как tutorial’s. Без него этот вызов отклоняется сno implementation for command.registerи набор пропускает ваш hook, поэтому ничего после вызова в hook не запускается. Тест не завершается неудачей в этой точке. Пропущенный hook указан подthe engine reported:только если позже проверка завершится неудачей. -
Hook, который возвращает
next(e), нуждается в stub для ответа. Когда вашui.renderhook возвращаетnext(e), например чтобы ничего не рисовать, пока Claude неактивен, mounting it завершается неудачей сno implementation for ui.render. Зарегистрируйте stub, который возвращает элемент как простые данные:С зарегистрированным stub монтирование успешно, иui.find({ type: 'Text' })возвращает этот элемент всякий раз, когда ваш hook возвращалnext(e). -
Stub для
turn.step— это асинхронный генератор, и тест читает поток до конца, чтобы получить результат:Когда цикл заканчивается,result— это объект, который вернул stub, после того как ваш hookturn.stepимел возможность его изменить. Здесьresult.answer— это'ok'. -
Вызовите tool call с именем инструмента и аргументами как полями, такие как
await $.tool.call({ tool: 'Bash', command: 'ls' }), и зарегистрируйте stubtool.call, который возвращает{ result }.
Посмотрите, что возвращает stub
Каждый вызов mods API, который ваш мод делает в тесте, нуждается в stub, который отвечает в место Claude Code, кроме нескольких, которые набор отвечает сам:$.ui.invalidate и $.state вызовы. Для вызовов $.clock используйте mock.clock(on), иначе $.clock.now() вашего мода завершится неудачей с no implementation for clock.now.
Эта таблица перечисляет те, которые моды используют чаще всего. Первый столбец — это вызов, который делает ваш мод, или событие, которое он передаёт с next(e). Второй — это функция для передачи on под этим именем, поэтому строка $.store.get становится on('store.get', ($, e) => ({ value: saved.get(e.key) })). '...' в stub отмечает текст для вас, чтобы заполнить:
expect имеет утверждения toBe, toEqual, toMatch, toMatchObject, toContain, toBeDefined, toBeUndefined и toThrow, и .not перед любым из них.
Тестируйте таймер
Мод, который запускает работу на таймере, нуждается в clock, который тест контролирует, поэтому тест может продвигать время вперёд вместо ожидания.const clock = mock.clock(on) возвращает mock clock, который начинается с 0 и движется только когда ваш тест его движет. Чтобы начать в другое время, передайте его в миллисекундах, как в mock.clock(on, { now: 5000 }). Clock имеет эти методы:
Этот hook принадлежит моду с именем
countdown и обрабатывает команду /countdown, которая принимает количество секунд, запускает таймер $.clock.every в одну секунду и показывает toast на нуле. Как с grader, файл содержит только тестируемый hook и не регистрирует команду:
countdown/hooks/register.js
/countdown 3 и движет mock clock, поэтому он проверяет три секунды поведения без ожидания трёх секунд:
countdown/tests/countdown.test.ts
expect показывает, что toast не приходит рано, и второй показывает, что он приходит один раз. Каждый advance разрешается после того, как таймеры, которые наступили, запустились, поэтому проверка на следующей строке видит их эффект.
Тестируйте рисунок
Тест может нарисовать один из render sites вашего мода, затем нажать, ввести текст в и найти элементы, которые он нарисовал.$.ui.mount рисует сайт через hook ui.render вашего мода и возвращает handle с методом для каждого из них. Чтобы охватить несколько приложений в одном тесте, установите surface на приложение для рисования. Этот тест открывает панель из Build a pane with tabs, переключает вкладки, нажимает кнопку и проверяет счётчик в терминале и приложении Desktop:
hello-tabs/tests/hello-tabs.test.ts
claude plugin test из директории hello-tabs. Тест проходит, когда оба приложения рисуют строку счётчика и мод сохранил 2. Счётчик переносится из первого приложения во второе, потому что оба mount используют один и тот же загруженный модуль.
Handle, который возвращает $.ui.mount, имеет эти методы, которые адресуют элементы по key, который вы им дали:
Каждый метод разрешается после того, как ваш обработчик завершился, поэтому вы можете проверить результат на следующей строке. Установите
props на то, что Claude Code передал бы для этого сайта. Таблица render sites перечисляет props каждого сайта, и типы для вашей сборки имеют их типы.
Тест рисунка проверяет дерево, которое возвращает ваш hook, и является ли оно действительным для этого приложения. Он не проверяет, как приложение его рисует, поэтому посмотрите новый макет в реальном сеансе также.
Тестируйте рисунок после /clear
Каждый тест начинается с каждого значения $.state на его значении по умолчанию, что то, как /clear их оставляет. Чтобы тестировать, что делает ваш мод дальше, пропустите session.start, вызовите classic.SessionStart с source: 'clear' и проверьте, что рисует ваш мод.
Этот тест проверяет модуль из Load a saved value again after /clear. Добавьте его в файл из Test a drawing, где определён PANE. Первый тест этого файла ожидает, что кнопка сохранит счётчик, как кнопка в Save from more than one session:
hello-tabs/tests/hello-tabs.test.ts
classic.SessionStart скопировал сохранённый 7 в $.state перед тем, как панель нарисовалась. Без этого hook в вашем модуле, панель рисует Count: 0, find возвращает undefined и тест завершается неудачей на toBeDefined.
Тестируйте мод, который судит другие моды
Мод, который ваша организация перечисляет вprependPlugins, может отказать другому моду перед его загрузкой. Чтобы тестировать один, установите уровень вашего мода и дайте тесту второй мод для вашего, чтобы допустить или отказать:
tier: вызовите его один раз в верхней части файла теста, как вtier('prepend'), чтобы загрузить ваш мод какprepend,appendилиbuiltin, его место в порядке запуска модов. Без него ваш мод загружается какuser.plugins: передайтеtestобъект опций перед телом теста. Его массивpluginsсодержит моды, которые вы пишете встроенными, каждый сnameи функциейregister. Чтобы загрузить один где-то кромеuser, добавьтеtierк нему.
acme-guard/tests/guard.test.ts
claude plugin test из директории acme-guard. Оба теста проходят с policy mod, как показано на странице admin.
Kit загружает каждый мод при первом вызове теста на $. Когда ваш мод отказывает одному, этот вызов выбрасывает, и сообщение называет отказанный мод, мод, который отказал, и вашу причину. Во втором тесте ничего не отказано, поэтому reader отвечает на вызов инструмента перед тем, как он достигнет stub.
Следующие шаги
- Troubleshoot a mod: узнайте, почему мод ничего не делает в сеансе
- Mods reference: каждое событие input и result для написания stubs