Einen Mod testen
Schreiben Sie automatisierte Tests für einen Claude Code Mod, der Ereignisse auslöst, Antworten von Claude Code simuliert und Schaltflächen drückt, ohne Sitzung, Anmeldung oder Netzwerk.
Sie können automatisierte Tests für einen Mod schreiben und diese von Ihrer Shell aus mit claude plugin test ausführen. Ein Test löst die Ereignisse aus, die Ihre Hooks verarbeiten, und überprüft, was die Hooks getan haben, damit Sie ein Problem fangen, bevor es eine Sitzung erreicht. Das erste Beispiel testet den Mod aus Einen Mod erstellen.
Schreiben Sie einen Test
Ein Test lädt Ihr Mod, sendet Ereignisse durch seine Hooks so, wie Claude Code es würde, und prüft, was die Hooks getan haben, ohne eine Sitzung, eine Anmeldung oder ein Netzwerk. Sie führen Tests aus Ihrer Shell mit claude plugin test aus, und jede Testdatei importiert das Test-Kit, eine Test-Bibliothek im Modul claude-code/testing.
Geben Sie jeder Testdatei einen Namen, der auf .test.ts endet, z. B. first-mod.test.ts, und speichern Sie sie überall im Plugin-Verzeichnis. Jede Testdatei benötigt mindestens einen test(), sonst schlägt die Ausführung mit declares no test(): nothing ran fehl. Eine Testdatei kann die eigenen Dateien Ihres Mods und Hilfsdateien mit .ts importieren, sodass Sie einfache Funktionen, wie die Regeln eines Spiels, ohne das Kit testen können.
Dieser Test löst zwei Tool-Aufrufe aus, führt den Befehl /tally aus Create a mod aus und prüft, dass die Antwort beide zählt. Seine erste Zeile ist ein Stub, der die Tool-Aufrufe an Stelle von Claude Code beantwortet. Speichern Sie ihn als first-mod/tests/first-mod.test.ts:
import { expect, test } from 'claude-code/testing'
test('/tally reports the tool calls the mod has seen', async ($, on) => {
// Answer each tool call in Claude Code's place, so no tool runs
on('tool.call', () => ({ result: 'ok' }))
// Raise two tool calls, which the mod's tool.call hook counts
await $.tool.call({ tool: 'Bash', command: 'ls' })
await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// Run /tally and check the text its hook returns
const answer = await $.command.run({ command: 'tally', args: '' })
expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')
})
Führen Sie in Ihrer Shell die Tests aus dem Verzeichnis first-mod aus:
claude plugin test
Die Ausgabe nennt jeden Test und ob er bestanden hat, mit Zeitangaben, die von Durchlauf zu Durchlauf variieren:
tests/first-mod.test.ts:
(pass) /tally reports the tool calls the mod has seen [22.87ms]
1 pass
0 fail
Ran 1 test across 1 file. [0.19s]
Jeder $.tool.call durchlief den tool.call-Hook des Mods, der eins zu seiner Zählung addierte und den Aufruf an den Stub weitergab. Kein ls wurde ausgeführt und keine Datei wurde gelesen. $.command.run ging dann zum command.run-Hook des Mods, und answer ist das Objekt, das dieser Hook zurückgab.
Der Befehl wird mit Status 1 beendet, wenn ein Test fehlschlägt, sodass er in CI funktioniert. Wenn Ihre eigenen Mods nicht in der Shell geladen werden können, die ihn ausführt, druckt er eine Zeile aus, die mit claude plugin test: hooks modules are turned off beginnt, mit dem Grund, und wird mit Status 1 beendet.
Stub was Claude Code antworten würde
Kein Modell, Speicher oder Tool wird in einem Test ausgeführt, daher liefert der Test überall dort, wo Ihr Mod erwartet, dass Claude Code antwortet, die Antwort mit einem Stub. Eine Test-Funktion erhält zwei Argumente dafür:
$: das eigene$des Tests, das an der Stelle von Claude Code steht. Es ist nicht die Mods-API, die ein Hook erhält. Jede seiner Methoden löst das Ereignis desselben Namens aus, sendet es durch die Hooks Ihres Mods und wird zum Ergebnis aufgelöst:$.tool.call({ tool: 'Bash', command: 'ls' })lösttool.callaus.$.command.run,$.prompt.submit,$.session.startund$.turn.completefunktionieren auf die gleiche Weise, und$.classic.Stopund die anderen$.classic-Methoden lösen ein Einstellungs-Hook-Ereignis aus. Ein Test kann einen Mods-API-Aufruf wieui.closenicht direkt auslösen. Lösen Sie ihn durch Ihr Mod aus, z. B. indem Sie auf die Schaltfläche klicken, die den Bereich schließt.on: Rufen Sie es auf, um Stubs zu registrieren, die Hooks sind, die an Stelle von Claude Code antworten. Benennen Sie einen Stub für einen Mods-API-Aufruf ohne das$., sodass ein alsstore.getregistrierter Stub die$.store.getIhres Mods beantwortet. Wenn Ihr Mod$.model.completeoder$.store.getaufruft, liefert ein Stub die Antwort.
Dieses Beispiel stubbt einen Modellaufruf. Der Hook gehört zu einem Mod namens grader und verarbeitet einen /grade-Befehl, der einen Satz an ein Modell sendet und meldet, ob die Antwort mit PASS beginnt. Die Datei enthält nur den Hook unter Test, daher benötigt das Mod auch eine plugin.json und eine hooks.json, wie in Create a mod. Um /grade in einer Sitzung einzugeben, muss das Mod auch den Befehl registrieren:
export function register(on) {
on('command.run', { command: 'grade' }, async ($, e) => {
// e.args is the text typed after /grade
const reply = await $.model.complete({
model: 'haiku',
system: 'Grade the sentence. Start your reply with PASS or FAIL.',
prompt: e.args,
})
const passed = reply.isAnswered && reply.text.startsWith('PASS')
return { text: passed ? 'Passed' : 'Try again' }
})
}
Dieser Test stubbt den Modellaufruf, um zu prüfen, was der Hook mit einer bestandenen Antwort tut:
import { expect, test } from 'claude-code/testing'
test('a passing grade is reported', async ($, on) => {
// Answer the mod's $.model.complete call with a fixed reply, so no model runs
on('model.complete', () => ({
value: {
isAnswered: true,
text: 'PASS\nNice sentence.',
usage: { input_tokens: 10, output_tokens: 5, cache_read_input_tokens: 0, cache_creation_input_tokens: 0 },
},
}))
// Run /grade, which makes the mod call the model
const answer = await $.command.run({ command: 'grade', args: 'The cat sat on the mat.' })
expect(answer.text).toBe('Passed')
})
Der Test besteht, weil die reply des Hooks das Objekt unter value ist, dessen text mit PASS beginnt. Um den anderen Zweig zu prüfen, fügen Sie einen zweiten Test hinzu, dessen Stub einen text zurückgibt, der mit FAIL beginnt, und erwarten Sie Try again.
Ein Stub für einen Mods-API-Aufruf gibt ein Objekt mit einem value-Feld zurück, das hält, was der Aufruf in Ihrem Mod aufgelöst wird: { value: 7 } macht $.store.get zu 7 aufgelöst. Ein Stub für eines der Ereignisse von Claude Code, wie turn.step oder tool.call, gibt das eigene Ergebnis dieses Ereignisses zurück, wie { result: 'ok' }. $.session.send und $.prompt.fill nehmen auch das Ergebnis des Ereignisses, wie die Tabelle zeigt. Nachschlagen, was ein Stub zurückgibt zeigt, welche Form jeder häufige Name annimmt. Zwei Fehler bedeuten, dass ein Stub falsch oder fehlend ist. Die Ausgabe eines fehlgeschlagenen Tests enthält einen Block mit der Überschrift the engine reported:, und jeder Fehler erscheint dort:
returned neither { value } nor { deny }: ein Stub für einen Mods-API-Aufruf gab einen bloßen Wert zurückno implementation forgefolgt von einem Namen: Ihr Mod hat diesen Aufruf gemacht und kein Stub beantwortet ihn
Das Kit exportiert auch speicherinterne Mocks, die einen ganzen Namespace für Sie beantworten. mock.clock(on) beantwortet $.clock, mock.store(on, { count: 7 }) beantwortet $.store aus einem Speicher, der mit diesen Einträgen beginnt, und mock.env(on, { CI: 'true' }) beantwortet $.env.get aus diesen Variablen. mock.clock gibt eine Mock-Uhr zurück, die Ihr Test vorantreibt, sodass ein Test eines Timers nicht wartet. mock.store gibt nichts zurück, daher schreiben Sie die beiden store-Stubs selbst, wie der Zeichnungstest es tut.
Befolgen Sie die Regeln des Test-Kits
Das Test-Kit hat ein paar eigene Regeln, und das Brechen einer produziert die Fehler, die neue Test-Autoren zuerst treffen:
-
Registrieren Sie jeden Stub vor dem ersten Aufruf des Tests auf
$. Das Aufrufen vonondanach wirft einen Fehler wieon("ui.render") after the test first called $. -
session.startwird nicht von selbst ausgeführt. Jeder Test beginnt mit Ihrem Modul frisch geladen und keiner seiner Hooks aufgerufen, daher halten Variablen auf Modulebene ihre Anfangswerte. Wenn ein Hook davon abhängt, wassession.starteinrichtet, lösen Sie es zuerst aus:// Answer the event after your hook passes it on with next(e) on('session.start', () => ({ cwd: '/work' })) // Answer the $.command.register call your hook makes on('command.register', () => ({ value: undefined })) // Raise the event, which runs your session.start hook await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })Der zweite Stub beantwortet den
$.command.register-Aufruf, den einsession.start-Hook wie der Tutorial macht. Ohne ihn wird dieser Aufruf mitno implementation for command.registerabgelehnt und das Kit überspringt Ihren Hook, sodass nichts nach dem Aufruf im Hook ausgeführt wird. Der Test schlägt an diesem Punkt nicht fehl. Der übersprungene Hook wird unterthe engine reported:nur aufgelistet, wenn eine spätere Prüfung fehlschlägt. -
Ein Hook, der
next(e)zurückgibt, benötigt einen Stub zum Beantworten. Wenn Ihrui.render-Hooknext(e)zurückgibt, z. B. um nichts zu zeichnen, während Claude untätig ist, schlägt das Mounten mitno implementation for ui.renderfehl. Registrieren Sie einen Stub, der ein Element als einfache Daten zurückgibt:// Stands for what Claude Code would draw at the site on('ui.render', () => ({ type: 'Text', props: {}, children: ['drawn by Claude Code'] }))Mit dem registrierten Stub wird das Mounten erfolgreich, und
ui.find({ type: 'Text' })gibt dieses Element zurück, wann immer Ihr Hooknext(e)zurückgab. -
Ein Stub für
turn.stepist ein asynchroner Generator, und der Test liest den Stream bis zum Ende, um das Ergebnis zu erhalten:on('turn.step', async function* ($, e) { // Each yield is one piece of the model's streamed reply yield { kind: 'text', index: 0, text: 'ok' } // The return value is the result of the whole request return { turnId: e.turnId, index: e.index, answer: 'ok', toolUses: [], stopReason: 'end_turn', usage: null } }) // Raise one request to the model, which runs your turn.step hook const stream = $.turn.step({ turnId: 't', index: 0, model: 'claude-test', messageCount: 1 }) // Read every piece until the stream says it's done let step = await stream.next() while (step.done !== true) step = await stream.next() const result = step.valueWenn die Schleife endet, ist
resultdas Objekt, das der Stub zurückgab, nachdem Ihrturn.step-Hook die Chance hatte, es zu ändern. Hier istresult.answer'ok'. -
Lösen Sie einen Tool-Aufruf mit dem Namen und den Argumenten des Tools als Felder aus, wie
await $.tool.call({ tool: 'Bash', command: 'ls' }), und registrieren Sie einentool.call-Stub, der{ result }zurückgibt.
Nachschlagen, was ein Stub zurückgibt
Jeder Mods-API-Aufruf, den Ihr Mod in einem Test macht, benötigt einen Stub, der an Stelle von Claude Code antwortet, außer den wenigen, die das Kit selbst beantwortet: $.ui.invalidate und $.state-Aufrufe. Für $.clock-Aufrufe verwenden Sie mock.clock(on), oder die $.clock.now() Ihres Mods schlägt mit no implementation for clock.now fehl.
Diese Tabelle listet die auf, die Mods am häufigsten verwenden. Die erste Spalte ist der Aufruf, den Ihr Mod macht, oder das Ereignis, das es mit next(e) weitergegeben wird. Die zweite ist die Funktion, die unter diesem Namen an on übergeben wird, sodass die Zeile $.store.get zu on('store.get', ($, e) => ({ value: saved.get(e.key) })) wird. Ein '...' in einem Stub markiert Text, den Sie ausfüllen müssen:
| Ihr Mod ruft auf oder gibt weiter | Stub |
|---|---|
$.command.register, $.tool.register, $.ui.toast, $.ui.log, $.ui.status, $.ui.close, $.store.set |
() => ({ value: undefined }). Für ui.toast und ui.log ist der Text e.text. |
$.store.get |
($, e) => ({ value: saved.get(e.key) }) |
$.fs.read |
($, e) => ({ value: e.path.endsWith('notes.md') ? '# Notes' : '' }). e.path kommt als absoluter Pfad an, daher vergleichen Sie mit endsWith. |
$.ui.open |
() => ({ value: { isPlaced: true } }) |
$.ui.ask |
Ein tool.call-Stub, weil die Frage es als Aufruf des AskUserQuestion-Tools erreicht: ($, e) => ({ result: { answers: { [e.questions[0].question]: 'Run it' } } }). Prüfen Sie zuerst e.tool, wenn Ihr Mod andere Tool-Aufrufe weitergegeben wird. |
$.model.complete |
() => ({ value: { isAnswered: true, text: '...', usage } }) |
$.process.run |
($, e) => ({ value: { exitCode: 0, stdout: '...', stderr: '' } }). e.argv ist die Argumentliste und e.init hält cwd und timeoutMs. |
| Jeder Mods-API-Aufruf, der fehlschlagen sollte | () => ({ deny: 'the reason' }), was den Aufruf in Ihrem Mod ablehnen lässt. Ein Stub, der wirft, wird stattdessen übersprungen. |
session.start |
() => ({ cwd: '/work' }) |
turn.start |
($, e) => ({ turnId: e.turnId }) |
tool.call |
() => ({ result: '...' }) |
turn.complete |
() => ({ text: '' }). Lösen Sie es mit $.turn.complete({ turnId, answer, durationMs, isAborted: false, usage: null }) aus. |
prompt.submit |
($, e) => ({ text: e.text }) |
prompt.fill |
() => ({ isFilled: true }) |
$.prompt.read |
() => ({ value: { text: '...', cursor: 0 } }) |
$.ui.copy |
() => ({ value: { isCopied: true } }) |
$.session.messages |
() => ({ value: [{ role: 'assistant', text: '...', toolUses: [] }] }) |
$.session.id, $.agent.list |
() => ({ value: 'abc123' }), () => ({ value: [] }) |
session.send |
() => ({ isDelivered: true }). e.to kommt als String an, auch wenn Ihr Mod { sessionId } weitergegeben hat. |
session.receive |
($, e) => ({ text: e.text }). Lösen Sie es mit $.session.receive({ origin: { kind: 'peer-send-message' }, text }) aus. |
ui.render |
() => ({ type: 'Text', props: {}, children: ['...'] }) |
expect hat die Assertions toBe, toEqual, toMatch, toMatchObject, toContain, toBeDefined, toBeUndefined und toThrow, und .not vor jedem von ihnen.
Einen Timer testen
Ein Mod, der Arbeit auf einem Timer ausführt, benötigt eine Uhr, die der Test steuert, sodass der Test die Zeit vorantreiben kann, anstatt zu warten. const clock = mock.clock(on) gibt eine Mock-Uhr zurück, die bei 0 beginnt und sich nur bewegt, wenn Ihr Test sie bewegt. Um bei einer anderen Zeit zu beginnen, übergeben Sie sie in Millisekunden, wie in mock.clock(on, { now: 5000 }). Die Uhr hat diese Methoden:
| Methode | Was sie tut |
|---|---|
await clock.advance(1000) |
Bewegt die Zeit um diese viele Millisekunden vorwärts und führt jeden Timer aus, der fällig wird |
await clock.set(5000) |
Bewegt die Zeit vorwärts zu diesem Wert, wie advance es würde |
clock.now() |
Gibt die Zeit zurück, die Ihr Mods $.clock.now() zu aufgelöst wird |
await clock.settle() |
Führt Timer aus, die bereits fällig sind, wie eine Kette von $.clock.after-Aufrufen mit Null-Verzögerung, ohne die Zeit zu bewegen |
await clock.sleep(2000) |
Innerhalb eines Stubs macht dies, dass dieser Stub nur antwortet, sobald der Test so weit vorangekommen ist, was ist, wie Sie ein langsames Modell oder einen langsamen Prozess simulieren |
Dieser Hook gehört zu einem Mod namens countdown und verarbeitet einen Befehl /countdown, der eine Anzahl von Sekunden nimmt, einen $.clock.every-Timer von einer Sekunde startet und einen Toast bei Null zeigt. Wie bei grader enthält die Datei nur den Hook unter Test und registriert den Befehl nicht:
export function register(on) {
on('command.run', { command: 'countdown' }, async ($, e) => {
// e.args is the text typed after /countdown
let left = Number(e.args)
const timer = $.clock.every(1000, () => {
left -= 1
if (left === 0) {
timer.cancel()
$.ui.toast('Time is up')
}
})
// Print nothing in the transcript
return {}
})
}
Dieser Test führt /countdown 3 aus und bewegt die Mock-Uhr, sodass er drei Sekunden Verhalten überprüft, ohne drei Sekunden zu warten:
import { expect, mock, test } from 'claude-code/testing'
test('the countdown ends with a toast', async ($, on) => {
// Answer every $.clock call from a clock the test controls
const clock = mock.clock(on)
// Collect the text of each toast the mod shows
const toasts: string[] = []
on('ui.toast', ($, e) => {
toasts.push(e.text)
return { value: undefined }
})
await $.command.run({ command: 'countdown', args: '3' })
// After two seconds the timer has fired twice, and no toast is due
await clock.advance(2000)
expect(toasts).toEqual([])
// The third second brings the count to zero
await clock.advance(1000)
expect(toasts).toEqual(['Time is up'])
})
Das erste expect zeigt, dass der Toast nicht früh kommt, und das zweite zeigt, dass er einmal kommt. Jeder advance wird aufgelöst, nachdem die Timer, die fällig wurden, ausgeführt wurden, sodass die Überprüfung auf der nächsten Zeile ihre Auswirkung sieht.
Eine Zeichnung testen
Ein Test kann eine der Render-Stellen Ihres Mods zeichnen, dann Elemente drücken, eingeben und finden, die er gezeichnet hat. $.ui.mount zeichnet die Stelle durch den Hook ui.render Ihres Mods und gibt ein Handle mit einer Methode für jeden zurück. Um mehrere Apps in einem Test abzudecken, setzen Sie surface auf die App, für die gezeichnet werden soll. Dieser Test öffnet den Bereich aus Einen Bereich mit Registerkarten erstellen, wechselt Registerkarten, drückt die Schaltfläche und überprüft die Zählung im Terminal und der Desktop-App:
import { expect, test } from 'claude-code/testing'
// What Claude Code passes to a ui.render hook for this pane, apart from the app
const PANE = {
plugin: 'hello-tabs',
component: 'Pane',
requestId: 'hello-tabs',
viewport: { columns: 100, rows: 30 },
props: {
title: 'Hello tabs',
isFocused: true,
bodyColumns: 60,
placement: 'inline',
scroll: { offset: 0, bodyRows: 10 },
view: {},
},
} as const
test('the second tab counts presses and saves the count', async ($, on) => {
// Stub $.store with a Map, so the test can read what the mod saved
const saved = new Map<string, unknown>()
on('store.get', ($, e) => ({ value: saved.get(e.key) }))
on('store.set', ($, e) => {
saved.set(e.key, e.value)
return { value: undefined }
})
// Draw the pane once for each app
for (const surface of ['terminal', 'desktop'] as const) {
const ui = await $.ui.mount({ ...PANE, surface })
// Press the buttons by the key the mod gave them
await ui.press({ key: 'tab-two' })
await ui.press({ key: 'more' })
// The second tab's count line is in the drawing
expect(await ui.find({ type: 'Text', text: /^Count: \d+$/ })).toBeDefined()
await ui.unmount()
}
// One press in each app makes two
expect(saved.get('count')).toBe(2)
})
Führen Sie in Ihrer Shell claude plugin test aus dem Verzeichnis hello-tabs aus. Der Test besteht, wenn beide Apps die Zählzeile zeichnen und der Mod 2 gespeichert hat. Die Zählung wird von der ersten App zur zweiten übertragen, weil beide Mounts das gleiche geladene Modul verwenden.
Das Handle, das $.ui.mount zurückgibt, hat diese Methoden, die Elemente nach dem key adressieren, den Sie ihnen gegeben haben:
| Methode | Was sie tut |
|---|---|
press({ key: 'more' }) |
Drückt die Button mit diesem Key |
input({ key: 'new-note', text: 'buy milk' }) |
Gibt den Text in die Input mit diesem Key ein und drückt Enter. Fügen Sie kind: 'change' hinzu, um einzugeben, ohne zu senden. |
select({ key: 'size', value: 'large' }) |
Wählt die Option mit diesem Wert in der Select mit diesem Key |
find({ key: 'more' }) oder find({ type: 'Text', text: 'Count: 2' }) |
Gibt das erste übereinstimmende Element als { type, props, children } zurück, oder undefined. text kann ein String oder ein regulärer Ausdruck sein. |
unmount() |
Entfernt die Zeichnung |
Jede Methode wird aufgelöst, nachdem Ihr Handler fertig ist, sodass Sie das Ergebnis auf der nächsten Zeile überprüfen können. Setzen Sie props auf das, was Claude Code für diese Stelle übergeben würde. Die Render-Stellen-Tabelle listet die Props jeder Stelle auf, und die Typen für Ihren Build haben ihre Typen.
Ein Zeichnungstest überprüft den Baum, den Ihr Hook zurückgibt, und ob er für diese App gültig ist. Er überprüft nicht, wie die App ihn malt, daher schauen Sie sich ein neues Layout auch in einer echten Sitzung an.
Eine Zeichnung nach `/clear` testen
Jeder Test beginnt mit jedem $.state-Wert bei seinem Standard, was ist, wie /clear sie hinterlässt. Um zu testen, was Ihr Mod als nächstes tut, überspringen Sie session.start, lösen Sie classic.SessionStart mit source: 'clear' aus und überprüfen Sie, was Ihr Mod zeichnet.
Dieser Test überprüft das Modul aus Einen gespeicherten Wert nach /clear erneut laden. Fügen Sie es zur Datei aus Eine Zeichnung testen hinzu, wo PANE definiert ist. Der erste Test dieser Datei erwartet, dass die Schaltfläche die Zählung speichert, wie die Schaltfläche in Von mehr als einer Sitzung speichern es tut:
test('the saved count comes back after /clear', async ($, on) => {
// The store already holds a count of 7
on('store.get', () => ({ value: 7 }))
// Answer the event after your hook passes it on with next(e)
on('classic.SessionStart', () => ({}))
// Raise the event that fires after /clear, which runs your hook
await $.classic.SessionStart({ source: 'clear' })
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' })
await ui.press({ key: 'tab-two' })
// The pane shows the stored count, not the default of 0
expect(await ui.find({ type: 'Text', text: 'Count: 7' })).toBeDefined()
})
Der Test besteht, wenn Ihr classic.SessionStart-Hook die gespeicherte 7 in $.state kopiert hat, bevor der Bereich gezeichnet wird. Ohne diesen Hook in Ihrem Modul zeichnet der Bereich Count: 0, find gibt undefined zurück, und der Test schlägt bei toBeDefined fehl.
Einen Mod testen, der andere Mods beurteilt
Ein Mod, den Ihre Organisation in prependPlugins auflistet, kann einen anderen Mod ablehnen, bevor er geladen wird. Um einen zu testen, setzen Sie den Tier Ihres Mods und geben Sie dem Test einen zweiten Mod, den Ihrer zulassen oder ablehnen kann:
tier: rufen Sie es einmal oben in der Testdatei auf, wie intier('prepend'), um Ihren Mod alsprepend,appendoderbuiltinzu laden, seinen Platz in der Reihenfolge, in der Mods ausgeführt werden. Ohne ihn wird Ihr Mod alsusergeladen.plugins: übergeben Sietestein Optionsobjekt vor dem Test-Body. Seinplugins-Array hält Mods, die Sie inline schreiben, jeder mit einemnameund einerregister-Funktion. Um einen an einer anderen Stelle alsuserzu laden, fügen Sietierhinzu.
Diese Testdatei lädt den Policy-Mod von der Admin-Seite zuerst. Sie überprüft, dass der Policy-Mod einen Mod ablehnt, der einen Prozess startet, und einen zulässt, der nicht:
import { expect, test, tier } from 'claude-code/testing'
// Load the mod under test ahead of every other mod
tier('prepend')
// A second mod whose code calls $.process.run, which the policy blocks
const runner = {
name: 'runner',
register(on) {
on('tool.call', async ($, e, next) => {
await $.process.run(['ls'])
return { result: 'runner answered' }
})
},
}
// A second mod that calls nothing the policy blocks
const reader = {
name: 'reader',
register(on) {
on('tool.call', async ($, e, next) => {
return { result: 'reader answered' }
})
},
}
test('refuses a mod that starts a process', { plugins: [runner] }, async ($, on) => {
on('tool.call', () => ({ result: 'claude code answered' }))
let message = ''
try {
// The first call on $ loads the mods, so the refusal is thrown here
await $.tool.call({ tool: 'Bash', command: 'ls' })
} catch (error) {
message = error.message
}
expect(message).toBe('runner: refused by acme-guard: Acme policy: mods may not call process.run')
})
test('admits a mod that starts no process', { plugins: [reader] }, async ($, on) => {
on('tool.call', () => ({ result: 'claude code answered' }))
const out = await $.tool.call({ tool: 'Bash', command: 'ls' })
// The answer comes from reader, which shows that it loaded
expect(out).toEqual({ result: 'reader answered' })
})
Führen Sie in Ihrer Shell claude plugin test aus dem Verzeichnis acme-guard aus. Beide Tests bestehen mit dem Policy-Mod, wie die Admin-Seite ihn zeigt.
Das Kit lädt jeden Mod beim ersten Aufruf des Tests auf $. Wenn Ihr Mod einen ablehnt, wirft dieser Aufruf, und die Nachricht nennt den abgelehnten Mod, den Mod, der ihn ablehnt, und Ihren Grund. Im zweiten Test wird nichts abgelehnt, daher beantwortet reader den Tool-Aufruf, bevor er den Stub erreicht.
Nächste Schritte
- Einen Mod beheben: finden Sie heraus, warum ein Mod in einer Sitzung nichts tut
- Mods-Referenz: jedes Ereignis-Input und Ergebnis zum Schreiben von Stubs