Pular para o conteúdo
MÓDULO 4.2

🧪 Testar com claude plugin test

O claude plugin test roda os seus testes contra o engine de verdade, sem modelo e sem gastar cota. A diferença para um teste comum é uma só: por baixo do mod não há nada, e quem faz o papel do Claude Code são os hooks que o próprio teste pendura.

6
Tópicos
~35
Minutos
Avançado
Nível
Prático
Tipo
0 de 60%
Versão conferida: testes e saídas deste módulo foram rodados no Claude Code 2.1.289, em 05/10/2026. O módulo claude-code/testing está declarado no mesmo arquivo de tipos da API: é lá que você confere cada função.
1

Entenda o que o teste simula

claude plugin test <pasta> procura os arquivos *.test.ts do mod e roda cada um contra o engine de verdade. O mod carrega como numa sessão, com a mesma cadeia e as mesmas regras de validação.

Cada teste recebe dois objetos. O $ do engine, para disparar eventos de cima (iniciar a sessão, chamar uma ferramenta, rodar um comando). E um on cujos hooks ficam embaixo do mod. A referência é direta: nada fica abaixo desses hooks; eles fazem o papel do engine.

🆕 Novo aqui? O kit de teste

  • claude-code/testing — o módulo de onde vêm test, describe, expect e mock.
  • test(nome, corpo) — o corpo é async ($, on) => {…}.
  • Mock — um hook do teste que responde no lugar do engine (ex.: on('fs.read', …) devolve o texto de um arquivo que não existe).
corpo do teste: $ do engine $.session.start · $.tool.call · $.command.run o seu mod register(on, options), carregado como numa sessão hooks do on do teste = o engine fs.read · store.get · command.register · tool.call abaixo: nada

Como ler o desenho: o teste empurra eventos de cima, o mod reage no meio, e o que o mod pede ao $ desce até a camada cinza. Se nenhum hook do teste responde a um pedido, ninguém responde: o teste falha dizendo qual noun faltou.

Numa sessãoNo teste
o modelo pede uma ferramentaawait $.tool.call({ tool: 'Write', … })
a ferramenta roda de verdadeon('tool.call', () => ({ result: 'ok', text: 'ok' }))
o disco respondeon('fs.read', …) lendo de um Map
a pessoa digita /reciboawait $.command.run(comando('recibo'))
2

Escreva os mocks antes do $

A regra que mais derruba teste novo: todo on(...) vem antes da primeira chamada a $. Um mock pendurado depois é recusado com a mensagem on(...) after the test first called $ (pegadinha 18 do COMO-FAZER-UM-MOD.md).

O inema-mods resolve isso com um arquivo de mundo: tests/mundo.ts pendura todos os mocks de uma vez e devolve um objeto onde o teste lê o que aconteceu (comandos registrados, toasts, processos rodados).

📄 inema-mods/mods/recibo-sessao/tests/mundo.ts (trecho)
  mock.clock(on)
  on('session.start', ($, e) => ({ cwd: e.cwd }))
  on('prompt.submit', ($, e) => ({ text: e.text, ...(e.context !== undefined && { context: e.context }) }))
  on('fs.exists', ($, e) => ({ value: mundo.arquivos.has(e.path) }))
  on('fs.read', ($, e) => {
    const f = mundo.arquivos.get(e.path)
    return f ? { value: f.texto } : { deny: `ENOENT: ${e.path}` }
  })
  on('store.get', () => ({ value: undefined }))
  on('store.set', () => ({ value: undefined }))
  on('command.register', ($, e) => {
    mundo.comandos.push(e.name)
    return { value: { command: e.name } }
  })
O que olhar: cada mock devolve a forma que o engine devolveria ({ value }, ou { deny } para "arquivo não existe"). O arquivo vai copiado em cada mod: um mod não importa nada de fora da própria pasta.

✓ Ordem certa

  • ✓ const mundo = mundoDe(on)
  • ✓ on('tool.call', …) extra do teste
  • ✓ await $.session.start(SESSAO)
  • ✓ chamadas e expect

✗ Ordem que falha

  • ✗ await $.session.start(...) primeiro
  • ✗ depois on('fs.read', …)
  • ✗ resultado: on(...) after the test first called $

⚠️ O mock específico antes do genérico

O $.ui.ask chega ao fundo como um tool.call de AskUserQuestion. No mundo.ts do freio-de-mao, o mock on('tool.call', { tool: 'AskUserQuestion' }, …) é registrado antes de qualquer mock genérico de tool.call, "para não ser engolido por ele", diz o comentário no arquivo.

