> ## Documentation Index
> Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# 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`](/docs/id/plugins/mods/reference#commands). 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](/docs/id/plugins/mods/create).

<h2 id="write-a-test">
  Tulis tes
</h2>

Tes memuat mod Anda, mengirim peristiwa melalui hook dengan cara Claude Code akan melakukannya, dan memeriksa apa yang dilakukan hook, tanpa sesi, masuk, atau jaringan. Anda menjalankan tes dari shell Anda dengan `claude plugin test`, dan setiap file tes mengimpor test kit, perpustakaan tes dalam modul `claude-code/testing`.

Berikan setiap file tes nama yang diakhiri dengan `.test.ts`, seperti `first-mod.test.ts`, dan simpan di mana saja dalam direktori plugin. Setiap file tes memerlukan setidaknya satu `test()`, atau jalankan gagal dengan `declares no test(): nothing ran`. File tes dapat mengimpor file mod Anda sendiri dan pembantu `.ts` saudara, sehingga Anda dapat menguji unit fungsi biasa, seperti aturan permainan, tanpa kit.

Tes ini menaikkan dua panggilan alat, menjalankan perintah `/tally` dari [Buat mod](/docs/id/plugins/mods/create), dan memeriksa bahwa balasan menghitung keduanya. Baris pertamanya adalah [stub](#stub-what-claude-code-would-answer), yang menjawab panggilan alat di tempat Claude Code. Simpan sebagai `first-mod/tests/first-mod.test.ts`:

```typescript first-mod/tests/first-mod.test.ts theme={null}
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')
})
```

Di shell Anda, jalankan tes dari direktori `first-mod`:

```bash theme={null}
claude plugin test
```

Output menamai setiap tes dan apakah itu lulus, dengan waktu yang bervariasi dari jalankan ke jalankan:

```text theme={null}
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` melalui hook [`tool.call`](/docs/id/plugins/mods/reference#tools) mod, yang menambah satu ke hitungannya dan meneruskan panggilan ke stub. Tidak ada `ls` yang berjalan dan tidak ada file yang dibaca. `$.command.run` kemudian pergi ke hook [`command.run`](/docs/id/plugins/mods/reference#commands-and-configuration) mod, dan `answer` adalah objek yang dikembalikan hook.

Perintah keluar dengan status 1 ketika tes gagal, sehingga berfungsi di CI. Jika mod Anda sendiri tidak dapat dimuat di shell yang menjalankannya, itu mencetak baris yang dimulai dengan `claude plugin test: hooks modules are turned off` dengan alasannya, dan keluar dengan status 1.

<h3 id="stub-what-claude-code-would-answer">
  Stub apa yang akan dijawab Claude Code
</h3>

Tidak ada model, toko, atau alat yang berjalan dalam tes, jadi di mana pun mod Anda mengharapkan Claude Code untuk menjawab, tes menyediakan jawaban dengan stub. Fungsi tes menerima dua argumen untuk itu:

* **`$`**: `$` tes sendiri, yang berdiri di mana Claude Code berada. Ini bukan [mods API](/docs/id/plugins/mods/reference#mods-api-methods) yang diterima hook. Setiap metodenya menaikkan peristiwa dengan nama yang sama, mengirimnya melalui hook mod Anda, dan menyelesaikan ke hasil: `$.tool.call({ tool: 'Bash', command: 'ls' })` menaikkan `tool.call`. `$.command.run`, `$.prompt.submit`, `$.session.start`, dan `$.turn.complete` bekerja dengan cara yang sama, dan `$.classic.Stop` dan metode `$.classic` lainnya menaikkan [peristiwa hook pengaturan](/docs/id/plugins/mods/events#hook-the-settings-hook-events). Tes tidak dapat menaikkan panggilan mods API seperti `ui.close` secara langsung. Picu melalui mod Anda, misalnya dengan menekan tombol yang menutup panel.
* **`on`**: panggil untuk mendaftarkan stub, yang merupakan hook yang menjawab di tempat Claude Code. Beri nama stub untuk panggilan mods API tanpa `$.`, jadi stub yang didaftarkan sebagai `store.get` menjawab `$.store.get` mod Anda. Ketika mod Anda memanggil [`$.model.complete`](/docs/id/plugins/mods/api#call-a-model) atau [`$.store.get`](/docs/id/plugins/mods/interface#keep-state), stub menyediakan jawaban.

Contoh ini stub panggilan model. Hook milik mod bernama `grader`, dan menangani perintah `/grade` yang mengirim kalimat ke model dan melaporkan apakah balasan dimulai dengan `PASS`. File hanya menyimpan hook yang diuji, jadi mod juga memerlukan `plugin.json` dan `hooks.json`, seperti dalam [Buat mod](/docs/id/plugins/mods/create#write-a-mod-yourself). Untuk mengetik `/grade` dalam sesi, mod juga harus [mendaftarkan perintah](/docs/id/plugins/mods/api#add-a-command):

```javascript grader/hooks/register.js theme={null}
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' }
  })
}
```

Tes ini stub panggilan model untuk memeriksa apa yang dilakukan hook dengan balasan yang lulus:

```typescript grader/tests/grader.test.ts theme={null}
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')
})
```

Tes lulus karena `reply` hook adalah objek di bawah `value`, yang `text` dimulai dengan `PASS`. Untuk memeriksa cabang lain, tambahkan tes kedua yang stub mengembalikan `text` yang dimulai dengan `FAIL`, dan harapkan `Try again`.

Stub untuk panggilan mods API mengembalikan objek dengan bidang `value`, yang menyimpan apa yang dipanggil dalam mod Anda: `{ value: 7 }` membuat `$.store.get` menyelesaikan ke `7`. Stub untuk salah satu peristiwa Claude Code, seperti [`turn.step`](/docs/id/plugins/mods/reference#turns) atau `tool.call`, mengembalikan hasil peristiwa itu sendiri, seperti `{ result: 'ok' }`. `$.session.send` dan `$.prompt.fill` mengambil hasil peristiwa juga, seperti yang ditunjukkan tabel. [Lihat apa yang dikembalikan stub](#look-up-what-a-stub-returns) menunjukkan bentuk mana yang diambil setiap nama umum. Dua kesalahan berarti stub salah atau hilang. Output tes yang gagal mencakup blok yang dikepalai `the engine reported:`, dan setiap kesalahan muncul di sana:

* `returned neither { value } nor { deny }`: stub untuk panggilan mods API mengembalikan nilai telanjang
* `no implementation for` diikuti oleh nama: mod Anda membuat panggilan itu dan tidak ada stub yang menjawabnya

Kit juga mengekspor mock dalam memori yang menjawab seluruh namespace untuk Anda. `mock.clock(on)` menjawab [`$.clock`](/docs/id/plugins/mods/api#run-work-in-the-background), `mock.store(on, { count: 7 })` menjawab `$.store` dari toko yang dimulai dengan entri tersebut, dan `mock.env(on, { CI: 'true' })` menjawab `$.env.get` dari variabel tersebut. `mock.clock` mengembalikan jam mock yang tes Anda maju, sehingga tes timer tidak menunggu. `mock.store` mengembalikan tidak ada, jadi untuk memeriksa apa yang disimpan mod Anda, tulis dua stub `store` sendiri seperti [tes gambar](#test-a-drawing) lakukan.

<h3 id="follow-the-test-kit’s-rules">
  Ikuti aturan test kit
</h3>

Test kit memiliki beberapa aturan sendiri, dan melanggar satu menghasilkan kesalahan yang ditemui penulis tes baru terlebih dahulu:

* **Daftarkan setiap stub sebelum panggilan pertama tes pada `$`.** Memanggil `on` setelah itu melempar kesalahan seperti `on("ui.render") after the test first called $`.

* **[`session.start`](/docs/id/plugins/mods/reference#session) tidak berjalan dengan sendirinya.** Setiap tes dimulai dengan modul Anda dimuat segar dan tidak ada hook yang dipanggil, jadi variabel tingkat modul menyimpan nilai awal mereka. Jika hook bergantung pada apa yang ditetapkan `session.start`, naikkan terlebih dahulu:

  ```typescript theme={null}
  // 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' })
  ```

  Stub kedua menjawab panggilan `$.command.register` yang dibuat hook `session.start` seperti [tutorial](/docs/id/plugins/mods/create#write-a-mod-yourself). Tanpa itu, panggilan itu menolak dengan `no implementation for command.register` dan kit melewati hook Anda, jadi tidak ada yang setelah panggilan dalam hook yang berjalan. Tes tidak gagal pada titik itu. Hook yang dilewati hanya tercantum di bawah `the engine reported:` jika pemeriksaan nanti gagal.

* **Hook yang mengembalikan `next(e)` memerlukan stub untuk menjawab.** Ketika hook [`ui.render`](/docs/id/plugins/mods/reference#interface) Anda mengembalikan `next(e)`, misalnya untuk tidak menggambar apa pun saat Claude menganggur, [memasangnya](#test-a-drawing) gagal dengan `no implementation for ui.render`. Daftarkan stub yang mengembalikan elemen sebagai data biasa:

  ```typescript theme={null}
  // 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 itu setiap kali hook Anda mengembalikan `next(e)`.

* **Stub untuk `turn.step` adalah generator async**, dan tes membaca aliran ke akhirnya untuk mendapatkan hasil:

  ```typescript theme={null}
  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.value
  ```

  Ketika loop berakhir, `result` adalah objek yang dikembalikan stub, setelah hook `turn.step` Anda memiliki kesempatan untuk mengubahnya. Di sini `result.answer` adalah `'ok'`.

* **Naikkan panggilan alat dengan nama alat dan argumen sebagai bidang**, seperti `await $.tool.call({ tool: 'Bash', command: 'ls' })`, dan daftarkan stub `tool.call` yang mengembalikan `{ result }`.

<h3 id="look-up-what-a-stub-returns">
  Lihat apa yang dikembalikan stub
</h3>

Setiap panggilan mods API yang dibuat mod Anda dalam tes memerlukan stub yang menjawab di tempat Claude Code, kecuali beberapa yang dijawab kit sendiri: panggilan [`$.ui.invalidate`](/docs/id/plugins/mods/interface#redraw-when-something-changes) dan [`$.state`](/docs/id/plugins/mods/interface#keep-state). Untuk panggilan `$.clock`, gunakan `mock.clock(on)`, atau `$.clock.now()` mod Anda gagal dengan `no implementation for clock.now`.

Tabel ini mencantumkan yang paling sering digunakan mod. Kolom pertama adalah panggilan yang dibuat mod Anda atau peristiwa yang dilewatkan dengan `next(e)`. Kolom kedua adalah fungsi untuk diteruskan ke `on` di bawah nama itu, jadi baris `$.store.get` menjadi `on('store.get', ($, e) => ({ value: saved.get(e.key) }))`. `'...'` dalam stub menandai teks untuk 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 jalur absolut, jadi bandingkan dengan `endsWith`. |
| `$.ui.open` | `() => ({ value: { isPlaced: true } })` |
| `$.ui.ask` | Stub `tool.call`, karena pertanyaan mencapainya sebagai panggilan ke alat `AskUserQuestion`: `($, e) => ({ result: { answers: { [e.questions[0].question]: 'Run it' } } })`. Periksa `e.tool` terlebih dahulu jika mod Anda meneruskan panggilan alat lain. |
| `$.model.complete` | `() => ({ value: { isAnswered: true, text: '...', usage } })` |
| `$.process.run` | `($, e) => ({ value: { exitCode: 0, stdout: '...', stderr: '' } })`. `e.argv` adalah daftar argumen dan `e.init` menyimpan `cwd` dan `timeoutMs`. |
| Panggilan mods API apa pun yang harus gagal | `() => ({ deny: 'the reason' })`, yang membuat panggilan menolak dalam mod Anda. Stub yang melempar dilewati sebagai gantinya. |
| `session.start` | `() => ({ cwd: '/work' })` |
| `turn.start` | `($, e) => ({ turnId: e.turnId })` |
| `tool.call` | `() => ({ result: '...' })` |
| `turn.complete` | `() => ({ text: '' })`. Naikkan 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 } })` |
| `$.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 melewatkan `{ sessionId }`. |
| `session.receive` | `($, e) => ({ text: e.text })`. Naikkan dengan `$.session.receive({ origin: { kind: 'peer-send-message' }, text })`. |
| `ui.render` | `() => ({ type: 'Text', props: {}, children: ['...'] })` |

`expect` memiliki asersi `toBe`, `toEqual`, `toMatch`, `toMatchObject`, `toContain`, `toBeDefined`, `toBeUndefined`, dan `toThrow`, dan `.not` sebelum salah satu dari mereka.

<h2 id="test-a-timer">
  Uji coba timer
</h2>

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:

```javascript countdown/hooks/register.js theme={null}
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:

```typescript countdown/tests/countdown.test.ts theme={null}
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.

<h2 id="test-a-drawing">
  Uji coba gambar
</h2>

Tes dapat menggambar salah satu [situs render](/docs/id/plugins/mods/reference#render-sites) 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](/docs/id/plugins/mods/interface#build-a-pane-with-tabs), beralih tab, menekan tombol, dan memeriksa hitungan di terminal dan aplikasi Desktop:

```typescript hello-tabs/tests/hello-tabs.test.ts theme={null}
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. |
| `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](/docs/id/plugins/mods/reference#render-sites) mencantumkan props setiap situs, dan [tipe untuk build Anda](/docs/id/plugins/mods/create#get-the-types-for-your-build) memiliki tipe mereka.

Tes gambar memeriksa pohon yang dikembalikan hook Anda dan apakah itu valid untuk aplikasi itu. Itu tidak memeriksa bagaimana aplikasi melukisnya, jadi lihat tata letak baru dalam sesi nyata juga.

<h3 id="test-a-drawing-after-clear">
  Uji coba gambar setelah `/clear`
</h3>

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`, naikkan `classic.SessionStart` dengan `source: 'clear'`, dan periksa apa yang digambar mod Anda.

Tes ini memeriksa modul dari [Muat nilai yang disimpan lagi setelah `/clear`](/docs/id/plugins/mods/interface#load-a-saved-value-again-after-clear). Tambahkan ke file dari [Uji coba gambar](#test-a-drawing), di mana `PANE` didefinisikan. Tes pertama file itu mengharapkan tombol untuk menyimpan hitungan, seperti tombol dalam [Simpan dari lebih dari satu sesi](/docs/id/plugins/mods/interface#save-from-more-than-one-session):

```typescript hello-tabs/tests/hello-tabs.test.ts theme={null}
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()
})
```

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`.

<h2 id="test-a-mod-that-judges-other-mods">
  Uji coba mod yang menilai mod lain
</h2>

Mod yang tercantum organisasi Anda dalam [`prependPlugins`](/docs/id/plugins/mods/admin) dapat menolak mod lain sebelum dimuat. Untuk menguji satu, atur tier mod Anda dan berikan tes mod kedua untuk diterima atau ditolak:

* **`tier`**: panggil sekali di bagian atas file tes, seperti dalam `tier('prepend')`, untuk memuat mod Anda sebagai `prepend`, `append`, atau `builtin`, tempatnya dalam [urutan mod berjalan](/docs/id/plugins/mods/events#the-order-mods-run-in). Tanpa itu, mod Anda memuat sebagai `user`.
* **`plugins`**: teruskan `test` objek opsi di depan badan tes. Array `plugins` menyimpan mod yang Anda tulis inline, masing-masing dengan `name` dan fungsi `register`. Untuk memuat satu di tempat lain selain `user`, tambahkan `tier` ke itu.

File tes ini memuat [mod kebijakan dari halaman admin](/docs/id/plugins/mods/admin#enforce-a-policy-with-a-mod-of-your-own) terlebih dahulu. Itu memeriksa bahwa mod kebijakan menolak mod yang memulai proses dan mengakui yang tidak:

```typescript acme-guard/tests/guard.test.ts theme={null}
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.

<h2 id="next-steps">
  Langkah berikutnya
</h2>

* [Troubleshoot mod](/docs/id/plugins/mods/troubleshoot): cari tahu mengapa mod tidak melakukan apa pun dalam sesi
* [Referensi mods](/docs/id/plugins/mods/reference): setiap peristiwa input dan hasil, untuk menulis stub
