Skip to main content
Você pode escrever testes automatizados para um mod e executá-los a partir do seu shell com claude plugin test. Um teste levanta os eventos que seus hooks tratam e verifica o que os hooks fizeram, para que você detecte um problema antes que ele chegue a uma sessão. O primeiro exemplo testa o mod de Create a mod.

Escrever um teste

Um teste carrega seu mod, envia eventos através de seus hooks da forma como Claude Code faria, e verifica o que os hooks fizeram, sem uma sessão, um login ou uma rede. Você executa testes a partir do seu shell com claude plugin test, e cada arquivo de teste importa o test kit, uma biblioteca de teste no módulo claude-code/testing. Dê a cada arquivo de teste um nome que termine em .test.ts, como first-mod.test.ts, e salve-o em qualquer lugar no diretório do plugin. Cada arquivo de teste precisa de pelo menos um test(), ou a execução falha com declares no test(): nothing ran. Um arquivo de teste pode importar seus próprios arquivos do mod e helpers .ts irmãos, para que você possa fazer testes unitários de funções simples, como as regras de um jogo, sem o kit. Este teste levanta duas chamadas de ferramenta, executa o comando /tally de Create a mod, e verifica se a resposta conta ambas. Sua primeira linha é um stub, que responde as chamadas de ferramenta no lugar do Claude Code. Salve-o como first-mod/tests/first-mod.test.ts:
first-mod/tests/first-mod.test.ts
No seu shell, execute os testes a partir do diretório first-mod:
A saída nomeia cada teste e se passou, com tempos que variam de execução para execução:
Cada $.tool.call passou pelo hook tool.call do mod, que adicionou um à sua contagem e passou a chamada para o stub. Nenhum ls foi executado e nenhum arquivo foi lido. $.command.run então foi para o hook command.run do mod, e answer é o objeto que esse hook retornou. O comando sai com status 1 quando um teste falha, então funciona em CI. Se seus próprios mods não conseguirem carregar no shell que o executa, ele imprime uma linha começando com claude plugin test: hooks modules are turned off com o motivo, e sai com status 1.

Simular o que Claude Code responderia

Nenhum modelo, armazenamento ou ferramenta é executado em um teste, então onde quer que seu mod espere que Claude Code responda, o teste fornece a resposta com um stub. Uma função de teste recebe dois argumentos para isso:
  • $: o próprio $ do teste, que fica no lugar do Claude Code. Não é a mods API que um hook recebe. Cada um de seus métodos levanta o evento de mesmo nome, o envia através dos hooks do seu mod, e resolve para o resultado: $.tool.call({ tool: 'Bash', command: 'ls' }) levanta tool.call. $.command.run, $.prompt.submit, $.session.start, e $.turn.complete funcionam da mesma forma, e $.classic.Stop e os outros métodos $.classic levantam um evento de hook de configurações. Um teste não pode levantar uma chamada de mods API como ui.close diretamente. Dispare-a através do seu mod, por exemplo pressionando o botão que fecha o painel.
  • on: chame-o para registrar stubs, que são hooks que respondem no lugar do Claude Code. Nomeie um stub para uma chamada de mods API sem o $., então um stub registrado como store.get responde seu $.store.get do mod. Quando seu mod chama $.model.complete ou $.store.get, um stub fornece a resposta.
Este exemplo simula uma chamada de modelo. O hook pertence a um mod chamado grader, e trata um comando /grade que envia uma frase para um modelo e relata se a resposta começa com PASS. O arquivo contém apenas o hook sob teste, então o mod também precisa de um plugin.json e um hooks.json, como em Create a mod. Para digitar /grade em uma sessão, o mod também tem que registrar o comando:
grader/hooks/register.js
Este teste simula a chamada do modelo para verificar o que o hook faz com uma resposta aprovada:
grader/tests/grader.test.ts
O teste passa porque o reply do hook é o objeto sob value, cujo text começa com PASS. Para verificar o outro ramo, adicione um segundo teste cujo stub retorna um text que começa com FAIL, e espere Try again. Um stub para uma chamada de mods API retorna um objeto com um campo value, que contém o que a chamada resolve em seu mod: { value: 7 } faz $.store.get resolver para 7. Um stub para um dos eventos do Claude Code, como turn.step ou tool.call, retorna o resultado próprio desse evento, como { result: 'ok' }. $.session.send e $.prompt.fill também levam o resultado do evento, como a tabela mostra. Look up what a stub returns mostra qual forma cada nome comum assume. Dois erros significam que um stub está errado ou faltando. A saída de um teste falhado inclui um bloco intitulado the engine reported:, e cada erro aparece lá:
  • returned neither { value } nor { deny }: um stub para uma chamada de mods API retornou um valor simples
  • no implementation for seguido por um nome: seu mod fez essa chamada e nenhum stub a responde