💡 A mensagem diz qual mock falta

Se o mod usa um noun que o mundo não responde (session.usage, fs.list, env.get...), o erro nomeia exatamente o que faltou (pegadinha 3). Acrescente o on('<noun>.<método>', …) e rode de novo.

3

Teste um comando de ponta a ponta

O teste do starter-mod (Claude Mods Starter Kit, da Prompt Advisers, MIT) cabe numa tela e cobre o mod inteiro: sessão, duas leituras (uma falha), o comando /readcount e a resposta.

📄 templates/starter-mod/tests/starter.test.ts (inteiro)
import { test, expect } from "claude-code/testing";
test("counts successful reads and excludes failed ones", async ($, on) => {
  on("session.start", ($, e) => ({ cwd: e.cwd }));
  on("command.register", ($, e) => ({ value: { command: e.name } }));
  on("tool.call", ($, e) => e.file_path === "/missing" ? { isError: true, result: "missing", text: "missing" } : { result: {}, text: "ok" });
  await $.session.start({ surface: "terminal", isInteractive: true, cwd: "/work" } as any);
  await $.tool.call({ tool: "Read", file_path: "/file" } as any);
  await $.tool.call({ tool: "Read", file_path: "/missing" } as any);
  const r = await $.command.run({ command: "readcount", args: "", origin: { kind: "composer" }, presentation: { isFullscreen: false, columns: 100 } } as any);
  expect(r.text).toBe("Successful reads observed: 1");
});
1

Três mocks, antes de tudo

Sessão, registro de comando e a ferramenta Read, que falha só para /missing.

2

$.session.start

Dispara o session.start do mod, que registra /readcount.

3

Duas chamadas de $.tool.call

Uma dá certo, outra volta com isError. O mod deve contar só a primeira.

4

$.command.run e expect

Roda o comando como se a pessoa digitasse e confere o texto exato.

🎯 Objetivo: rodar o teste do starter-mod

No terminal, dentro da pasta claude-mods-starter-kit:

claude plugin test templates/starter-mod

Saída real (05/10/2026, Claude Code 2.1.289):

tests/starter.test.ts:
(pass) counts successful reads and excludes failed ones [20.64ms]

 1 pass
 0 fail
Ran 1 test across 1 file. [0.13s]
Como verificar: aparecem 1 pass e 0 fail. Os tempos entre colchetes mudam de máquina para máquina. Para ver o teste pegar um erro, troque o 1 do expect por 2, rode de novo e desfaça.

🆕 Novo aqui? test(nome, { options }, corpo)

Entre o nome e o corpo pode vir um objeto de opções. options faz o papel dos valores do /config: o mod recebe em register(on, options). O freio-de-mao usa assim: test('sem tela com sem_tela=permitir: roda', { options: { sem_tela: 'permitir' } }, async ($, on) => {…}). Sem options, valem os padrões do manifesto. O outro campo é timeoutMs (tópico 6).

4

Controle o relógio

Mod que agenda trabalho ($.clock.after, $.clock.every) ou carimba hora ($.clock.now) precisa de um relógio que só anda quando o teste manda. É o mock.clock(on).

Por isso a pegadinha 7 manda usar $.clock.now() e nunca Date.now(): o segundo lê o relógio da máquina, que o teste não controla.

Do relógio que mock.clock devolveFaz
now()a hora que o $.clock.now() do mod recebe
advance(ms)anda e solta, em ordem, cada espera vencida no caminho
set(ms)vai até uma hora igual ou maior que a atual
settle()igual a advance(0): deixa rodar o que já está pronto, sem andar
sleep(ms)um mock do teste que responde "atrasado"
📄 claude-mods-starter-kit/plugins/auto-handoff/tests/auto-handoff.test.ts (trecho)
    const clock = mock.clock(on, { now: START });
    mock.store(on);
    const { fs, seen } = world(on, { [`${CWD}/.git/HEAD`]: "ref" });
    await $.session.start({ surface: "terminal", isInteractive: true, cwd: CWD } as any);

    await $.session.measure(measure(50));
    await clock.settle();
    expect(seen.forks).toBe(0);

    await $.session.measure(measure(86));
    await clock.settle();
    expect(seen.forks).toBe(1);
O que olhar: o mod dispara trabalho sem esperar (pegadinha 16: $.clock.after(0, …)). O settle() é o passo entre "disparei" e "confiro o que aconteceu".

⚠️ Espera presa tem limite

