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

# Tester un mod

> Écrivez des tests automatisés pour un mod Claude Code qui lèvent des événements, remplacent les réponses de Claude Code et appuient sur des boutons, sans session, connexion ou réseau.

Vous pouvez écrire des tests automatisés pour un mod et les exécuter depuis votre shell avec [`claude plugin test`](/docs/fr/plugins/mods/reference#commands). Un test lève les événements que vos hooks gèrent et vérifie ce que les hooks ont fait, afin que vous détectiez un problème avant qu'il n'atteigne une session. Le premier exemple teste le mod de [Créer un mod](/docs/fr/plugins/mods/create).

<h2 id="write-a-test">
  Écrire un test
</h2>

Un test charge votre mod, envoie des événements via ses hooks de la manière que Claude Code le ferait, et vérifie ce que les hooks ont fait, sans session, connexion ou réseau. Vous exécutez les tests depuis votre shell avec `claude plugin test`, et chaque fichier de test importe le kit de test, une bibliothèque de test dans le module `claude-code/testing`.

Donnez à chaque fichier de test un nom qui se termine par `.test.ts`, comme `first-mod.test.ts`, et enregistrez-le n'importe où dans le répertoire du plugin. Chaque fichier de test a besoin d'au moins un `test()`, sinon l'exécution échoue avec `declares no test(): nothing ran`. Un fichier de test peut importer vos propres fichiers du mod et les helpers `.ts` frères, afin que vous puissiez tester les fonctions simples, comme les règles d'un jeu, sans le kit.

Ce test lève deux appels d'outils, exécute la commande `/tally` de [Créer un mod](/docs/fr/plugins/mods/create), et vérifie que la réponse compte les deux. Sa première ligne est un [stub](#stub-what-claude-code-would-answer), qui répond aux appels d'outils à la place de Claude Code. Enregistrez-le sous `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')
})
```

Dans votre shell, exécutez les tests depuis le répertoire `first-mod` :

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

La sortie nomme chaque test et s'il a réussi, avec des timings qui varient d'une exécution à l'autre :

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

Chaque `$.tool.call` a traversé le hook [`tool.call`](/docs/fr/plugins/mods/reference#tools) du mod, qui a ajouté un à son compte et a transmis l'appel au stub. Aucun `ls` n'a été exécuté et aucun fichier n'a été lu. `$.command.run` a ensuite accédé au hook [`command.run`](/docs/fr/plugins/mods/reference#commands-and-configuration) du mod, et `answer` est l'objet que ce hook a retourné.

La commande se termine avec le statut 1 quand un test échoue, afin qu'elle fonctionne dans CI. Si vos propres mods ne peuvent pas se charger dans le shell qui l'exécute, il imprime une ligne commençant par `claude plugin test: hooks modules are turned off` avec la raison, et se termine avec le statut 1.

<h3 id="stub-what-claude-code-would-answer">
  Remplacer ce que Claude Code répondrait
</h3>

Aucun modèle, magasin ou outil ne s'exécute dans un test, donc partout où votre mod s'attend à ce que Claude Code réponde, le test fournit la réponse avec un stub. Une fonction de test reçoit deux arguments pour cela :

* **`$`** : le propre `$` du test, qui se tient à la place de Claude Code. Ce n'est pas l'[API des mods](/docs/fr/plugins/mods/reference#mods-api-methods) qu'un hook reçoit. Chacune de ses méthodes lève l'événement du même nom, l'envoie via les hooks de votre mod, et se résout au résultat : `$.tool.call({ tool: 'Bash', command: 'ls' })` lève `tool.call`. `$.command.run`, `$.prompt.submit`, `$.session.start`, et `$.turn.complete` fonctionnent de la même manière, et `$.classic.Stop` et les autres méthodes `$.classic` lèvent un [événement de hook de paramètres](/docs/fr/plugins/mods/events#hook-the-settings-hook-events). Un test ne peut pas lever directement un appel d'API des mods comme `ui.close`. Déclenchez-le via votre mod, par exemple en appuyant sur le bouton qui ferme le volet.
* **`on`** : appelez-le pour enregistrer des stubs, qui sont des hooks qui répondent à la place de Claude Code. Nommez un stub pour un appel d'API des mods sans le `$.`, afin qu'un stub enregistré comme `store.get` réponde à votre mod `$.store.get`. Quand votre mod appelle [`$.model.complete`](/docs/fr/plugins/mods/api#call-a-model) ou [`$.store.get`](/docs/fr/plugins/mods/interface#keep-state), un stub fournit la réponse.

Cet exemple remplace un appel de modèle. Le hook appartient à un mod nommé `grader`, et gère une commande `/grade` qui envoie une phrase à un modèle et rapporte si la réponse commence par `PASS`. Le fichier ne contient que le hook testé, donc le mod a également besoin d'un `plugin.json` et d'un `hooks.json`, comme dans [Créer un mod](/docs/fr/plugins/mods/create#write-a-mod-yourself). Pour taper `/grade` dans une session, le mod doit également [enregistrer la commande](/docs/fr/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' }
  })
}
```

Ce test remplace l'appel de modèle pour vérifier ce que le hook fait avec une réponse réussie :

```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')
})
```

Le test réussit parce que le `reply` du hook est l'objet sous `value`, dont le `text` commence par `PASS`. Pour vérifier l'autre branche, ajoutez un deuxième test dont le stub retourne un `text` qui commence par `FAIL`, et attendez `Try again`.

Un stub pour un appel d'API des mods retourne un objet avec un champ `value`, qui contient ce que l'appel se résout en dans votre mod : `{ value: 7 }` fait que `$.store.get` se résout à `7`. Un stub pour l'un des événements de Claude Code, comme [`turn.step`](/docs/fr/plugins/mods/reference#turns) ou `tool.call`, retourne le propre résultat de cet événement, comme `{ result: 'ok' }`. `$.session.send` et `$.prompt.fill` prennent également le résultat de l'événement, comme le montre le tableau. [Rechercher ce qu'un stub retourne](#look-up-what-a-stub-returns) montre quelle forme chaque nom courant prend. Deux erreurs signifient qu'un stub est incorrect ou manquant. La sortie d'un test échoué inclut un bloc intitulé `the engine reported:`, et chaque erreur y apparaît :

* `returned neither { value } nor { deny }` : un stub pour un appel d'API des mods a retourné une valeur nue
* `no implementation for` suivi d'un nom : votre mod a fait cet appel et aucun stub ne le répond

Le kit exporte également des mocks en mémoire qui répondent à un espace de noms entier pour vous. `mock.clock(on)` répond à [`$.clock`](/docs/fr/plugins/mods/api#run-work-in-the-background), `mock.store(on, { count: 7 })` répond à `$.store` à partir d'un magasin qui commence par ces entrées, et `mock.env(on, { CI: 'true' })` répond à `$.env.get` à partir de ces variables. `mock.clock` retourne une horloge simulée que votre test avance, afin qu'un test d'une minuterie n'attende pas. `mock.store` ne retourne rien, donc pour vérifier ce que votre mod a enregistré, écrivez vous-même les deux stubs `store` comme le fait le [test de dessin](#test-a-drawing).

<h3 id="follow-the-test-kit’s-rules">
  Suivre les règles du kit de test
</h3>

Le kit de test a quelques règles qui lui sont propres, et en enfreindre une produit les erreurs que les nouveaux auteurs de tests rencontrent en premier :

* **Enregistrez chaque stub avant le premier appel du test sur `$`.** Appeler `on` après cela lève une erreur comme `on("ui.render") after the test first called $`.

* **[`session.start`](/docs/fr/plugins/mods/reference#session) ne s'exécute pas par lui-même.** Chaque test commence avec votre module fraîchement chargé et aucun de ses hooks appelés, donc les variables au niveau du module conservent leurs valeurs initiales. Si un hook dépend de ce que `session.start` configure, levez-le d'abord :

  ```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' })
  ```

  Le deuxième stub répond à l'appel `$.command.register` qu'un hook `session.start` comme celui du [tutoriel](/docs/fr/plugins/mods/create#write-a-mod-yourself) fait. Sans lui, cet appel rejette avec `no implementation for command.register` et le kit saute votre hook, donc rien après l'appel dans le hook ne s'exécute. Le test n'échoue pas à ce stade. Le hook ignoré est listé sous `the engine reported:` uniquement si une vérification ultérieure échoue.

* **Un hook qui retourne `next(e)` a besoin d'un stub pour répondre.** Quand votre hook [`ui.render`](/docs/fr/plugins/mods/reference#interface) retourne `next(e)`, par exemple pour ne rien dessiner pendant que Claude est inactif, [le monter](#test-a-drawing) échoue avec `no implementation for ui.render`. Enregistrez un stub qui retourne un élément en tant que données simples :

  ```typescript theme={null}
  // Stands for what Claude Code would draw at the site
  on('ui.render', () => ({ type: 'Text', props: {}, children: ['drawn by Claude Code'] }))
  ```

  Avec le stub enregistré, le montage réussit, et `ui.find({ type: 'Text' })` retourne cet élément chaque fois que votre hook a retourné `next(e)`.

* **Un stub pour `turn.step` est un générateur asynchrone**, et le test lit le flux jusqu'à la fin pour obtenir le résultat :

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

  Quand la boucle se termine, `result` est l'objet que le stub a retourné, après que votre hook `turn.step` ait eu la chance de le modifier. Ici `result.answer` est `'ok'`.

* **Levez un appel d'outil avec le nom et les arguments de l'outil en tant que champs**, comme `await $.tool.call({ tool: 'Bash', command: 'ls' })`, et enregistrez un stub `tool.call` qui retourne `{ result }`.

<h3 id="look-up-what-a-stub-returns">
  Rechercher ce qu'un stub retourne
</h3>

Chaque appel d'API des mods que votre mod fait dans un test a besoin d'un stub qui répond à la place de Claude Code, sauf les quelques-uns que le kit répond lui-même : les appels [`$.ui.invalidate`](/docs/fr/plugins/mods/interface#redraw-when-something-changes) et [`$.state`](/docs/fr/plugins/mods/interface#keep-state). Pour les appels `$.clock`, utilisez `mock.clock(on)`, sinon votre mod `$.clock.now()` échoue avec `no implementation for clock.now`.

Ce tableau liste ceux que les mods utilisent le plus. La première colonne est l'appel que votre mod fait ou l'événement qu'il transmet avec `next(e)`. La deuxième est la fonction à passer à `on` sous ce nom, afin que la ligne `$.store.get` devienne `on('store.get', ($, e) => ({ value: saved.get(e.key) }))`. Un `'...'` dans un stub marque le texte pour vous à remplir :

| Votre mod appelle ou transmet | Stub |
| :- | :- |
| `$.command.register`, `$.tool.register`, `$.ui.toast`, `$.ui.log`, `$.ui.status`, `$.ui.close`, `$.store.set` | `() => ({ value: undefined })`. Pour `ui.toast` et `ui.log`, le texte est `e.text`. |
| `$.store.get` | `($, e) => ({ value: saved.get(e.key) })` |
| `$.fs.read` | `($, e) => ({ value: e.path.endsWith('notes.md') ? '# Notes' : '' })`. `e.path` arrive en tant que chemin absolu, donc comparez avec `endsWith`. |
| `$.ui.open` | `() => ({ value: { isPlaced: true } })` |
| `$.ui.ask` | Un stub `tool.call`, parce que la question l'atteint comme un appel à l'outil `AskUserQuestion` : `($, e) => ({ result: { answers: { [e.questions[0].question]: 'Run it' } } })`. Vérifiez `e.tool` d'abord si votre mod transmet d'autres appels d'outils. |
| `$.model.complete` | `() => ({ value: { isAnswered: true, text: '...', usage } })` |
| `$.process.run` | `($, e) => ({ value: { exitCode: 0, stdout: '...', stderr: '' } })`. `e.argv` est la liste des arguments et `e.init` contient `cwd` et `timeoutMs`. |
| Tout appel d'API des mods qui devrait échouer | `() => ({ deny: 'the reason' })`, ce qui fait que l'appel rejette dans votre mod. Un stub qui lance une exception est ignoré à la place. |
| `session.start` | `() => ({ cwd: '/work' })` |
| `turn.start` | `($, e) => ({ turnId: e.turnId })` |
| `tool.call` | `() => ({ result: '...' })` |
| `turn.complete` | `() => ({ text: '' })`. Levez-le avec `$.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` arrive en tant que chaîne même quand votre mod a passé `{ sessionId }`. |
| `session.receive` | `($, e) => ({ text: e.text })`. Levez-le avec `$.session.receive({ origin: { kind: 'peer-send-message' }, text })`. |
| `ui.render` | `() => ({ type: 'Text', props: {}, children: ['...'] })` |

`expect` a les assertions `toBe`, `toEqual`, `toMatch`, `toMatchObject`, `toContain`, `toBeDefined`, `toBeUndefined`, et `toThrow`, et `.not` avant n'importe lequel d'entre eux.

<h2 id="test-a-timer">
  Tester une minuterie
</h2>

Un mod qui exécute du travail sur une minuterie a besoin d'une horloge que le test contrôle, afin que le test puisse avancer le temps au lieu d'attendre. `const clock = mock.clock(on)` retourne une horloge simulée qui commence à `0` et ne se déplace que quand votre test la déplace. Pour commencer à un autre moment, passez-le en millisecondes, comme dans `mock.clock(on, { now: 5000 })`. L'horloge a ces méthodes :

| Méthode | Ce qu'elle fait |
| :- | :- |
| `await clock.advance(1000)` | Avance le temps de ce nombre de millisecondes et exécute chaque minuterie qui arrive à échéance |
| `await clock.set(5000)` | Avance le temps à cette valeur, comme `advance` le ferait |
| `clock.now()` | Retourne l'heure, ce que votre mod `$.clock.now()` se résout à |
| `await clock.settle()` | Exécute les minuteries qui sont déjà dues, comme une chaîne d'appels `$.clock.after` à délai zéro, sans déplacer le temps |
| `await clock.sleep(2000)` | À l'intérieur d'un stub, fait que ce stub ne répond que quand le test a avancé jusque-là, ce qui est comment vous simulez un modèle ou un processus lent |

Ce hook appartient à un mod nommé `countdown`, et gère une commande `/countdown` qui prend un nombre de secondes, démarre une minuterie `$.clock.every` d'une seconde, et affiche un toast à zéro. Comme avec `grader`, le fichier ne contient que le hook testé et n'enregistre pas la commande :

```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 {}
  })
}
```

Ce test exécute `/countdown 3` et déplace l'horloge simulée, afin qu'il vérifie trois secondes de comportement sans attendre trois secondes :

```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'])
})
```

Le premier `expect` montre que le toast ne vient pas tôt, et le deuxième montre qu'il vient une fois. Chaque `advance` se résout après que les minuteries qui sont venues à échéance aient exécuté, afin que la vérification sur la ligne suivante voie leur effet.

<h2 id="test-a-drawing">
  Tester un dessin
</h2>

Un test peut dessiner l'un de vos sites de [rendu](/docs/fr/plugins/mods/reference#render-sites) du mod, puis appuyer, taper et trouver les éléments qu'il a dessinés. `$.ui.mount` dessine le site via le hook `ui.render` de votre mod et retourne un handle avec une méthode pour chacun d'eux. Pour couvrir plusieurs applications dans un test, définissez `surface` sur l'application à dessiner. Ce test ouvre le volet de [Construire un volet avec des onglets](/docs/fr/plugins/mods/interface#build-a-pane-with-tabs), bascule les onglets, appuie sur le bouton, et vérifie le compte dans le terminal et l'application 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)
})
```

