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

# 모드 테스트

> 이벤트를 발생시키고, Claude Code의 답변을 스텁하고, 버튼을 누르는 Claude Code 모드에 대한 자동화된 테스트를 작성합니다. 세션, 로그인, 네트워크가 필요하지 않습니다.

모드에 대한 자동화된 테스트를 작성하고 [`claude plugin test`](/docs/ko/plugins/mods/reference#commands)를 사용하여 셸에서 실행할 수 있습니다. 테스트는 훅이 처리하는 이벤트를 발생시키고 훅이 수행한 작업을 확인하므로 세션에 도달하기 전에 문제를 발견할 수 있습니다. 첫 번째 예제는 [모드 만들기](/docs/ko/plugins/mods/create)의 모드를 테스트합니다.

<h2 id="write-a-test">
  테스트 작성
</h2>

테스트는 모드를 로드하고, Claude Code가 하는 방식으로 훅을 통해 이벤트를 보내고, 세션, 로그인, 네트워크 없이 훅이 수행한 작업을 확인합니다. 셸에서 `claude plugin test`를 사용하여 테스트를 실행하며, 각 테스트 파일은 `claude-code/testing` 모듈의 테스트 라이브러리인 테스트 키트를 가져옵니다.

각 테스트 파일에 `first-mod.test.ts`와 같이 `.test.ts`로 끝나는 이름을 지정하고 플러그인 디렉토리의 어디든지 저장합니다. 모든 테스트 파일에는 최소한 하나의 `test()`가 필요하며, 그렇지 않으면 `declares no test(): nothing ran`으로 실행이 실패합니다. 테스트 파일은 모드의 자체 파일과 형제 `.ts` 헬퍼를 가져올 수 있으므로 게임의 규칙과 같은 일반 함수를 키트 없이 단위 테스트할 수 있습니다.

이 테스트는 두 개의 도구 호출을 발생시키고, [모드 만들기](/docs/ko/plugins/mods/create)의 `/tally` 명령을 실행하고, 회신이 둘 다 계산하는지 확인합니다. 첫 번째 줄은 [스텁](#stub-what-claude-code-would-answer)이며, Claude Code 대신 도구 호출에 답변합니다. `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')
})
```

셸에서 `first-mod` 디렉토리에서 테스트를 실행합니다:

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

출력은 각 테스트와 통과 여부를 이름으로 지정하며, 실행마다 다양한 타이밍을 표시합니다:

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

각 `$.tool.call`은 모드의 [`tool.call`](/docs/ko/plugins/mods/reference#tools) 훅을 통과했으며, 이는 개수에 1을 더하고 호출을 스텁으로 전달했습니다. `ls`는 실행되지 않았고 파일도 읽지 않았습니다. `$.command.run`은 모드의 [`command.run`](/docs/ko/plugins/mods/reference#commands-and-configuration) 훅으로 이동했으며, `answer`는 해당 훅이 반환한 객체입니다.

테스트가 실패하면 명령이 상태 1로 종료되므로 CI에서 작동합니다. 자신의 모드를 실행하는 셸에서 로드할 수 없으면 `claude plugin test: hooks modules are turned off`로 시작하는 줄을 이유와 함께 출력하고 상태 1로 종료합니다.

<h3 id="stub-what-claude-code-would-answer">
  Claude Code가 답변할 내용 스텁하기
</h3>

테스트에서는 모델, 저장소, 도구가 실행되지 않으므로 모드가 Claude Code의 답변을 기대하는 곳마다 테스트는 스텁으로 답변을 제공합니다. 테스트 함수는 다음 두 가지 인수를 받습니다:

* **`$`**: 테스트의 자체 `$`이며, Claude Code가 있는 곳에 서 있습니다. 훅이 받는 [mods API](/docs/ko/plugins/mods/reference#mods-api-methods)가 아닙니다. 각 메서드는 같은 이름의 이벤트를 발생시키고, 모드의 훅을 통해 보내고, 결과로 해결됩니다: `$.tool.call({ tool: 'Bash', command: 'ls' })`는 `tool.call`을 발생시킵니다. `$.command.run`, `$.prompt.submit`, `$.session.start`, `$.turn.complete`는 같은 방식으로 작동하며, `$.classic.Stop` 및 기타 `$.classic` 메서드는 [설정 훅 이벤트](/docs/ko/plugins/mods/events#hook-the-settings-hook-events)를 발생시킵니다. 테스트는 `ui.close`와 같은 mods API 호출을 직접 발생시킬 수 없습니다. 예를 들어 창을 닫는 버튼을 눌러 모드를 통해 트리거합니다.
* **`on`**: 스텁을 등록하기 위해 호출합니다. 스텁은 Claude Code 대신 답변하는 훅입니다. `$.` 없이 mods API 호출의 이름을 지정하므로 `store.get`으로 등록된 스텁은 모드의 `$.store.get`에 답변합니다. 모드가 [`$.model.complete`](/docs/ko/plugins/mods/api#call-a-model) 또는 [`$.store.get`](/docs/ko/plugins/mods/interface#keep-state)을 호출할 때 스텁이 답변을 제공합니다.

이 예제는 모델 호출을 스텁합니다. 훅은 `grader`라는 모드에 속하며 문장을 모델로 보내고 회신이 `PASS`로 시작하는지 보고하는 `/grade` 명령을 처리합니다. 파일은 테스트 중인 훅만 포함하므로 모드에는 [모드 만들기](/docs/ko/plugins/mods/create#write-a-mod-yourself)와 같이 `plugin.json` 및 `hooks.json`도 필요합니다. 세션에서 `/grade`를 입력하려면 모드도 [명령을 등록](/docs/ko/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' }
  })
}
```