Segundo os tipos, uma espera presa no relógio falso além do orçamento de um hook (dez segundos de tempo real) é solta, como um hook que estourou. Se o teste esquece de chamar advance, o sintoma é lentidão seguida de falha, não um travamento eterno.

5

Monte a tela em terminal e desktop

Para testar o que o mod desenha, o teste monta um componente numa superfície que ele nomeia: $.ui.mount({ plugin, surface, component, … }). A superfície nunca é presumida.

A referência recomenda escrever o corpo uma vez e repetir para ['terminal', 'desktop'] as const. Assim o teste prova que o mod não depende de uma tela só (lembre: Raster só existe no terminal).

📄 inema-mods/mods/marcador-sessao/tests/marcador.test.ts (trecho)
    for (const surface of ['terminal', 'desktop'] as const) {
      const ui = await $.ui.mount({
        plugin: 'marcador-sessao',
        surface,
        component: 'Pane',
        requestId: 'marcador-sessao',
        viewport: VIEWPORT,
        props: PANE_PROPS,
      })
      expect(await ui.find({ type: 'Text', text: 'rotas' })).toBeDefined()
      await ui.press({ key: 'colar0' })
      await ui.unmount()
    }
    expect(mundo.preenchidos).toHaveLength(2)
O que olhar: o botão é achado pela key; o texto, pelo conteúdo. No fim, duas colagens: uma por superfície.
um corpo de testefor (const surface …) mount: terminalfind · press · unmount mount: desktopfind · press · unmount expect2 colagens

Como ler o desenho: o mesmo código passa pelas duas superfícies. Se o mod usasse um elemento que o desktop não tem, a montagem do desktop recusaria a árvore e o teste falharia ali, não na mão do usuário.

✓ Ache assim

  • ✓ Text pelo texto: { type: 'Text', text: /usado/ }
  • ✓ Button e Box pela key
  • ✓ Na faixa, um ui.render do teste por baixo devolvendo { type: 'Text', children: [''] } (pegadinha 11)

✗ Não funciona

  • ✗ Text pela key: ela some na árvore desenhada (pegadinha 12)
  • ✗ Testar só no terminal e publicar para o desktop
  • ✗ Esquecer o unmount() entre as voltas
6

Leia uma falha de teste

Uma falha real, de 05/10/2026, nesta máquina. Rodando bash scripts/check.sh no kit da Prompt Advisers (validate e test de cada plugin), o resultado foi 123 de 124 testes passando. Todos os validates deram ok.

O que falhou: o terminal-pet, com 12 passando e 1 falhando. O teste "the whole sprite set stays far under the terminal's color-pair table" estourou o limite de 5 s com a máquina carregada. E o check.sh parou ali.

1

Ache o plugin e o teste

O check.sh abre com set -euo pipefail: no primeiro plugin que falha, o laço para, e o que vem depois dele (aqui, o starter-mod) não roda. Por isso o registro diz "parou no terminal-pet".

2

Separe tempo de asserção

Os tipos dizem: um teste falha quando lança, rejeita ou passa do seu tempo (5000 ms, ou timeoutMs). Aqui nenhum expect falhou; o tempo acabou.

3

Leia o corpo do teste

Ele não chama $: decodifica cada quadro do bichinho, a cada 10 ms ao longo de 6 s, para todos os humores e três níveis. É cálculo puro, e cálculo puro fica lento quando a CPU está ocupada.

4

Decida: bug ou ambiente?

Repita só aquele plugin com a máquina livre. Se passar, é ambiente. A correção mínima é dar tempo ao teste pesado: test(nome, { timeoutMs: 15000 }, corpo), sem mexer no mod.

✓ Sinais de tempo estourado

  • ✓ Passa sozinho, falha com a máquina ocupada
  • ✓ Corpo com laços grandes e nenhum await
  • ✓ Nenhuma linha de expect citada

✗ Sinais de bug de verdade

  • ✗ Falha igual toda vez
  • ✗ Mostra o valor esperado e o recebido
  • ✗ Mensagem nomeando um mock que falta

Teste rápido (opcional): o teste do Rafa falha com on(...) after the test first called $. O que ele corrige?

🎓 Resumo do módulo

✓
Engine real, nada por baixo — os hooks do teste fazem o papel dele.
✓
Mocks antes do $ — o mundo.ts pendura todos de uma vez.
✓
test(nome, { options }, corpo) — simula o /config.
✓
mock.clock e settle — o tempo anda quando o teste manda.
✓
Tempo estourado não é asserção — leia antes de mexer no mod.

Próximo módulo:

4.3 — Depurar e as pegadinhas