O kit também exporta mocks em memória que respondem um namespace inteiro para você. mock.clock(on) responde $.clock, mock.store(on, { count: 7 }) responde $.store de um armazenamento que começa com essas entradas, e mock.env(on, { CI: 'true' }) responde $.env.get dessas variáveis. mock.clock retorna um relógio simulado que seu teste avança, então um teste de um temporizador não espera. mock.store não retorna nada, então para verificar o que seu mod salvou, escreva os dois stubs store você mesmo como o drawing test faz.

Seguir as regras do test kit

O test kit tem algumas regras próprias, e quebrar uma produz os erros que novos autores de testes encontram primeiro:
  • Registre cada stub antes da primeira chamada do teste em $. Chamar on depois disso lança um erro como on("ui.render") after the test first called $.
  • session.start não é executado por si só. Cada teste começa com seu módulo carregado recentemente e nenhum de seus hooks chamado, então variáveis de nível de módulo mantêm seus valores iniciais. Se um hook depende do que session.start configura, levante-o primeiro:
    O segundo stub responde a chamada $.command.register que um hook session.start como o do tutorial faz. Sem ele, essa chamada rejeita com no implementation for command.register e o kit pula seu hook, então nada depois da chamada no hook é executado. O teste não falha nesse ponto. O hook pulado é listado sob the engine reported: apenas se uma verificação posterior falhar.
  • Um hook que retorna next(e) precisa de um stub para responder. Quando seu hook ui.render retorna next(e), por exemplo para não desenhar nada enquanto Claude está ocioso, montá-lo falha com no implementation for ui.render. Registre um stub que retorna um elemento como dados simples:
    Com o stub registrado, a montagem é bem-sucedida, e ui.find({ type: 'Text' }) retorna esse elemento sempre que seu hook retornou next(e).
  • Um stub para turn.step é um gerador assíncrono, e o teste lê o fluxo até o final para obter o resultado:
    Quando o loop termina, result é o objeto que o stub retornou, depois que seu hook turn.step teve a chance de alterá-lo. Aqui result.answer é 'ok'.
  • Levante uma chamada de ferramenta com o nome da ferramenta e argumentos como campos, como await $.tool.call({ tool: 'Bash', command: 'ls' }), e registre um stub tool.call que retorna { result }.

Procurar o que um stub retorna

Cada chamada de mods API que seu mod faz em um teste precisa de um stub que responda no lugar do Claude Code, exceto as poucas que o kit responde por si: chamadas $.ui.invalidate e $.state. Para chamadas $.clock, use mock.clock(on), ou seu $.clock.now() do mod falha com no implementation for clock.now. Esta tabela lista as que mods usam mais. A primeira coluna é a chamada que seu mod faz ou o evento que passa com next(e). A segunda é a função para passar para on sob esse nome, então a linha $.store.get se torna on('store.get', ($, e) => ({ value: saved.get(e.key) })). Um '...' em um stub marca texto para você preencher: expect tem as asserções toBe, toEqual, toMatch, toMatchObject, toContain, toBeDefined, toBeUndefined, e toThrow, e .not antes de qualquer uma delas.

Testar um temporizador

