Skip to main content
Puoi scrivere test automatizzati per un mod ed eseguirli dalla tua shell con claude plugin test. Un test genera gli eventi che i tuoi hook gestiscono e controlla cosa hanno fatto gli hook, così puoi individuare un problema prima che raggiunga una sessione. Il primo esempio testa il mod da Create a mod.

Scrivi un test

Un test carica il tuo mod, invia eventi attraverso i suoi hook nel modo in cui Claude Code farebbe, e controlla cosa hanno fatto gli hook, senza una sessione, un accesso o una rete. Esegui i test dalla tua shell con claude plugin test, e ogni file di test importa il test kit, una libreria di test nel modulo claude-code/testing. Dai a ogni file di test un nome che termina in .test.ts, come first-mod.test.ts, e salvalo ovunque nella directory del plugin. Ogni file di test ha bisogno di almeno un test(), altrimenti l’esecuzione fallisce con declares no test(): nothing ran. Un file di test può importare i tuoi file mod e helper .ts fratelli, così puoi fare unit test di funzioni semplici, come le regole di un gioco, senza il kit. Questo test genera due chiamate di strumento, esegue il comando /tally da Create a mod, e controlla che la risposta conti entrambe. La sua prima riga è uno stub, che risponde alle chiamate di strumento al posto di Claude Code. Salvalo come first-mod/tests/first-mod.test.ts:
first-mod/tests/first-mod.test.ts
Nella tua shell, esegui i test dalla directory first-mod:
L’output nomina ogni test e se è passato, con tempi che variano da esecuzione a esecuzione:
Ogni $.tool.call è passato attraverso l’hook tool.call del mod, che ha aggiunto uno al suo conteggio e ha passato la chiamata allo stub. Nessun ls è stato eseguito e nessun file è stato letto. $.command.run è poi andato all’hook command.run del mod, e answer è l’oggetto che quell’hook ha restituito. Il comando esce con stato 1 quando un test fallisce, quindi funziona in CI. Se i tuoi mod non riescono a caricarsi nella shell che lo esegue, stampa una riga che inizia con claude plugin test: hooks modules are turned off con il motivo, ed esce con stato 1.

Simula quello che Claude Code risponderebbe

Nessun modello, archivio o strumento viene eseguito in un test, quindi ovunque il tuo mod si aspetti una risposta da Claude Code, il test fornisce la risposta con uno stub. Una funzione di test riceve due argomenti per questo:
  • $: il $ del test, che sta dove Claude Code fa. Non è l’API dei mod che un hook riceve. Ognuno dei suoi metodi genera l’evento dello stesso nome, lo invia attraverso i tuoi hook del mod, e si risolve nel risultato: $.tool.call({ tool: 'Bash', command: 'ls' }) genera tool.call. $.command.run, $.prompt.submit, $.session.start, e $.turn.complete funzionano allo stesso modo, e $.classic.Stop e gli altri metodi $.classic generano un evento hook delle impostazioni. Un test non può generare direttamente una chiamata API dei mod come ui.close. Attivala attraverso il tuo mod, ad esempio premendo il pulsante che chiude il riquadro.
  • on: chiamalo per registrare gli stub, che sono hook che rispondono al posto di Claude Code. Nomina uno stub per una chiamata API dei mod senza il $., quindi uno stub registrato come store.get risponde al $.store.get del tuo mod. Quando il tuo mod chiama $.model.complete o $.store.get, uno stub fornisce la risposta.
Questo esempio simula una chiamata di modello. L’hook appartiene a un mod denominato grader, e gestisce un comando /grade che invia una frase a un modello e segnala se la risposta inizia con PASS. Il file contiene solo l’hook in prova, quindi il mod ha anche bisogno di un plugin.json e un hooks.json, come in Create a mod. Per digitare /grade in una sessione, il mod deve anche registrare il comando:
grader/hooks/register.js
Questo test simula la chiamata del modello per controllare cosa fa l’hook con una risposta positiva:
grader/tests/grader.test.ts
Il test passa perché il reply dell’hook è l’oggetto sotto value, il cui text inizia con PASS. Per controllare l’altro ramo, aggiungi un secondo test il cui stub restituisce un text che inizia con FAIL, e aspettati Try again. Uno stub per una chiamata API dei mod restituisce un oggetto con un campo value, che contiene ciò a cui la chiamata si risolve nel tuo mod: { value: 7 } fa sì che $.store.get si risolva in 7. Uno stub per uno degli eventi di Claude Code, come turn.step o tool.call, restituisce il risultato proprio di quell’evento, come { result: 'ok' }. $.session.send e $.prompt.fill prendono anche il risultato dell’evento, come mostra la tabella. Guarda cosa restituisce uno stub mostra quale forma assume ogni nome comune. Due errori significano che uno stub è sbagliato o mancante. L’output di un test fallito include un blocco intitolato the engine reported:, e ogni errore appare lì:
  • returned neither { value } nor { deny }: uno stub per una chiamata API dei mod ha restituito un valore nudo
  • no implementation for seguito da un nome: il tuo mod ha fatto quella chiamata e nessuno stub la risponde
