Skip to main content
Вы можете написать автоматизированные тесты для мода и запустить их из оболочки с помощью claude plugin test. Тест вызывает события, которые обрабатывают ваши хуки, и проверяет, что сделали хуки, чтобы вы поймали проблему до того, как она попадёт в сессию. Первый пример тестирует мод из раздела Создание мода.

Напишите тест

Тест загружает ваш мод, отправляет события через его hooks так, как это делал бы Claude Code, и проверяет, что сделали hooks, без сеанса, входа или сети. Вы запускаете тесты из своей оболочки с помощью claude plugin test, и каждый файл теста импортирует набор для тестирования, библиотеку тестирования в модуле claude-code/testing. Дайте каждому файлу теста имя, заканчивающееся на .test.ts, например first-mod.test.ts, и сохраните его где угодно в директории плагина. Каждый файл теста должен содержать по крайней мере один test(), иначе запуск завершится с ошибкой declares no test(): nothing ran. Файл теста может импортировать собственные файлы вашего мода и вспомогательные файлы .ts соседних уровней, поэтому вы можете модульно тестировать простые функции, такие как правила игры, без набора. Этот тест выполняет два вызова инструментов, запускает команду /tally из Create a mod и проверяет, что ответ учитывает оба. Его первая строка — это stub, который отвечает на вызовы инструментов вместо 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, который получает хук. Каждый из его методов вызывает событие с тем же именем, отправляет его через хуки вашего мода и разрешается в результат: $.tool.call({ tool: 'Bash', command: 'ls' }) вызывает tool.call. $.command.run, $.prompt.submit, $.session.start и $.turn.complete работают так же, а $.classic.Stop и другие методы $.classic вызывают событие хука настроек. Тест не может напрямую вызвать метод mods API, такой как ui.close. Вызовите его через ваш мод, например нажав кнопку, которая закрывает панель.
  • on: вызовите её для регистрации stub, которые являются hooks, отвечающими в место Claude Code. Назовите stub для вызова mods API без $., поэтому stub, зарегистрированный как store.get, отвечает на $.store.get вашего мода. Когда ваш мод вызывает $.model.complete или $.store.get, stub предоставляет ответ.
Этот пример stub вызова модели. Hook принадлежит моду с именем grader и обрабатывает команду /grade, которая отправляет предложение модели и сообщает, начинается ли ответ с PASS. Файл содержит только hook под тестом, поэтому моду также нужны plugin.json и hooks.json, как в Create a mod. Чтобы ввести /grade в сеансе, моду также нужно зарегистрировать команду:
grader/hooks/register.js
Этот тест stub вызова модели для проверки того, что делает hook с проходящим ответом:
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 для часов, хранилища, переменных окружения и строк, добавляемых в диалог:
  • mock.clock(on): отвечает на $.clock и возвращает mock часов, которые ваш тест продвигает, поэтому тест таймера не ждёт.
  • mock.store(on, { count: 7 }): отвечает на $.store из хранилища, которое начинается с этих записей. Он ничего не возвращает, поэтому чтобы проверить, что сохранил ваш мод, напишите два store stub сами, как это делает drawing test.
  • mock.env(on, { CI: 'true' }): отвечает на $.env.get из этих переменных.
  • mock.session(on): возвращает mock сессии, чей метод appended() перечисляет строки, добавленные вашим модом с помощью $.session.append, начиная с самых старых; требуется Claude Code v2.1.293 или новее.

Следуйте правилам набора для тестирования

Набор для тестирования имеет несколько собственных правил, и нарушение одного из них производит ошибки, которые встречают первые авторы тестов:
  • Зарегистрируйте каждый stub перед первым вызовом теста на $. Вызов on после этого выбрасывает ошибку, такую как on("ui.render") after the test first called $.
  • session.start не запускается сам по себе. Каждый тест начинается со свежезагруженного модуля, ни один из хуков которого ещё не вызван, поэтому переменные уровня модуля содержат свои начальные значения. Если хук зависит от того, что настраивает session.start, сначала вызовите это событие:
    Второй stub отвечает на вызов $.command.register, который делает hook session.start, такой как tutorial’s. Без него этот вызов отклоняется с no implementation for command.register и набор пропускает ваш hook, поэтому ничего после вызова в hook не запускается. Тест не завершается неудачей в этой точке. Пропущенный hook указан под the engine reported: только если позже проверка завершится неудачей.
  • Hook, который возвращает next(e), нуждается в stub для ответа. Когда ваш ui.render hook возвращает next(e), например чтобы ничего не рисовать, пока Claude неактивен, mounting it завершается неудачей с no implementation for ui.render. Зарегистрируйте stub, который возвращает элемент как простые данные:
    С зарегистрированным stub монтирование успешно, и ui.find({ type: 'Text' }) возвращает этот элемент всякий раз, когда ваш hook возвращал next(e).
  • Stub для turn.step — это асинхронный генератор, и тест читает поток до конца, чтобы получить результат:
    Когда цикл заканчивается, result — это объект, который вернул stub, после того как ваш hook turn.step имел возможность его изменить. Здесь result.answer — это 'ok'.
  • Выполняйте вызов инструмента, передавая имя инструмента и аргументы как поля, например await $.tool.call({ tool: 'Bash', command: 'ls' }), и зарегистрируйте stub tool.call, который возвращает { result }.

Посмотрите, что возвращает stub

Каждый вызов mods API, который ваш мод делает в тесте, нуждается в stub, который отвечает вместо Claude Code, кроме нескольких, на которые набор отвечает сам: вызовы $.ui.invalidate, $.state и $.session.append. Для вызовов $.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 перед любым из них. Когда expect не выполняется внутри stub или хука, который вы передали в on как обычную функцию, а не как асинхронный генератор, тест не проходит. Движок пропускает этот хук, а вывод ошибки называет его, например in the test's store.set hook.

Тестируйте таймер

Мод, который запускает работу на таймере, нуждается в 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
Тест проходит, когда ваш hook 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 к нему.
Этот файл теста загружает policy mod со страницы admin первым. Он проверяет, что policy mod отказывает моду, который запускает процесс, и допускает тот, который не запускает:
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