Um mod que executa trabalho em um temporizador precisa de um relógio que o teste controla, para que o teste possa avançar o tempo em vez de esperar. const clock = mock.clock(on) retorna um relógio simulado que começa em 0 e se move apenas quando seu teste o move. Para começar em outro tempo, passe-o em milissegundos, como em mock.clock(on, { now: 5000 }). O relógio tem estes métodos: Este hook pertence a um mod chamado countdown, e trata um comando /countdown que leva um número de segundos, inicia um temporizador $.clock.every de um segundo, e mostra um toast em zero. Como com grader, o arquivo contém apenas o hook sob teste e não registra o comando:
countdown/hooks/register.js
Este teste executa /countdown 3 e move o relógio simulado, então verifica três segundos de comportamento sem esperar três segundos:
countdown/tests/countdown.test.ts
O primeiro expect mostra que o toast não vem cedo, e o segundo mostra que vem uma vez. Cada advance resolve depois que os temporizadores que vencem foram executados, então a verificação na próxima linha vê seu efeito.

Testar um desenho

Um teste pode desenhar um dos render sites do seu mod, então pressionar, digitar em e encontrar os elementos que desenhou. $.ui.mount desenha o site através do hook ui.render do seu mod e retorna um identificador com um método para cada um desses. Para cobrir vários aplicativos em um teste, defina surface para o aplicativo a desenhar. Este teste abre o painel de Build a pane with tabs, muda abas, pressiona o botão, e verifica a contagem no terminal e no aplicativo Desktop:
hello-tabs/tests/hello-tabs.test.ts
No seu shell, execute claude plugin test a partir do diretório hello-tabs. O teste passa quando ambos os aplicativos desenham a linha de contagem e o mod salvou 2. A contagem é transferida do primeiro aplicativo para o segundo porque ambas as montagens usam o mesmo módulo carregado. O identificador que $.ui.mount retorna tem estes métodos, que endereçam elementos pela key que você deu a eles: Cada método resolve depois que seu manipulador terminou, então você pode verificar o resultado na próxima linha. Defina props para o que Claude Code passaria para esse site. A tabela de render sites lista os props de cada site, e os tipos para sua compilação têm seus tipos. Um teste de desenho verifica a árvore que seu hook retorna e se é válida para esse aplicativo. Não verifica como o aplicativo a pinta, então veja um novo layout em uma sessão real também.

Testar um desenho após /clear

Cada teste começa com cada valor $.state em seu padrão, que é como /clear os deixa. Para testar o que seu mod faz a seguir, pule session.start, levante classic.SessionStart com source: 'clear', e verifique o que seu mod desenha. Este teste verifica o módulo de Load a saved value again after /clear. Adicione-o ao arquivo de Test a drawing, onde PANE é definido. O primeiro teste desse arquivo espera que o botão salve a contagem, como o botão em Save from more than one session faz:
hello-tabs/tests/hello-tabs.test.ts
O teste passa quando seu hook classic.SessionStart copiou o 7 armazenado em $.state antes do painel desenhar. Sem esse hook em seu módulo, o painel desenha Count: 0, find retorna undefined, e o teste falha em toBeDefined.

Testar um mod que julga outros mods

Um mod que sua organização lista em prependPlugins pode recusar outro mod antes que ele carregue. Para testar um, defina o nível do seu mod e dê ao teste um segundo mod para o seu admitir ou recusar:
  • tier: chame-o uma vez no topo do arquivo de teste, como em tier('prepend'), para carregar seu mod como prepend, append, ou builtin, seu lugar na ordem que mods são executados. Sem ele, seu mod carrega como user.
  • plugins: passe test um objeto de opções antes do corpo do teste. Seu array plugins contém mods que você escreve inline, cada um com um name e uma função register. Para carregar um em algum lugar diferente de user, adicione tier a ele.
Este arquivo de teste carrega o policy mod da página admin primeiro. Verifica que o policy mod recusa um mod que inicia um processo e admite um que não:
acme-guard/tests/guard.test.ts
No seu shell, execute claude plugin test a partir do diretório acme-guard. Ambos os testes passam com o policy mod como a página admin mostra. O kit carrega cada mod na primeira chamada do teste em $. Quando seu mod recusa um, essa chamada lança, e a mensagem nomeia o mod recusado, o mod que o recusou, e seu motivo. No segundo teste nada é recusado, então reader responde a chamada de ferramenta antes que chegue ao stub.

Próximos passos