Il kit esporta anche mock in memoria che rispondono a un intero namespace per te. mock.clock(on) risponde a $.clock, mock.store(on, { count: 7 }) risponde a $.store da un archivio che inizia con quelle voci, e mock.env(on, { CI: 'true' }) risponde a $.env.get da quelle variabili. mock.clock restituisce un orologio mock che il tuo test avanza, quindi un test di un timer non aspetta. mock.store non restituisce nulla, quindi per controllare cosa il tuo mod ha salvato, scrivi i due stub store tu stesso come fa il test di disegno.

Segui le regole del test kit

Il test kit ha alcune regole proprie, e infrangerne una produce gli errori che i nuovi autori di test incontrano per primi:
  • Registra ogni stub prima della prima chiamata del test su $. Chiamare on dopo questo genera un errore come on("ui.render") after the test first called $.
  • session.start non viene eseguito da solo. Ogni test inizia con il tuo modulo appena caricato e nessuno dei suoi hook chiamato, quindi le variabili a livello di modulo mantengono i loro valori iniziali. Se un hook dipende da ciò che session.start configura, generalo per primo:
    Il secondo stub risponde alla chiamata $.command.register che un hook session.start come quello del tutorial fa. Senza di esso, quella chiamata rifiuta con no implementation for command.register e il kit salta il tuo hook, quindi nulla dopo la chiamata nell’hook viene eseguito. Il test non fallisce a quel punto. L’hook saltato è elencato sotto the engine reported: solo se un controllo successivo fallisce.
  • Un hook che restituisce next(e) ha bisogno di uno stub per rispondere. Quando il tuo hook ui.render restituisce next(e), ad esempio per non disegnare nulla mentre Claude è inattivo, montarlo fallisce con no implementation for ui.render. Registra uno stub che restituisce un elemento come dati semplici:
    Con lo stub registrato, il montaggio ha successo, e ui.find({ type: 'Text' }) restituisce quell’elemento ogni volta che il tuo hook ha restituito next(e).
  • Uno stub per turn.step è un generatore asincrono, e il test legge il flusso fino alla fine per ottenere il risultato:
    Quando il ciclo termina, result è l’oggetto che lo stub ha restituito, dopo che il tuo hook turn.step ha avuto la possibilità di cambiarlo. Qui result.answer è 'ok'.
  • Genera una chiamata di strumento con il nome dello strumento e gli argomenti come campi, come await $.tool.call({ tool: 'Bash', command: 'ls' }), e registra uno stub tool.call che restituisce { result }.

Guarda cosa restituisce uno stub

Ogni chiamata API dei mod che il tuo mod fa in un test ha bisogno di uno stub che risponda al posto di Claude Code, tranne i pochi che il kit risponde da solo: $.ui.invalidate e $.state chiama. Per le chiamate $.clock, usa mock.clock(on), altrimenti il $.clock.now() del tuo mod fallisce con no implementation for clock.now. Questa tabella elenca quelli che i mod usano di più. La prima colonna è la chiamata che il tuo mod fa o l’evento che passa con next(e). La seconda è la funzione da passare a on con quel nome, quindi la riga $.store.get diventa on('store.get', ($, e) => ({ value: saved.get(e.key) })). Un '...' in uno stub contrassegna il testo che devi compilare: expect ha le asserzioni toBe, toEqual, toMatch, toMatchObject, toContain, toBeDefined, toBeUndefined, e toThrow, e .not prima di una qualsiasi di esse.

Testa un timer

