claude plugin test를 사용하여 셸에서 실행할 수 있습니다. 테스트는 훅이 처리하는 이벤트를 발생시키고 훅이 수행한 작업을 확인하므로 세션에 도달하기 전에 문제를 발견할 수 있습니다. 첫 번째 예제는 모드 만들기의 모드를 테스트합니다.
테스트 작성
테스트는 모드를 로드하고, Claude Code가 하는 방식으로 훅을 통해 이벤트를 보내고, 세션, 로그인, 네트워크 없이 훅이 수행한 작업을 확인합니다. 셸에서claude plugin test를 사용하여 테스트를 실행하며, 각 테스트 파일은 claude-code/testing 모듈의 테스트 라이브러리인 테스트 키트를 가져옵니다.
각 테스트 파일에 first-mod.test.ts와 같이 .test.ts로 끝나는 이름을 지정하고 플러그인 디렉토리의 어디든지 저장합니다. 모든 테스트 파일에는 최소한 하나의 test()가 필요하며, 그렇지 않으면 declares no test(): nothing ran으로 실행이 실패합니다. 테스트 파일은 모드의 자체 파일과 형제 .ts 헬퍼를 가져올 수 있으므로 게임의 규칙과 같은 일반 함수를 키트 없이 단위 테스트할 수 있습니다.
이 테스트는 두 개의 도구 호출을 발생시키고, 모드 만들기의 /tally 명령을 실행하고, 회신이 둘 다 계산하는지 확인합니다. 첫 번째 줄은 스텁이며, Claude Code 대신 도구 호출에 답변합니다. first-mod/tests/first-mod.test.ts로 저장합니다:
first-mod/tests/first-mod.test.ts
first-mod 디렉토리에서 테스트를 실행합니다:
$.tool.call은 모드의 tool.call 훅을 통과했으며, 이는 개수에 1을 더하고 호출을 스텁으로 전달했습니다. ls는 실행되지 않았고 파일도 읽지 않았습니다. $.command.run은 모드의 command.run 훅으로 이동했으며, answer는 해당 훅이 반환한 객체입니다.
테스트가 실패하면 명령이 상태 1로 종료되므로 CI에서 작동합니다. 자신의 모드를 실행하는 셸에서 로드할 수 없으면 claude plugin test: hooks modules are turned off로 시작하는 줄을 이유와 함께 출력하고 상태 1로 종료합니다.
Claude Code가 답변할 내용 스텁하기
테스트에서는 모델, 저장소, 도구가 실행되지 않으므로 모드가 Claude Code의 답변을 기대하는 곳마다 테스트는 스텁으로 답변을 제공합니다. 테스트 함수는 다음 두 가지 인수를 받습니다:$: 테스트의 자체$이며, Claude Code가 있는 곳에 서 있습니다. 훅이 받는 mods API가 아닙니다. 각 메서드는 같은 이름의 이벤트를 발생시키고, 모드의 훅을 통해 보내고, 결과로 해결됩니다:$.tool.call({ tool: 'Bash', command: 'ls' })는tool.call을 발생시킵니다.$.command.run,$.prompt.submit,$.session.start,$.turn.complete는 같은 방식으로 작동하며,$.classic.Stop및 기타$.classic메서드는 설정 훅 이벤트를 발생시킵니다. 테스트는ui.close와 같은 mods API 호출을 직접 발생시킬 수 없습니다. 예를 들어 창을 닫는 버튼을 눌러 모드를 통해 트리거합니다.on: 스텁을 등록하기 위해 호출합니다. 스텁은 Claude Code 대신 답변하는 훅입니다.$.없이 mods API 호출의 이름을 지정하므로store.get으로 등록된 스텁은 모드의$.store.get에 답변합니다. 모드가$.model.complete또는$.store.get을 호출할 때 스텁이 답변을 제공합니다.
grader라는 모드에 속하며 문장을 모델로 보내고 회신이 PASS로 시작하는지 보고하는 /grade 명령을 처리합니다. 파일은 테스트 중인 훅만 포함하므로 모드에는 모드 만들기와 같이 plugin.json 및 hooks.json도 필요합니다. 세션에서 /grade를 입력하려면 모드도 명령을 등록해야 합니다:
grader/hooks/register.js
grader/tests/grader.test.ts
reply가 value 아래의 객체이고 text가 PASS로 시작하기 때문에 통과합니다. 다른 분기를 확인하려면 스텁이 FAIL로 시작하는 text를 반환하는 두 번째 테스트를 추가하고 Try again을 기대합니다.
mods API 호출에 대한 스텁은 value 필드가 있는 객체를 반환하며, 이는 모드에서 호출이 해결되는 것을 보유합니다: { value: 7 }은 $.store.get이 7로 해결되도록 합니다. turn.step 또는 tool.call과 같은 Claude Code의 이벤트에 대한 스텁은 해당 이벤트의 자체 결과(예: { result: 'ok' })를 반환합니다. $.session.send 및 $.prompt.fill도 테이블이 표시하는 대로 이벤트의 결과를 사용합니다. 스텁이 반환하는 것 조회는 각 일반 이름이 취하는 형식을 보여줍니다. 두 가지 오류는 스텁이 잘못되었거나 누락되었음을 의미합니다. 실패한 테스트의 출력에는 the engine reported:로 시작하는 블록이 포함되며, 각 오류가 표시됩니다:
returned neither { value } nor { deny }: mods API 호출에 대한 스텁이 일반 값을 반환했습니다no implementation for다음에 이름: 모드가 해당 호출을 수행했고 스텁이 답변하지 않습니다
mock.clock(on)은 $.clock에 답변하고, mock.store(on, { count: 7 })은 해당 항목으로 시작하는 저장소에서 $.store에 답변하고, mock.env(on, { CI: 'true' })는 해당 변수에서 $.env.get에 답변합니다. mock.clock은 테스트가 진행하는 모의 시계를 반환하므로 타이머 테스트는 대기하지 않습니다. mock.store은 아무것도 반환하지 않으므로 모드가 저장한 것을 확인하려면 그리기 테스트처럼 두 개의 store 스텁을 직접 작성합니다.
테스트 키트의 규칙 따르기
테스트 키트에는 자체 규칙이 몇 가지 있으며, 하나를 위반하면 새로운 테스트 작성자가 처음 만나는 오류가 발생합니다:-
$의 첫 번째 호출 전에 모든 스텁을 등록합니다. 그 후에on을 호출하면on("ui.render") after the test first called $와 같은 오류가 발생합니다. -
session.start는 자체적으로 실행되지 않습니다. 각 테스트는 모듈이 새로 로드되고 훅이 호출되지 않은 상태로 시작되므로 모듈 수준 변수는 초기 값을 유지합니다. 훅이session.start가 설정하는 것에 의존하면 먼저 발생시킵니다:두 번째 스텁은 튜토리얼과 같은session.start훅이 수행하는$.command.register호출에 답변합니다. 없으면 해당 호출이no implementation for command.register로 거부되고 키트가 훅을 건너뛰므로 훅의 호출 후 아무것도 실행되지 않습니다. 테스트는 그 시점에서 실패하지 않습니다. 건너뛴 훅은 나중에 확인이 실패할 경우에만the engine reported:아래에 나열됩니다. -
next(e)를 반환하는 훅에는 답변할 스텁이 필요합니다. 예를 들어ui.render훅이next(e)를 반환할 때, Claude가 유휴 상태일 때 아무것도 그리지 않으려면 마운트가no implementation for ui.render로 실패합니다. 요소를 일반 데이터로 반환하는 스텁을 등록합니다:스텁이 등록되면 마운트가 성공하고,ui.find({ type: 'Text' })는 훅이next(e)를 반환할 때마다 해당 요소를 반환합니다. -
turn.step에 대한 스텁은 비동기 생성기이며, 테스트는 결과를 얻기 위해 스트림을 끝까지 읽습니다:루프가 끝나면result는turn.step훅이 변경할 기회를 가진 후 스텁이 반환한 객체입니다. 여기서result.answer는'ok'입니다. -
도구 호출을 도구의 이름과 인수를 필드로 발생시킵니다, 예:
await $.tool.call({ tool: 'Bash', command: 'ls' }), 그리고{ result }를 반환하는tool.call스텁을 등록합니다.
스텁이 반환하는 것 조회
모드가 테스트에서 수행하는 모든 mods API 호출에는 Claude Code 대신 답변할 스텁이 필요합니다. 단, 키트가 자체적으로 답변하는 몇 가지는 제외됩니다:$.ui.invalidate 및 $.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) }))가 됩니다. 스텁의 '...'는 채울 텍스트를 표시합니다:
expect는 toBe, toEqual, toMatch, toMatchObject, toContain, toBeDefined, toBeUndefined, toThrow 어설션을 가지며, 이들 중 어느 것 앞에도 .not을 사용할 수 있습니다.
타이머 테스트
타이머에서 작업을 실행하는 모드는 테스트가 대기하는 대신 시간을 앞으로 이동할 수 있도록 테스트가 제어하는 시계가 필요합니다.const clock = mock.clock(on)은 0에서 시작하고 테스트가 이동할 때만 이동하는 모의 시계를 반환합니다. 다른 시간에 시작하려면 mock.clock(on, { now: 5000 })과 같이 밀리초 단위로 전달합니다. 시계에는 다음 메서드가 있습니다:
이 훅은
countdown이라는 모드에 속하며 초 단위로 숫자를 사용하는 /countdown 명령을 처리하고, 1초 $.clock.every 타이머를 시작하고, 0에서 토스트를 표시합니다. grader와 마찬가지로 파일은 테스트 중인 훅만 포함하고 명령을 등록하지 않습니다:
countdown/hooks/register.js
/countdown 3을 실행하고 모의 시계를 이동하므로 3초를 기다리지 않고 3초의 동작을 확인합니다:
countdown/tests/countdown.test.ts
expect는 토스트가 일찍 오지 않음을 보여주고, 두 번째는 한 번 옴을 보여줍니다. 각 advance는 기한이 된 타이머가 실행된 후 해결되므로 다음 줄의 확인은 그 효과를 봅니다.
그리기 테스트
테스트는 모드의 렌더 사이트 중 하나를 그리고, 그린 요소를 누르고, 입력하고, 찾을 수 있습니다.$.ui.mount는 모드의 ui.render 훅을 통해 사이트를 그리고 각각에 대한 메서드가 있는 핸들을 반환합니다. 한 테스트에서 여러 앱을 다루려면 surface를 그릴 앱으로 설정합니다. 이 테스트는 탭으로 창 만들기의 창을 열고, 탭을 전환하고, 버튼을 누르고, 터미널과 Desktop 앱의 개수를 확인합니다:
hello-tabs/tests/hello-tabs.test.ts
hello-tabs 디렉토리에서 claude plugin test를 실행합니다. 테스트는 두 앱이 모두 개수 줄을 그리고 모드가 2를 저장했을 때 통과합니다. 두 마운트가 모두 같은 로드된 모듈을 사용하기 때문에 개수는 첫 번째 앱에서 두 번째 앱으로 이월됩니다.
$.ui.mount가 반환하는 핸들에는 제공한 key로 요소를 주소 지정하는 다음 메서드가 있습니다:
각 메서드는 핸들러가 완료된 후 해결되므로 다음 줄에서 결과를 확인할 수 있습니다.
props를 Claude Code가 해당 사이트에 전달할 것으로 설정합니다. 렌더 사이트 표는 각 사이트의 props를 나열하고, 빌드의 타입은 해당 타입을 가집니다.
그리기 테스트는 훅이 반환하는 트리와 해당 앱에 유효한지 확인합니다. 앱이 이를 그리는 방식을 확인하지 않으므로 실제 세션에서 새 레이아웃을 살펴봅니다.
/clear 후 그리기 테스트
각 테스트는 모든 $.state 값이 기본값으로 시작하며, 이는 /clear가 남기는 방식입니다. 모드가 다음에 수행하는 작업을 테스트하려면 session.start를 건너뛰고, source: 'clear'로 classic.SessionStart를 발생시키고, 모드가 그리는 것을 확인합니다.
이 테스트는 ‘/clear’ 후 저장된 값 다시 로드의 모듈을 확인합니다. 그리기 테스트의 파일에 추가합니다. 여기서 PANE이 정의됩니다. 해당 파일의 첫 번째 테스트는 하나 이상의 세션에서 저장의 버튼처럼 버튼이 개수를 저장할 것으로 기대합니다:
hello-tabs/tests/hello-tabs.test.ts
classic.SessionStart 훅이 저장된 7을 창이 그리기 전에 $.state에 복사했을 때 통과합니다. 모듈에 해당 훅이 없으면 창이 Count: 0을 그리고, find가 undefined를 반환하고, 테스트가 toBeDefined에서 실패합니다.
다른 모드를 판단하는 모드 테스트
조직이prependPlugins에 나열하는 모드는 다른 모드가 로드되기 전에 거부할 수 있습니다. 하나를 테스트하려면 모드의 계층을 설정하고 테스트에 모드가 허용하거나 거부할 두 번째 모드를 제공합니다:
tier: 테스트 파일의 맨 위에서 한 번 호출합니다. 예:tier('prepend')로 모드를prepend,append,builtin으로 로드합니다. 이는 모드가 실행되는 순서에서의 위치입니다. 없으면 모드가user로 로드됩니다.plugins: 테스트 본문 앞에test에 옵션 객체를 전달합니다. 해당plugins배열은 인라인으로 작성한 모드를 보유하며, 각각name및register함수를 가집니다.user이외의 곳에 하나를 로드하려면tier를 추가합니다.
acme-guard/tests/guard.test.ts
acme-guard 디렉토리에서 claude plugin test를 실행합니다. 두 테스트 모두 정책 모드가 관리 페이지에 표시된 대로 통과합니다.
키트는 테스트의 첫 번째 $ 호출에서 모든 모드를 로드합니다. 모드가 하나를 거부하면 해당 호출이 예외를 발생시키고, 메시지는 거부된 모드, 거부한 모드, 이유를 이름으로 지정합니다. 두 번째 테스트에서는 아무것도 거부되지 않으므로 reader가 스텁에 도달하기 전에 도구 호출에 답변합니다.