Uji coba mod
Tulis tes otomatis untuk mod Claude Code yang menaikkan peristiwa, stub jawaban Claude Code, dan tekan tombol, tanpa sesi, masuk, atau jaringan.
Anda dapat menulis tes otomatis untuk mod dan menjalankannya dari shell Anda dengan claude plugin test. Tes menaikkan peristiwa yang ditangani hook Anda dan memeriksa apa yang dilakukan hook, sehingga Anda menangkap masalah sebelum mencapai sesi. Contoh pertama menguji mod dari Buat mod.
Menulis pengujian
Sebuah pengujian memuat mod Anda, mengirimkan event melalui hook-nya seperti yang dilakukan Claude Code, dan memeriksa apa yang dilakukan hook tersebut, tanpa sesi, tanpa masuk, dan tanpa jaringan. Anda menjalankan pengujian dari shell dengan claude plugin test, dan setiap file pengujian mengimpor test kit, sebuah pustaka pengujian dalam modul claude-code/testing.
Beri setiap file pengujian nama yang diakhiri dengan .test.ts, seperti first-mod.test.ts, dan simpan di mana saja di dalam direktori plugin. Setiap file pengujian memerlukan setidaknya satu test(), atau eksekusi gagal dengan declares no test(): nothing ran. Sebuah file pengujian dapat mengimpor file milik mod Anda sendiri dan helper .ts di sebelahnya, sehingga Anda dapat melakukan unit test pada fungsi biasa, seperti aturan sebuah permainan, tanpa kit tersebut.
Pengujian ini memicu dua panggilan tool, menjalankan perintah /tally dari Membuat mod, dan memeriksa bahwa balasannya menghitung keduanya. Baris pertamanya adalah sebuah stub, yang menjawab panggilan tool sebagai pengganti Claude Code. Simpan sebagai 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' }))
// Fire 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')
})
Di shell Anda, jalankan pengujian dari direktori first-mod:
claude plugin test
Output menyebutkan setiap pengujian dan apakah pengujian tersebut lulus, dengan waktu yang bervariasi dari satu eksekusi ke eksekusi lainnya:
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]
Setiap $.tool.call melewati hook tool.call milik mod, yang menambahkan satu ke hitungannya dan meneruskan panggilan tersebut ke stub. Tidak ada ls yang dijalankan dan tidak ada file yang dibaca. $.command.run kemudian menuju hook command.run milik mod, dan answer adalah objek yang dikembalikan oleh hook tersebut.
Perintah ini keluar dengan status 1 ketika sebuah pengujian gagal, sehingga dapat digunakan di CI. Jika mod Anda sendiri tidak dapat dimuat di shell yang menjalankannya, perintah ini mencetak baris yang diawali dengan claude plugin test: hooks modules are turned off beserta alasannya, dan keluar dengan status 1.
Stub untuk apa yang akan dijawab Claude Code
Tidak ada model, store, atau tool yang berjalan dalam pengujian, sehingga di mana pun mod Anda mengharapkan Claude Code menjawab, pengujian menyediakan jawabannya dengan stub. Sebuah fungsi pengujian menerima dua argumen untuk itu:
$:$milik pengujian itu sendiri, yang berperan sebagai Claude Code. Ini bukan mods API yang diterima oleh hook. Setiap metodenya memicu event dengan nama yang sama, mengirimkannya melalui hook mod Anda, dan di-resolve ke hasilnya:$.tool.call({ tool: 'Bash', command: 'ls' })memicutool.call.$.command.run,$.prompt.submit,$.session.start, dan$.turn.completebekerja dengan cara yang sama, dan$.classic.Stopserta metode$.classiclainnya memicu event hook pengaturan. Sebuah pengujian tidak dapat memicu panggilan mods API sepertiui.closesecara langsung. Picu melalui mod Anda, misalnya dengan menekan tombol yang menutup panel.on: panggil untuk mendaftarkan stub, yaitu hook yang menjawab sebagai pengganti Claude Code. Beri nama stub untuk panggilan mods API tanpa$., sehingga stub yang didaftarkan sebagaistore.getmenjawab$.store.getmilik mod Anda. Ketika mod Anda memanggil$.model.completeatau$.store.get, sebuah stub menyediakan jawabannya.
Contoh ini membuat stub untuk panggilan model. Hook ini milik mod bernama grader, dan menangani perintah /grade yang mengirimkan sebuah kalimat ke model dan melaporkan apakah balasannya diawali dengan PASS. File ini hanya berisi hook yang diuji, sehingga mod juga memerlukan plugin.json dan hooks.json, seperti di Membuat mod. Untuk mengetik /grade dalam sebuah sesi, mod juga harus mendaftarkan perintah tersebut:
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' }
})
}
Pengujian ini membuat stub untuk panggilan model guna memeriksa apa yang dilakukan hook dengan balasan yang lulus:
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')
})
Pengujian ini lulus karena reply milik hook adalah objek di bawah value, yang text-nya diawali dengan PASS. Untuk memeriksa cabang lainnya, tambahkan pengujian kedua yang stub-nya mengembalikan text yang diawali dengan FAIL, dan harapkan Try again.
Stub untuk panggilan mods API mengembalikan objek dengan field value, yang berisi nilai yang menjadi hasil resolve panggilan tersebut di mod Anda: { value: 7 } membuat $.store.get di-resolve ke 7. Stub untuk salah satu event Claude Code, seperti turn.step atau tool.call, mengembalikan hasil milik event tersebut, seperti { result: 'ok' }. $.session.send dan $.prompt.fill juga menggunakan hasil event-nya, seperti yang ditunjukkan tabel. Mencari apa yang dikembalikan stub menunjukkan bentuk mana yang digunakan oleh setiap nama umum. Error berikut berarti sebuah stub salah atau tidak ada. Output pengujian yang gagal menyertakan blok berjudul the engine reported:, dan setiap error muncul di sana:
returned neither { value } nor { deny }: stub untuk panggilan mods API mengembalikan nilai polos, yang membuat pengujian gagalno implementation fordiikuti sebuah nama: mod Anda melakukan panggilan tersebut dan tidak ada stub yang menjawabnya
Kit ini juga mengekspor mock siap pakai untuk jam, store, environment variable, dan baris yang ditambahkan ke percakapan:
mock.clock(on): menjawab$.clockdan mengembalikan jam tiruan yang dimajukan oleh pengujian Anda, sehingga pengujian sebuah timer tidak perlu menunggu.mock.store(on, { count: 7 }): menjawab$.storedari store yang dimulai dengan entri tersebut. Mock ini tidak mengembalikan apa pun, jadi untuk memeriksa apa yang disimpan mod Anda, tulis sendiri dua stubstoreseperti yang dilakukan pengujian gambar.mock.env(on, { CI: 'true' }): menjawab$.env.getdari variabel tersebut.mock.session(on): mengembalikan sesi tiruan yang metodeappended()-nya mencantumkan baris yang ditambahkan mod Anda dengan$.session.append, dari yang terlama; mock ini memerlukan Claude Code v2.1.293 atau yang lebih baru.
Mengikuti aturan test kit
Test kit memiliki beberapa aturan tersendiri, dan melanggar salah satunya menghasilkan error yang pertama kali dijumpai oleh penulis pengujian baru:
-
Daftarkan setiap stub sebelum panggilan pertama pengujian pada
$. Memanggilonsetelah itu akan melempar error sepertion("ui.render") after the test first called $. -
session.starttidak berjalan dengan sendirinya. Setiap pengujian dimulai dengan modul Anda yang baru dimuat dan belum ada hook-nya yang dipanggil, sehingga variabel tingkat modul menyimpan nilai awalnya. Jika sebuah hook bergantung pada apa yang disiapkan olehsession.start, picu terlebih dahulu:// 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 })) // Fire the event, which runs your session.start hook await $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })Stub kedua menjawab panggilan
$.command.registeryang dilakukan oleh hooksession.startseperti milik tutorial. Tanpanya, panggilan tersebut ditolak denganno implementation for command.registerdan kit melewati hook Anda, sehingga tidak ada apa pun setelah panggilan tersebut di dalam hook yang berjalan. Pengujian tidak gagal pada titik itu. Hook yang dilewati dicantumkan di bawahthe engine reported:hanya jika pemeriksaan berikutnya gagal. -
Hook yang mengembalikan
next(e)memerlukan stub untuk menjawab. Ketika hookui.renderAnda mengembalikannext(e), misalnya untuk tidak menggambar apa pun saat Claude sedang idle, memasangnya gagal denganno implementation for ui.render. Daftarkan stub yang mengembalikan sebuah elemen sebagai data polos:// Stands for what Claude Code would draw at the site on('ui.render', () => ({ type: 'Text', props: {}, children: ['drawn by Claude Code'] }))Dengan stub terdaftar, pemasangan berhasil, dan
ui.find({ type: 'Text' })mengembalikan elemen tersebut setiap kali hook Anda mengembalikannext(e). -
Stub untuk
turn.stepadalah async generator, dan pengujian membaca stream hingga akhir untuk mendapatkan hasilnya: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 } }) // Fire 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.valueKetika loop berakhir,
resultadalah objek yang dikembalikan stub, setelah hookturn.stepAnda berkesempatan mengubahnya. Di siniresult.answerbernilai'ok'. -
Picu panggilan tool dengan nama tool dan argumennya sebagai field, seperti
await $.tool.call({ tool: 'Bash', command: 'ls' }), dan daftarkan stubtool.callyang mengembalikan{ result }.
Mencari apa yang dikembalikan stub
Setiap panggilan mods API yang dilakukan mod Anda dalam pengujian memerlukan stub yang menjawab sebagai pengganti Claude Code, kecuali beberapa yang dijawab oleh kit itu sendiri: panggilan $.ui.invalidate, $.state, dan $.session.append. Untuk panggilan $.clock, gunakan mock.clock(on), atau $.clock.now() milik mod Anda gagal dengan no implementation for clock.now.
Tabel ini mencantumkan yang paling sering digunakan mod. Kolom pertama adalah panggilan yang dilakukan mod Anda atau event yang diteruskannya dengan next(e). Kolom kedua adalah fungsi yang diberikan ke on dengan nama tersebut, sehingga baris $.store.get menjadi on('store.get', ($, e) => ({ value: saved.get(e.key) })). Sebuah '...' dalam stub menandai teks yang perlu Anda isi:
| Mod Anda memanggil atau meneruskan | Stub |
|---|---|
$.command.register, $.tool.register, $.ui.toast, $.ui.log, $.ui.status, $.ui.close, $.store.set |
() => ({ value: undefined }). Untuk ui.toast dan ui.log, teksnya adalah e.text. |
$.store.get |
($, e) => ({ value: saved.get(e.key) }) |
$.fs.read |
($, e) => ({ value: e.path.endsWith('notes.md') ? '# Notes' : '' }). e.path tiba sebagai path absolut, jadi bandingkan dengan endsWith. |
$.ui.open |
() => ({ value: { isPlaced: true } }) |
$.ui.ask |
Stub tool.call, karena pertanyaan tersebut mencapainya sebagai panggilan ke tool AskUserQuestion: ($, e) => ({ result: { answers: { [e.questions[0].question]: 'Run it' } } }). Periksa e.tool terlebih dahulu jika mod Anda meneruskan panggilan tool lainnya. |
$.model.complete |
() => ({ value: { isAnswered: true, text: '...', usage } }) |
$.process.run |
($, e) => ({ value: { exitCode: 0, stdout: '...', stderr: '' } }). e.argv adalah daftar argumen dan e.init berisi cwd dan timeoutMs. |
| Panggilan mods API apa pun yang seharusnya gagal | () => ({ deny: 'the reason' }), yang membuat panggilan tersebut ditolak di mod Anda. Stub yang melempar error justru dilewati. |
session.start |
() => ({ cwd: '/work' }) |
turn.start |
($, e) => ({ turnId: e.turnId }) |
tool.call |
() => ({ result: '...' }) |
turn.complete |
() => ({ text: '' }). Picu dengan $.turn.complete({ turnId, answer, durationMs, isAborted: false, usage: null }). |
prompt.submit |
($, e) => ({ text: e.text }) |
prompt.fill |
() => ({ isFilled: true }) |
$.prompt.read |
() => ({ value: { text: '...', cursor: 0 } }) |
$.ui.copy |
() => ({ value: { isCopied: true } }) |
$.ui.notify |
() => ({ value: { isSent: true, channel: 'terminal_bell' } }). Teksnya adalah e.text dan judulnya adalah e.title. |
$.session.messages |
() => ({ value: [{ role: 'assistant', text: '...', toolUses: [] }] }) |
$.session.id, $.agent.list |
() => ({ value: 'abc123' }), () => ({ value: [] }) |
session.send |
() => ({ isDelivered: true }). e.to tiba sebagai string bahkan ketika mod Anda memberikan { sessionId }. |
session.receive |
($, e) => ({ text: e.text }). Picu dengan $.session.receive({ origin: { kind: 'peer-send-message' }, text }). |
ui.render |
() => ({ type: 'Text', props: {}, children: ['...'] }) |
Memeriksa hasil dengan expect
Kit ini memiliki asersi berikut, dan asersi yang Anda pilih ditempatkan setelah nilai yang sedang Anda periksa, seperti dalam expect(answer.text).toBe('ok'):
- Kesetaraan:
toBeuntuk nilai atau objek yang sama,toEqualuntuk struktur,toStrictEqualuntuk struktur beserta fieldundefineddan prototipe, sertatoMatchObjectuntuk field yang Anda sebutkan - Teks:
toMatch, yang menerima regular expression atau substring,toStartWith, dantoEndWith - Angka:
toBeGreaterThan,toBeGreaterThanOrEqual,toBeLessThan,toBeLessThanOrEqual - Isi:
toContainuntuk substring atau anggota,toContainEqualuntuk anggota yang dibandingkan berdasarkan struktur,toHaveLength, dantoHaveProperty - Keberadaan dan tipe:
toBeDefined,toBeUndefined,toBeNull,toBeTruthy,toBeFalsy,toBeNaN,toBeInstanceOf - Pelemparan error:
toThrow, untuk fungsi yang dipanggil olehexpect, atau untuk promise yang Anda periksa dengan.rejects
Letakkan .not di depan sebuah asersi untuk memeriksa kebalikannya, dan .resolves atau .rejects untuk memeriksa nilai akhir sebuah promise.
Ketika sebuah expect gagal di dalam stub atau hook yang Anda berikan ke on sebagai fungsi biasa, bukan sebagai async generator, pengujian gagal. Engine melewati hook tersebut, dan output kegagalan menyebutkannya, seperti in the test's store.set hook.
Uji coba timer
Mod yang menjalankan pekerjaan pada timer memerlukan jam yang dikontrol tes, sehingga tes dapat memajukan waktu alih-alih menunggu. const clock = mock.clock(on) mengembalikan jam mock yang dimulai pada 0 dan bergerak hanya ketika tes Anda memindahkannya. Untuk memulai pada waktu lain, teruskan dalam milidetik, seperti dalam mock.clock(on, { now: 5000 }). Jam memiliki metode ini:
| Metode | Apa yang dilakukannya |
|---|---|
await clock.advance(1000) |
Memajukan waktu sebanyak milidetik itu dan menjalankan setiap timer yang jatuh tempo |
await clock.set(5000) |
Memajukan waktu ke nilai itu, seperti yang akan dilakukan advance |
clock.now() |
Mengembalikan waktu, yang merupakan apa yang diselesaikan $.clock.now() mod Anda |
await clock.settle() |
Menjalankan timer yang sudah jatuh tempo, seperti rantai panggilan $.clock.after penundaan nol, tanpa memajukan waktu |
await clock.sleep(2000) |
Di dalam stub, membuat stub itu menjawab hanya setelah tes telah maju sejauh itu, yang adalah cara Anda mensimulasikan model atau proses yang lambat |
Hook ini milik mod bernama countdown, dan menangani perintah /countdown yang mengambil jumlah detik, memulai timer $.clock.every satu detik, dan menunjukkan toast pada nol. Seperti dengan grader, file hanya menyimpan hook yang diuji dan tidak mendaftarkan perintah:
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 {}
})
}
Tes ini menjalankan /countdown 3 dan memajukan jam mock, sehingga memeriksa tiga detik perilaku tanpa menunggu tiga detik:
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'])
})
expect pertama menunjukkan bahwa toast tidak datang lebih awal, dan yang kedua menunjukkan bahwa itu datang sekali. Setiap advance menyelesaikan setelah timer yang jatuh tempo telah berjalan, sehingga pemeriksaan pada baris berikutnya melihat efeknya.
Uji coba gambar
Tes dapat menggambar salah satu situs render mod Anda, kemudian menekan, mengetik ke, dan menemukan elemen yang digambarnya. $.ui.mount menggambar situs melalui hook ui.render mod Anda dan mengembalikan handle dengan metode untuk masing-masing. Untuk mencakup beberapa aplikasi dalam satu tes, atur surface ke aplikasi untuk digambar. Tes ini membuka panel dari Bangun panel dengan tab, beralih tab, menekan tombol, dan memeriksa hitungan di terminal dan aplikasi Desktop:
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)
})
Di shell Anda, jalankan claude plugin test dari direktori hello-tabs. Tes lulus ketika kedua aplikasi menggambar garis hitungan dan mod telah menyimpan 2. Hitungan dibawa dari aplikasi pertama ke yang kedua karena kedua pemasangan menggunakan modul yang dimuat sama.
Handle yang dikembalikan $.ui.mount memiliki metode ini, yang mengatasi elemen berdasarkan key yang Anda berikan:
| Metode | Apa yang dilakukannya |
|---|---|
press({ key: 'more' }) |
Menekan Button dengan kunci itu |
input({ key: 'new-note', text: 'buy milk' }) |
Mengetik teks ke Input dengan kunci itu dan menekan Enter. Tambahkan kind: 'change' untuk mengetik tanpa mengirimkan. Untuk field dengan rows, teks dapat memuat \n. |
select({ key: 'size', value: 'large' }) |
Memilih opsi dengan nilai itu dalam Select dengan kunci itu |
find({ key: 'more' }) atau find({ type: 'Text', text: 'Count: 2' }) |
Mengembalikan elemen pertama yang cocok sebagai { type, props, children }, atau undefined. text dapat berupa string atau ekspresi reguler. |
unmount() |
Menghapus gambar |
Setiap metode menyelesaikan setelah handler Anda selesai, sehingga Anda dapat memeriksa hasil pada baris berikutnya. Atur props ke apa yang akan dilewatkan Claude Code untuk situs itu. Tabel situs render mencantumkan props setiap situs, dan tipe untuk build Anda memiliki tipe mereka.
Untuk menangkap pohon elemen yang tidak valid untuk aplikasi yang Anda sebutkan di surface, periksa dengan find elemen yang ditambahkan hook Anda. Tes tidak memeriksa bagaimana aplikasi melukis pohon, jadi lihat tata letak baru dalam sesi nyata juga.
Uji coba gambar setelah /clear
Setiap tes dimulai dengan setiap nilai $.state pada default-nya, yang merupakan cara /clear meninggalkannya. Untuk menguji apa yang dilakukan mod Anda selanjutnya, lewati session.start, picu classic.SessionStart dengan source: 'clear', dan periksa apa yang digambar mod Anda.
Tes ini memeriksa modul dari Muat nilai yang disimpan lagi setelah /clear. Tambahkan ke file dari Uji coba gambar, di mana PANE didefinisikan. Tes pertama file itu mengharapkan tombol untuk menyimpan hitungan, seperti tombol dalam Simpan dari lebih dari satu sesi:
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', () => ({}))
// Fire the event that follows /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()
})
Tes lulus ketika hook classic.SessionStart Anda telah menyalin 7 yang disimpan ke $.state sebelum panel menggambar. Tanpa hook itu dalam modul Anda, panel menggambar Count: 0, find mengembalikan undefined, dan tes gagal di toBeDefined.
Uji coba mod kebijakan
Mod yang tercantum organisasi Anda dalam prependPlugins dapat menolak mod lain sebelum dimuat. Untuk menguji satu, atur tier mod Anda dan berikan tes mod kedua untuk diizinkan atau ditolak oleh mod Anda:
tier: panggil sekali di bagian atas file tes, seperti dalamtier('prepend'), untuk memuat mod Anda sebagaiprepend,append, ataubuiltin, tempatnya dalam urutan mod berjalan. Tanpa itu, mod Anda memuat sebagaiuser.plugins: teruskantestobjek opsi di depan badan tes. Arraypluginsmenyimpan mod yang Anda tulis inline, masing-masing dengannamedan fungsiregister. Untuk memuat satu di tempat lain selainuser, tambahkantierke itu.
File tes ini memuat mod kebijakan dari halaman admin terlebih dahulu. Itu memeriksa bahwa mod kebijakan menolak mod yang memulai proses dan mengizinkan yang tidak:
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' })
})
Di shell Anda, jalankan claude plugin test dari direktori acme-guard. Kedua tes lulus dengan mod kebijakan seperti yang ditunjukkan halaman admin.
Kit memuat setiap mod pada panggilan pertama tes pada $. Ketika mod Anda menolak satu, panggilan itu melempar, dan pesan menamai mod yang ditolak, mod yang menolaknya, dan alasan Anda. Dalam tes kedua tidak ada yang ditolak, jadi reader menjawab panggilan alat sebelum mencapai stub.
Langkah berikutnya
- Troubleshoot mod: cari tahu mengapa mod tidak melakukan apa pun dalam sesi
- Referensi mods: setiap peristiwa input dan hasil, untuk menulis stub