Un mod che esegue lavoro su un timer ha bisogno di un orologio che il test controlla, così il test può spostare il tempo in avanti invece di aspettare. const clock = mock.clock(on) restituisce un orologio mock che inizia a 0 e si muove solo quando il tuo test lo muove. Per iniziare in un altro momento, passalo in millisecondi, come in mock.clock(on, { now: 5000 }). L’orologio ha questi metodi: Questo hook appartiene a un mod denominato countdown, e gestisce un comando /countdown che accetta un numero di secondi, avvia un timer $.clock.every di un secondo, e mostra un toast a zero. Come con grader, il file contiene solo l’hook in prova e non registra il comando:
countdown/hooks/register.js
Questo test esegue /countdown 3 e sposta l’orologio mock, così controlla tre secondi di comportamento senza aspettare tre secondi:
countdown/tests/countdown.test.ts
Il primo expect mostra che il toast non arriva presto, e il secondo mostra che arriva una volta. Ogni advance si risolve dopo che i timer che sono scaduti hanno eseguito, così il controllo sulla riga successiva vede il loro effetto.

Testa un disegno

Un test può disegnare uno dei siti di rendering del tuo mod, quindi premere, digitare in, e trovare gli elementi che ha disegnato. $.ui.mount disegna il sito attraverso l’hook ui.render del tuo mod e restituisce un handle con un metodo per ognuno di quelli. Per coprire diverse app in un test, imposta surface all’app per cui disegnare. Questo test apre il riquadro da Build a pane with tabs, cambia schede, preme il pulsante, e controlla il conteggio nel terminale e nell’app Desktop:
hello-tabs/tests/hello-tabs.test.ts
Nella tua shell, esegui claude plugin test dalla directory hello-tabs. Il test passa quando entrambe le app disegnano la riga di conteggio e il mod ha salvato 2. Il conteggio si trasporta dalla prima app alla seconda perché entrambi i montaggi usano lo stesso modulo caricato. L’handle che $.ui.mount restituisce ha questi metodi, che indirizzano gli elementi per la key che hai dato loro: Ogni metodo si risolve dopo che il tuo handler ha finito, così puoi controllare il risultato sulla riga successiva. Imposta props a ciò che Claude Code passerebbe per quel sito. La tabella dei siti di rendering elenca i prop di ogni sito, e i tipi per la tua build hanno i loro tipi. Un test di disegno controlla l’albero che il tuo hook restituisce e se è valido per quell’app. Non controlla come l’app lo dipinge, quindi guarda un nuovo layout in una sessione reale anche.

Testa un disegno dopo /clear

Ogni test inizia con ogni valore $.state al suo default, che è come /clear li lascia. Per testare cosa fa il tuo mod dopo, salta session.start, genera classic.SessionStart con source: 'clear', e controlla cosa disegna il tuo mod. Questo test controlla il modulo da Load a saved value again after /clear. Aggiungilo al file da Test a drawing, dove PANE è definito. Il primo test di quel file si aspetta che il pulsante salvi il conteggio, come il pulsante in Save from more than one session fa:
hello-tabs/tests/hello-tabs.test.ts
Il test passa quando il tuo hook classic.SessionStart ha copiato il 7 salvato in $.state prima che il riquadro disegni. Senza quell’hook nel tuo modulo, il riquadro disegna Count: 0, find restituisce undefined, e il test fallisce a toBeDefined.

Testa un mod che giudica altri mod

Un mod che la tua organizzazione elenca in prependPlugins può rifiutare un altro mod prima che carichi. Per testarne uno, imposta il tier del tuo mod e dai al test un secondo mod per il tuo da ammettere o rifiutare:
  • tier: chiamalo una volta in cima al file di test, come in tier('prepend'), per caricare il tuo mod come prepend, append, o builtin, il suo posto nell’ordine in cui i mod vengono eseguiti. Senza di esso, il tuo mod carica come user.
  • plugins: passa a test un oggetto opzioni davanti al corpo del test. Il suo array plugins contiene mod che scrivi inline, ognuno con un name e una funzione register. Per caricare uno da qualche parte diversa da user, aggiungi tier ad esso.
Questo file di test carica il mod di policy dalla pagina admin per primo. Controlla che il mod di policy rifiuti un mod che avvia un processo e ammetta uno che non lo fa:
acme-guard/tests/guard.test.ts
Nella tua shell, esegui claude plugin test dalla directory acme-guard. Entrambi i test passano con il mod di policy come mostra la pagina admin. Il kit carica ogni mod alla prima chiamata del test su $. Quando il tuo mod ne rifiuta uno, quella chiamata genera un’eccezione, e il messaggio nomina il mod rifiutato, il mod che lo ha rifiutato, e il tuo motivo. Nel secondo test nulla viene rifiutato, quindi reader risponde alla chiamata di strumento prima che raggiunga lo stub.

Passaggi successivi