이 테스트는 모델 호출을 스텁하여 훅이 통과 회신으로 수행하는 작업을 확인합니다:

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

테스트는 훅의 `reply`가 `value` 아래의 객체이고 `text`가 `PASS`로 시작하기 때문에 통과합니다. 다른 분기를 확인하려면 스텁이 `FAIL`로 시작하는 `text`를 반환하는 두 번째 테스트를 추가하고 `Try again`을 기대합니다.

mods API 호출에 대한 스텁은 `value` 필드가 있는 객체를 반환하며, 이는 모드에서 호출이 해결되는 것을 보유합니다: `{ value: 7 }`은 `$.store.get`이 `7`로 해결되도록 합니다. [`turn.step`](/docs/ko/plugins/mods/reference#turns) 또는 `tool.call`과 같은 Claude Code의 이벤트에 대한 스텁은 해당 이벤트의 자체 결과(예: `{ result: 'ok' }`)를 반환합니다. `$.session.send` 및 `$.prompt.fill`도 테이블이 표시하는 대로 이벤트의 결과를 사용합니다. [스텁이 반환하는 것 조회](#look-up-what-a-stub-returns)는 각 일반 이름이 취하는 형식을 보여줍니다. 두 가지 오류는 스텁이 잘못되었거나 누락되었음을 의미합니다. 실패한 테스트의 출력에는 `the engine reported:`로 시작하는 블록이 포함되며, 각 오류가 표시됩니다:

* `returned neither { value } nor { deny }`: mods API 호출에 대한 스텁이 일반 값을 반환했습니다
* `no implementation for` 다음에 이름: 모드가 해당 호출을 수행했고 스텁이 답변하지 않습니다

키트는 또한 전체 네임스페이스에 답변하는 메모리 내 모의 객체를 내보냅니다. `mock.clock(on)`은 [`$.clock`](/docs/ko/plugins/mods/api#run-work-in-the-background)에 답변하고, `mock.store(on, { count: 7 })`은 해당 항목으로 시작하는 저장소에서 `$.store`에 답변하고, `mock.env(on, { CI: 'true' })`는 해당 변수에서 `$.env.get`에 답변합니다. `mock.clock`은 테스트가 진행하는 모의 시계를 반환하므로 타이머 테스트는 대기하지 않습니다. `mock.store`은 아무것도 반환하지 않으므로 모드가 저장한 것을 확인하려면 [그리기 테스트](#test-a-drawing)처럼 두 개의 `store` 스텁을 직접 작성합니다.

<h3 id="follow-the-test-kit’s-rules">
  테스트 키트의 규칙 따르기
</h3>

테스트 키트에는 자체 규칙이 몇 가지 있으며, 하나를 위반하면 새로운 테스트 작성자가 처음 만나는 오류가 발생합니다:

* **`$`의 첫 번째 호출 전에 모든 스텁을 등록합니다.** 그 후에 `on`을 호출하면 `on("ui.render") after the test first called $`와 같은 오류가 발생합니다.

* **[`session.start`](/docs/ko/plugins/mods/reference#session)는 자체적으로 실행되지 않습니다.** 각 테스트는 모듈이 새로 로드되고 훅이 호출되지 않은 상태로 시작되므로 모듈 수준 변수는 초기 값을 유지합니다. 훅이 `session.start`가 설정하는 것에 의존하면 먼저 발생시킵니다:

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

  두 번째 스텁은 [튜토리얼](/docs/ko/plugins/mods/create#write-a-mod-yourself)과 같은 `session.start` 훅이 수행하는 `$.command.register` 호출에 답변합니다. 없으면 해당 호출이 `no implementation for command.register`로 거부되고 키트가 훅을 건너뛰므로 훅의 호출 후 아무것도 실행되지 않습니다. 테스트는 그 시점에서 실패하지 않습니다. 건너뛴 훅은 나중에 확인이 실패할 경우에만 `the engine reported:` 아래에 나열됩니다.

* **`next(e)`를 반환하는 훅에는 답변할 스텁이 필요합니다.** 예를 들어 [`ui.render`](/docs/ko/plugins/mods/reference#interface) 훅이 `next(e)`를 반환할 때, Claude가 유휴 상태일 때 아무것도 그리지 않으려면 [마운트](#test-a-drawing)가 `no implementation for ui.render`로 실패합니다. 요소를 일반 데이터로 반환하는 스텁을 등록합니다:

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

  스텁이 등록되면 마운트가 성공하고, `ui.find({ type: 'Text' })`는 훅이 `next(e)`를 반환할 때마다 해당 요소를 반환합니다.

* **`turn.step`에 대한 스텁은 비동기 생성기이며**, 테스트는 결과를 얻기 위해 스트림을 끝까지 읽습니다:

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

  루프가 끝나면 `result`는 `turn.step` 훅이 변경할 기회를 가진 후 스텁이 반환한 객체입니다. 여기서 `result.answer`는 `'ok'`입니다.

* **도구 호출을 도구의 이름과 인수를 필드로 발생시킵니다**, 예: `await $.tool.call({ tool: 'Bash', command: 'ls' })`, 그리고 `{ result }`를 반환하는 `tool.call` 스텁을 등록합니다.

<h3 id="look-up-what-a-stub-returns">
  스텁이 반환하는 것 조회
</h3>

모드가 테스트에서 수행하는 모든 mods API 호출에는 Claude Code 대신 답변할 스텁이 필요합니다. 단, 키트가 자체적으로 답변하는 몇 가지는 제외됩니다: [`$.ui.invalidate`](/docs/ko/plugins/mods/interface#redraw-when-something-changes) 및 [`$.state`](/docs/ko/plugins/mods/interface#keep-state) 호출. `$.clock` 호출의 경우 `mock.clock(on)`을 사용하거나 모드의 `$.clock.now()`가 `no implementation for clock.now`로 실패합니다.

이 표는 모드가 가장 많이 사용하는 것들을 나열합니다. 첫 번째 열은 모드가 수행하거나 `next(e)`로 전달하는 호출 또는 이벤트입니다. 두 번째는 해당 이름 아래 `on`에 전달할 함수이므로 `$.store.get` 행은 `on('store.get', ($, e) => ({ value: saved.get(e.key) }))`가 됩니다. 스텁의 `'...'`는 채울 텍스트를 표시합니다:

| 모드가 호출하거나 전달하는 것 | 스텁 |
| :- | :- |
| `$.command.register`, `$.tool.register`, `$.ui.toast`, `$.ui.log`, `$.ui.status`, `$.ui.close`, `$.store.set` | `() => ({ value: undefined })`. `ui.toast` 및 `ui.log`의 경우 텍스트는 `e.text`입니다. |
| `$.store.get` | `($, e) => ({ value: saved.get(e.key) })` |
| `$.fs.read` | `($, e) => ({ value: e.path.endsWith('notes.md') ? '# Notes' : '' })`. `e.path`는 절대 경로로 도착하므로 `endsWith`와 비교합니다. |
| `$.ui.open` | `() => ({ value: { isPlaced: true } })` |
| `$.ui.ask` | `tool.call` 스텁입니다. 질문이 `AskUserQuestion` 도구에 대한 호출로 도착하기 때문입니다: `($, e) => ({ result: { answers: { [e.questions[0].question]: 'Run it' } } })`. 모드가 다른 도구 호출을 전달하면 먼저 `e.tool`을 확인합니다. |
| `$.model.complete` | `() => ({ value: { isAnswered: true, text: '...', usage } })` |
| `$.process.run` | `($, e) => ({ value: { exitCode: 0, stdout: '...', stderr: '' } })`. `e.argv`는 인수 목록이고 `e.init`은 `cwd` 및 `timeoutMs`를 보유합니다. |
| 실패해야 하는 모든 mods API 호출 | `() => ({ deny: 'the reason' })`. 이는 모드에서 호출이 거부되도록 합니다. 예외를 발생시키는 스텁은 대신 건너뜁니다. |
| `session.start` | `() => ({ cwd: '/work' })` |
| `turn.start` | `($, e) => ({ turnId: e.turnId })` |
| `tool.call` | `() => ({ result: '...' })` |
| `turn.complete` | `() => ({ text: '' })`. `$.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`는 모드가 `{ sessionId }`를 전달했을 때도 문자열로 도착합니다. |
| `session.receive` | `($, e) => ({ text: e.text })`. `$.session.receive({ origin: { kind: 'peer-send-message' }, text })`로 발생시킵니다. |
| `ui.render` | `() => ({ type: 'Text', props: {}, children: ['...'] })` |

`expect`는 `toBe`, `toEqual`, `toMatch`, `toMatchObject`, `toContain`, `toBeDefined`, `toBeUndefined`, `toThrow` 어설션을 가지며, 이들 중 어느 것 앞에도 `.not`을 사용할 수 있습니다.

<h2 id="test-a-timer">
  타이머 테스트
</h2>

타이머에서 작업을 실행하는 모드는 테스트가 대기하는 대신 시간을 앞으로 이동할 수 있도록 테스트가 제어하는 시계가 필요합니다. `const clock = mock.clock(on)`은 `0`에서 시작하고 테스트가 이동할 때만 이동하는 모의 시계를 반환합니다. 다른 시간에 시작하려면 `mock.clock(on, { now: 5000 })`과 같이 밀리초 단위로 전달합니다. 시계에는 다음 메서드가 있습니다:

| 메서드 | 수행하는 작업 |
| :- | :- |
| `await clock.advance(1000)` | 시간을 해당 밀리초만큼 앞으로 이동하고 기한이 된 각 타이머를 실행합니다 |
| `await clock.set(5000)` | 시간을 해당 값으로 앞으로 이동합니다. `advance`처럼 |
| `clock.now()` | 시간을 반환합니다. 모드의 `$.clock.now()`가 해결되는 것입니다 |
| `await clock.settle()` | 이미 기한이 된 타이머(예: 0 지연 `$.clock.after` 호출 체인)를 실행합니다. 시간을 이동하지 않습니다 |
| `await clock.sleep(2000)` | 스텁 내부에서 테스트가 그 거리만큼 진행할 때까지만 해당 스텁이 답변하도록 합니다. 느린 모델이나 프로세스를 시뮬레이션하는 방법입니다 |

이 훅은 `countdown`이라는 모드에 속하며 초 단위로 숫자를 사용하는 `/countdown` 명령을 처리하고, 1초 `$.clock.every` 타이머를 시작하고, 0에서 토스트를 표시합니다. `grader`와 마찬가지로 파일은 테스트 중인 훅만 포함하고 명령을 등록하지 않습니다:

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

이 테스트는 `/countdown 3`을 실행하고 모의 시계를 이동하므로 3초를 기다리지 않고 3초의 동작을 확인합니다:

```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`는 토스트가 일찍 오지 않음을 보여주고, 두 번째는 한 번 옴을 보여줍니다. 각 `advance`는 기한이 된 타이머가 실행된 후 해결되므로 다음 줄의 확인은 그 효과를 봅니다.

<h2 id="test-a-drawing">
  그리기 테스트
</h2>

테스트는 모드의 [렌더 사이트](/docs/ko/plugins/mods/reference#render-sites) 중 하나를 그리고, 그린 요소를 누르고, 입력하고, 찾을 수 있습니다. `$.ui.mount`는 모드의 `ui.render` 훅을 통해 사이트를 그리고 각각에 대한 메서드가 있는 핸들을 반환합니다. 한 테스트에서 여러 앱을 다루려면 `surface`를 그릴 앱으로 설정합니다. 이 테스트는 [탭으로 창 만들기](/docs/ko/plugins/mods/interface#build-a-pane-with-tabs)의 창을 열고, 탭을 전환하고, 버튼을 누르고, 터미널과 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)
})
```

셸에서 `hello-tabs` 디렉토리에서 `claude plugin test`를 실행합니다. 테스트는 두 앱이 모두 개수 줄을 그리고 모드가 `2`를 저장했을 때 통과합니다. 두 마운트가 모두 같은 로드된 모듈을 사용하기 때문에 개수는 첫 번째 앱에서 두 번째 앱으로 이월됩니다.

`$.ui.mount`가 반환하는 핸들에는 제공한 `key`로 요소를 주소 지정하는 다음 메서드가 있습니다:

| 메서드 | 수행하는 작업 |
| :- | :- |
| `press({ key: 'more' })` | 해당 키가 있는 `Button`을 누릅니다 |
| `input({ key: 'new-note', text: 'buy milk' })` | 텍스트를 해당 키가 있는 `Input`에 입력하고 Enter를 누릅니다. `kind: 'change'`를 추가하여 제출하지 않고 입력합니다. |
| `select({ key: 'size', value: 'large' })` | 해당 키가 있는 `Select`에서 해당 값이 있는 옵션을 선택합니다 |
| `find({ key: 'more' })` 또는 `find({ type: 'Text', text: 'Count: 2' })` | 첫 번째 일치하는 요소를 `{ type, props, children }`으로 반환하거나 `undefined`를 반환합니다. `text`는 문자열 또는 정규식일 수 있습니다. |
| `unmount()` | 그리기를 제거합니다 |

각 메서드는 핸들러가 완료된 후 해결되므로 다음 줄에서 결과를 확인할 수 있습니다. `props`를 Claude Code가 해당 사이트에 전달할 것으로 설정합니다. [렌더 사이트 표](/docs/ko/plugins/mods/reference#render-sites)는 각 사이트의 props를 나열하고, [빌드의 타입](/docs/ko/plugins/mods/create#get-the-types-for-your-build)은 해당 타입을 가집니다.

그리기 테스트는 훅이 반환하는 트리와 해당 앱에 유효한지 확인합니다. 앱이 이를 그리는 방식을 확인하지 않으므로 실제 세션에서 새 레이아웃을 살펴봅니다.

<h3 id="test-a-drawing-after-clear">
  `/clear` 후 그리기 테스트
</h3>

각 테스트는 모든 `$.state` 값이 기본값으로 시작하며, 이는 `/clear`가 남기는 방식입니다. 모드가 다음에 수행하는 작업을 테스트하려면 `session.start`를 건너뛰고, `source: 'clear'`로 `classic.SessionStart`를 발생시키고, 모드가 그리는 것을 확인합니다.

이 테스트는 ['/clear' 후 저장된 값 다시 로드](/docs/ko/plugins/mods/interface#load-a-saved-value-again-after-clear)의 모듈을 확인합니다. [그리기 테스트](#test-a-drawing)의 파일에 추가합니다. 여기서 `PANE`이 정의됩니다. 해당 파일의 첫 번째 테스트는 [하나 이상의 세션에서 저장](/docs/ko/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()
})
```

테스트는 `classic.SessionStart` 훅이 저장된 `7`을 창이 그리기 전에 `$.state`에 복사했을 때 통과합니다. 모듈에 해당 훅이 없으면 창이 `Count: 0`을 그리고, `find`가 `undefined`를 반환하고, 테스트가 `toBeDefined`에서 실패합니다.

<h2 id="test-a-mod-that-judges-other-mods">
  다른 모드를 판단하는 모드 테스트
</h2>

조직이 [`prependPlugins`](/docs/ko/plugins/mods/admin)에 나열하는 모드는 다른 모드가 로드되기 전에 거부할 수 있습니다. 하나를 테스트하려면 모드의 계층을 설정하고 테스트에 모드가 허용하거나 거부할 두 번째 모드를 제공합니다:

* **`tier`**: 테스트 파일의 맨 위에서 한 번 호출합니다. 예: `tier('prepend')`로 모드를 `prepend`, `append`, `builtin`으로 로드합니다. 이는 [모드가 실행되는 순서](/docs/ko/plugins/mods/events#the-order-mods-run-in)에서의 위치입니다. 없으면 모드가 `user`로 로드됩니다.
* **`plugins`**: 테스트 본문 앞에 `test`에 옵션 객체를 전달합니다. 해당 `plugins` 배열은 인라인으로 작성한 모드를 보유하며, 각각 `name` 및 `register` 함수를 가집니다. `user` 이외의 곳에 하나를 로드하려면 `tier`를 추가합니다.

이 테스트 파일은 [관리 페이지의 정책 모드](/docs/ko/plugins/mods/admin#enforce-a-policy-with-a-mod-of-your-own)를 먼저 로드합니다. 정책 모드가 프로세스를 시작하는 모드를 거부하고 그렇지 않은 모드를 허용하는지 확인합니다:

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

셸에서 `acme-guard` 디렉토리에서 `claude plugin test`를 실행합니다. 두 테스트 모두 정책 모드가 관리 페이지에 표시된 대로 통과합니다.

키트는 테스트의 첫 번째 `$` 호출에서 모든 모드를 로드합니다. 모드가 하나를 거부하면 해당 호출이 예외를 발생시키고, 메시지는 거부된 모드, 거부한 모드, 이유를 이름으로 지정합니다. 두 번째 테스트에서는 아무것도 거부되지 않으므로 `reader`가 스텁에 도달하기 전에 도구 호출에 답변합니다.

<h2 id="next-steps">
  다음 단계
</h2>

* [모드 문제 해결](/docs/ko/plugins/mods/troubleshoot): 모드가 세션에서 아무것도 하지 않는 이유 알아보기
* [모드 참조](/docs/ko/plugins/mods/reference): 스텁 작성을 위한 모든 이벤트의 입력 및 결과