Dans votre shell, exécutez `claude plugin test` depuis le répertoire `hello-tabs`. Le test réussit quand les deux applications dessinent la ligne de compte et le mod a enregistré `2`. Le compte se reporte de la première application à la deuxième parce que les deux montages utilisent le même module chargé.

Le handle que `$.ui.mount` retourne a ces méthodes, qui adressent les éléments par la `key` que vous leur avez donnée :

| Méthode | Ce qu'elle fait |
| :- | :- |
| `press({ key: 'more' })` | Appuie sur le `Button` avec cette clé |
| `input({ key: 'new-note', text: 'buy milk' })` | Tape le texte dans l'`Input` avec cette clé et appuie sur Entrée. Ajoutez `kind: 'change'` pour taper sans soumettre. |
| `select({ key: 'size', value: 'large' })` | Choisit l'option avec cette valeur dans le `Select` avec cette clé |
| `find({ key: 'more' })` ou `find({ type: 'Text', text: 'Count: 2' })` | Retourne le premier élément correspondant comme `{ type, props, children }`, ou `undefined`. `text` peut être une chaîne ou une expression régulière. |
| `unmount()` | Supprime le dessin |

Chaque méthode se résout après que votre gestionnaire ait terminé, afin que vous puissiez vérifier le résultat sur la ligne suivante. Définissez `props` à ce que Claude Code passerait pour ce site. Le [tableau des sites de rendu](/docs/fr/plugins/mods/reference#render-sites) liste les props de chaque site, et [les types pour votre build](/docs/fr/plugins/mods/create#get-the-types-for-your-build) ont leurs types.

Un test de dessin vérifie l'arborescence que votre hook retourne et si elle est valide pour cette application. Il ne vérifie pas comment l'application la peint, donc regardez une nouvelle mise en page dans une vraie session aussi.

<h3 id="test-a-drawing-after-clear">
  Tester un dessin après `/clear`
</h3>

Chaque test commence avec chaque valeur `$.state` à sa valeur par défaut, ce qui est comment `/clear` les laisse. Pour tester ce que votre mod fait ensuite, ignorez `session.start`, levez `classic.SessionStart` avec `source: 'clear'`, et vérifiez ce que votre mod dessine.

Ce test vérifie le module de [Charger une valeur enregistrée à nouveau après `/clear`](/docs/fr/plugins/mods/interface#load-a-saved-value-again-after-clear). Ajoutez-le au fichier de [Tester un dessin](#test-a-drawing), où `PANE` est défini. Le premier test de ce fichier s'attend à ce que le bouton enregistre le compte, comme le bouton dans [Enregistrer à partir de plus d'une session](/docs/fr/plugins/mods/interface#save-from-more-than-one-session) le fait :

```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()
})
```

Le test réussit quand votre hook `classic.SessionStart` a copié le `7` enregistré dans `$.state` avant que le volet ne se dessine. Sans ce hook dans votre module, le volet dessine `Count: 0`, `find` retourne `undefined`, et le test échoue à `toBeDefined`.

<h2 id="test-a-mod-that-judges-other-mods">
  Tester un mod qui juge d'autres mods
</h2>

Un mod que votre organisation liste dans [`prependPlugins`](/docs/fr/plugins/mods/admin) peut refuser un autre mod avant qu'il ne se charge. Pour en tester un, définissez le tier de votre mod et donnez au test un deuxième mod pour que le vôtre admette ou refuse :

* **`tier`** : appelez-le une fois en haut du fichier de test, comme dans `tier('prepend')`, pour charger votre mod comme `prepend`, `append`, ou `builtin`, sa place dans l'[ordre dans lequel les mods s'exécutent](/docs/fr/plugins/mods/events#the-order-mods-run-in). Sans lui, votre mod se charge comme `user`.
* **`plugins`** : passez à `test` un objet d'options avant le corps du test. Son tableau `plugins` contient des mods que vous écrivez en ligne, chacun avec un `name` et une fonction `register`. Pour charger un ailleurs que `user`, ajoutez `tier` à celui-ci.

Ce fichier de test charge le [mod de politique de la page d'administration](/docs/fr/plugins/mods/admin#enforce-a-policy-with-a-mod-of-your-own) d'abord. Il vérifie que le mod de politique refuse un mod qui démarre un processus et en admet un qui ne le fait pas :

```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' })
})
```

Dans votre shell, exécutez `claude plugin test` depuis le répertoire `acme-guard`. Les deux tests réussissent avec le mod de politique comme la page d'administration le montre.

Le kit charge chaque mod au premier appel du test sur `$`. Quand votre mod en refuse un, cet appel lance une exception, et le message nomme le mod refusé, le mod qui l'a refusé, et votre raison. Dans le deuxième test rien n'est refusé, donc `reader` répond à l'appel d'outil avant qu'il n'atteigne le stub.

<h2 id="next-steps">
  Étapes suivantes
</h2>

* [Dépanner un mod](/docs/fr/plugins/mods/troubleshoot) : découvrez pourquoi un mod ne fait rien dans une session
* [Référence des mods](/docs/fr/plugins/mods/reference) : chaque événement d'entrée et résultat, pour écrire des stubs
