תיעוד 81
בדיקת מוד
כתבו בדיקות אוטומטיות למוד של Claude Code שיורות אירועים, שמות stub במקום התשובות של Claude Code, ולוחצות על כפתורים, בלי סשן, בלי התחברות ובלי רשת.
אפשר לכתוב בדיקות אוטומטיות למוד ולהריץ אותן מהמעטפת עם claude plugin test. בדיקה יורה את האירועים שה-hooks שלכם מטפלים בהם ובודקת מה ה-hooks עשו, כדי שתתפסו בעיה לפני שהיא מגיעה לסשן. הדוגמה הראשונה בודקת את המוד מתוך יצירת מוד.
#כתיבת בדיקה
בדיקה טוענת את המוד שלכם, שולחת אירועים דרך ה-hooks שלו כמו ש-Claude Code היה עושה, ובודקת מה ה-hooks עשו, בלי סשן, בלי התחברות ובלי רשת. מריצים בדיקות מהמעטפת עם claude plugin test, וכל קובץ בדיקה מייבא את ערכת הבדיקות, ספריית בדיקות במודול claude-code/testing.
תנו לכל קובץ בדיקה שם שמסתיים ב-.test.ts, למשל first-mod.test.ts, ושמרו אותו בכל מקום בתיקיית התוסף. כל קובץ בדיקה צריך לפחות test() אחד, אחרת ההרצה נכשלת עם declares no test(): nothing ran. קובץ בדיקה יכול לייבא את הקבצים של המוד עצמו ועוזרי .ts אחים, כך שאפשר לבדוק ביחידה פונקציות רגילות, למשל כללים של משחק, בלי הערכה.
הבדיקה הזו יורה שתי קריאות כלי, מריצה את הפקודה /tally מתוך יצירת מוד, ובודקת שהתשובה סופרת את שתיהן. השורה הראשונה שלה היא stub, שעונה על קריאות הכלי במקום Claude Code. שמרו אותה כ-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')
})במעטפת, הריצו את הבדיקות מתוך התיקייה first-mod:
claude plugin testהפלט מציין כל בדיקה והאם היא עברה, עם זמנים שמשתנים מהרצה להרצה:
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 עבר דרך ה-hook tool.call של המוד, שהוסיף אחד לספירה והעביר את הקריאה הלאה ל-stub. שום ls לא רץ ושום קובץ לא נקרא. אחר כך $.command.run הגיע ל-hook command.run של המוד, ו-answer הוא האובייקט שה-hook הזה החזיר.
הפקודה יוצאת עם סטטוס 1 כשבדיקה נכשלת, ולכן היא עובדת ב-CI. אם המודים שלכם לא יכולים להיטען במעטפת שמריצה אותה, היא מדפיסה שורה שמתחילה ב-claude plugin test: hooks modules are turned off עם הסיבה, ויוצאת עם סטטוס 1.
#stub: מה ש-Claude Code היה עונה
אף מודל, מאגר או כלי לא רץ בבדיקה, ולכן בכל מקום שהמוד שלכם מצפה ש-Claude Code יענה, הבדיקה מספקת את התשובה עם stub. פונקציית בדיקה מקבלת שני ארגומנטים בשביל זה:
$: ה-$של הבדיקה עצמה, שפועל כמו Claude Code. זה לא ה-API של mods ש-hook מקבל. כל אחת מהמתודות שלו יורה את האירוע באותו שם, שולחת אותו דרך ה-hooks של המוד שלכם, ומתרחשת לתוצאה:$.tool.call({ tool: 'Bash', command: 'ls' })יורהtool.call.$.command.run,$.prompt.submit,$.session.startו-$.turn.completeעובדים באותו אופן, ו-$.classic.Stopושאר המתודות של$.classicיורים אירוע hook של הגדרות. בדיקה לא יכולה לירות קריאת API של mods כמוui.closeישירות. הפעילו אותה דרך המוד שלכם, למשל בלחיצה על הכפתור שסוגר את החלונית.on: קראו לו כדי לרשום stubs, שהם hooks שעונים במקום Claude Code. תנו ל-stub שם של קריאת API של mods בלי ה-$., כך ש-stub שנרשם כ-store.getעונה על$.store.getשל המוד שלכם. כשהמוד שלכם קורא ל-$.model.completeאו ל-$.store.get, stub מספק את התשובה.
הדוגמה הזו שמה stub על קריאת מודל. ה-hook שייך למוד בשם grader, ומטפל בפקודה /grade ששולחת משפט למודל ומדווחת אם התשובה מתחילה ב-PASS. הקובץ מחזיק רק את ה-hook שנבדק, ולכן המוד צריך גם plugin.json וגם hooks.json, כמו ב-יצירת מוד. כדי להקליד /grade בסשן, המוד צריך גם לרשום את הפקודה:
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' }
})
}הבדיקה הזו שמה stub על קריאת המודל כדי לבדוק מה ה-hook עושה עם תשובה שעוברת:
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 של ה-hook הוא האובייקט תחת value, שה-text שלו מתחיל ב-PASS. כדי לבדוק את הענף השני, הוסיפו בדיקה שנייה שה-stub שלה מחזיר text שמתחיל ב-FAIL, וצפו ל-Try again.
stub לקריאת API של mods מחזיר אובייקט עם שדה value, שמחזיק את מה שהקריאה מתרחשת אליו במוד שלכם: { value: 7 } גורם ל-$.store.get להתפרש ל-7. stub לאחד מהאירועים של Claude Code, למשל turn.step או tool.call, מחזיר את התוצאה של האירוע עצמו, למשל { result: 'ok' }. גם $.session.send ו-$.prompt.fill מקבלים את התוצאה של האירוע שלהם, כמו שהטבלה מראה. חפשו מה stub מחזיר מראה איזו צורה כל שם נפוץ מקבל. השגיאות האלה אומרות ש-stub שגוי או חסר. הפלט של בדיקה שנכשלה כולל בלוק שכותרתו the engine reported:, וכל שגיאה מופיעה שם:
returned neither { value } nor { deny }: stub לקריאת API של mods החזיר ערך חשוף, וזה נכשל בבדיקהno implementation forואחריו שם: המוד שלכם ביצע את הקריאה הזו ואין stub שעונה עליה
הערכה גם מייצאת mocks מוכנים לשעון, למאגר, למשתני סביבה ולשורות שנוספו לשיחה:
mock.clock(on): עונה על$.clockומחזיר שעון mock שהבדיקה שלכם מקדמת, כך שבדיקה של טיימר לא ממתינה.mock.store(on, { count: 7 }): עונה על$.storeממאגר שמתחיל עם הרשומות האלה. הוא לא מחזיר כלום, אז כדי לבדוק מה המוד שלכם שמר, כתבו את שני ה-stubs שלstoreבעצמכם כמו שבדיקת ציור עושה.mock.env(on, { CI: 'true' }): עונה על$.env.getמהמשתנים האלה.mock.session(on): מחזיר סשן mock שהמתודהappended()שלו מפרטת את השורות שהמוד שלכם הוסיף עם$.session.append, מהישנה לחדשה. זה דורש Claude Code מגרסה v2.1.293 ואילך.
#עמידה בכללים של ערכת הבדיקות
לערכת הבדיקות יש כמה כללים משלה, והפרה של אחד מהם מייצרת את השגיאות שמחברי בדיקות חדשים נתקלים בהן קודם:
רשמו כל stub לפני הקריאה הראשונה של הבדיקה על
$. קריאה ל-onאחרי זה זורקת שגיאה כמוon("ui.render") after the test first called $.session.startלא רץ מעצמו. כל בדיקה מתחילה כשהמודול שלכם נטען מחדש ואף אחד מה-hooks שלו לא נקרא, כך שמשתנים ברמת המודול מחזיקים את הערכים ההתחלתיים שלהם. אם hook תלוי במה ש-session.startמכין, ירו אותו קודם:// 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 השני עונה על קריאת
$.command.registerש-hook שלsession.startכמו זה שבמדריך מבצע. בלי זה, הקריאה הזו נדחית עםno implementation for command.registerוהערכה מדלגת על ה-hook שלכם, כך ששום דבר אחרי הקריאה בתוך ה-hook לא רץ. הבדיקה לא נכשלת בנקודה הזו. ה-hook שדולג מופיע תחתthe engine reported:רק אם בדיקה מאוחרת יותר נכשלת.hook שמחזיר
next(e)צריך stub שיענה. כשה-hookui.renderשלכם מחזירnext(e), למשל כדי לא לצייר כלום בזמן ש-Claude לא פעיל, הטעינה שלו נכשלת עםno implementation for ui.render. רשמו stub שמחזיר אלמנט כנתונים רגילים:// Stands for what Claude Code would draw at the site on('ui.render', () => ({ type: 'Text', props: {}, children: ['drawn by Claude Code'] }))כשה-stub רשום, הטעינה מצליחה, ו-
ui.find({ type: 'Text' })מחזיר את האלמנט הזה בכל פעם שה-hook שלכם החזירnext(e).stub ל-
turn.stepהוא async generator, והבדיקה קוראת את הזרם עד סופו כדי לקבל את התוצאה: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.valueכשהלולאה נגמרת,
resultהוא האובייקט שה-stub החזיר, אחרי של-hookturn.stepשלכם הייתה הזדמנות לשנות אותו. כאןresult.answerהוא'ok'.ירו קריאת כלי עם שם הכלי והארגומנטים כשדות, למשל
await $.tool.call({ tool: 'Bash', command: 'ls' }), ורשמו stub שלtool.callשמחזיר{ result }.
#חיפוש מה stub מחזיר
כל קריאת API של mods שהמוד שלכם מבצע בבדיקה צריכה stub שעונה במקום Claude Code, חוץ מהמעטים שהערכה עונה עליהם בעצמה: קריאות $.ui.invalidate, $.state, ו-$.session.append. לקריאות $.clock, השתמשו ב-mock.clock(on), אחרת $.clock.now() של המוד שלכם נכשל עם no implementation for clock.now.
הטבלה הזו מפרטת את אלה ש-mods משתמשים בהם הכי הרבה. העמודה הראשונה היא הקריאה שהמוד שלכם מבצע או האירוע שהוא מעביר הלאה עם next(e). השנייה היא הפונקציה להעביר ל-on תחת השם הזה, כך ששורת $.store.get הופכת ל-on('store.get', ($, e) => ({ value: saved.get(e.key) })). '...' בתוך stub מסמן טקסט שאתם ממלאים:
| המוד שלכם קורא או מעביר הלאה | stub |
|---|---|
$.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 | stub של 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. |
| כל קריאת API של mods שאמורה להיכשל | () => ({ deny: 'the reason' }), מה שגורם לקריאה להידחות במוד שלכם. stub שזורק מדולג במקום זאת. |
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 } }) |
$.ui.notify | () => ({ value: { isSent: true, channel: 'terminal_bell' } }). הטקסט הוא e.text והכותרת היא e.title. |
$.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 לפני כל אחת מהן.
כש-expect נכשל בתוך stub או hook שהעברתם ל-on כפונקציה רגילה ולא כ-async generator, הבדיקה נכשלת. המנוע מדלג על ה-hook הזה, ופלט הכישלון מציין אותו, למשל in the test's store.set hook.
#בדיקת טיימר
מוד שמריץ עבודה לפי טיימר צריך שעון שהבדיקה שולטת בו, כדי שהבדיקה תוכל להזיז את הזמן קדימה במקום לחכות. 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() | מריצה טיימרים שכבר הגיע זמנם, כמו שרשרת קריאות $.clock.after עם השהיה אפס, בלי להזיז את הזמן |
await clock.sleep(2000) | בתוך stub, גורמת ל-stub הזה לענות רק אחרי שהבדיקה התקדמה עד לשם, וכך מדמים מודל או תהליך איטי |
ה-hook הזה שייך למוד בשם countdown, ומטפל בפקודה /countdown שמקבלת מספר שניות, מתחילה טיימר $.clock.every של שנייה אחת, ומציגה toast באפס. כמו ב-grader, הקובץ מכיל רק את ה-hook שנבדק ולא רושם את הפקודה:
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 ומזיזה את השעון המדומה, ולכן היא בודקת שלוש שניות של התנהגות בלי לחכות שלוש שניות:
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 הראשון מראה שה-toast לא מגיע מוקדם, והשני מראה שהוא מגיע פעם אחת. כל advance נפתר אחרי שהטיימרים שהגיע זמנם רצו, ולכן הבדיקה בשורה הבאה רואה את ההשפעה שלהם.
#בדיקת ציור
בדיקה יכולה לצייר אחד מ-אתרי הרינדור של המוד, ואז ללחוץ, להקליד לתוך, ולמצוא את הרכיבים שהיא ציירה. $.ui.mount מצייר את האתר דרך ה-hook ui.render של המוד ומחזיר handle עם מתודה לכל אחת מהפעולות האלה. כדי לכסות כמה אפליקציות בבדיקה אחת, מגדירים את surface לאפליקציה שעבורה מציירים. הבדיקה הזו פותחת את ה-pane מ-בניית pane עם לשוניות, מחליפה לשוניות, לוחצת על הכפתור, ובודקת את הספירה בטרמינל ובאפליקציית 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)
})במעטפת, הריצו claude plugin test מתוך התיקייה hello-tabs. הבדיקה עוברת כששתי האפליקציות מציירות את שורת הספירה והמוד שמר 2. הספירה עוברת מהאפליקציה הראשונה לשנייה כי שני ה-mount משתמשים באותו מודול שנטען.
ל-handle ש-$.ui.mount מחזיר יש המתודות האלה, שפונות לרכיבים לפי ה-key שנתתם להם:
| מתודה | מה היא עושה |
|---|---|
press({ key: 'more' }) | לוחצת על ה-Button עם ה-key הזה |
input({ key: 'new-note', text: 'buy milk' }) | מקלידה את הטקסט לתוך ה-Input עם ה-key הזה ולוחצת Enter. מוסיפים kind: 'change' כדי להקליד בלי לשלוח. בשדה עם rows, הטקסט יכול להכיל \n. |
select({ key: 'size', value: 'large' }) | בוחרת את האפשרות עם הערך הזה ב-Select עם ה-key הזה |
find({ key: 'more' }) או find({ type: 'Text', text: 'Count: 2' }) | מחזירה את הרכיב התואם הראשון כ-{ type, props, children }, או undefined. text יכול להיות מחרוזת או ביטוי רגולרי. |
unmount() | מסירה את הציור |
כל מתודה נפתרת אחרי שה-handler שלכם סיים, כך שאפשר לבדוק את התוצאה בשורה הבאה. מגדירים את props למה ש-Claude Code היה מעביר לאתר הזה. טבלת אתרי הרינדור מפרטת את ה-props של כל אתר, ו-הטיפוסים לבילד שלכם מכילים את הטיפוסים שלהם.
כדי לתפוס עץ רכיבים שאינו תקף לאפליקציה שציינתם ב-surface, בודקים עם find רכיב שה-hook שלכם מוסיף. הבדיקה לא בודקת איך האפליקציה מציירת עץ, אז כדאי להסתכל גם על פריסה חדשה בסשן אמיתי.
#בדיקת ציור אחרי /clear
כל בדיקה מתחילה כשכל ערך $.state בברירת המחדל שלו, וכך /clear משאיר אותם. כדי לבדוק מה המוד עושה אחר כך, מדלגים על session.start, יורים classic.SessionStart עם source: 'clear', ובודקים מה המוד מצייר.
הבדיקה הזו בודקת את המודול מ-טעינת ערך שמור שוב אחרי /clear. מוסיפים אותה לקובץ מ-בדיקת ציור, שבו PANE מוגדר. הבדיקה הראשונה באותו קובץ מצפה שהכפתור ישמור את הספירה, כמו שהכפתור ב-שמירה מיותר מסשן אחד עושה:
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()
})הבדיקה עוברת כשה-hook classic.SessionStart שלכם העתיק את ה-7 השמור אל $.state לפני שה-pane מצייר. בלי ה-hook הזה במודול, ה-pane מצייר Count: 0, find מחזיר undefined, והבדיקה נכשלת ב-toBeDefined.
#בדיקת מוד מדיניות
מוד שהארגון שלכם מציין ב-prependPlugins יכול לסרב למוד אחר לפני שהוא נטען. כדי לבדוק אחד כזה, מגדירים את ה-tier של המוד ונותנים לבדיקה מוד שני שהמוד שלכם יתיר או יסרב לו:
tier: קוראים לו פעם אחת בראש קובץ הבדיקה, כמו ב-tier('prepend'), כדי לטעון את המוד כ-prepend,appendאוbuiltin, מקומו ב-סדר שבו מודים רצים. בלי זה, המוד נטען כ-user.plugins: מעבירים ל-testאובייקט אפשרויות לפני גוף הבדיקה. מערך ה-pluginsשלו מחזיק מודים שכותבים inline, כל אחד עםnameופונקצייתregister. כדי לטעון אחד במקום שאינוuser, מוסיפים לוtier.
קובץ הבדיקה הזה טוען קודם את מוד המדיניות מדף הניהול. הוא בודק שמוד המדיניות מסרב למוד שמתחיל תהליך ומתיר אחד שלא:
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' })
})במעטפת, הריצו claude plugin test מתוך התיקייה acme-guard. שתי הבדיקות עוברות עם מוד המדיניות כפי שדף הניהול מציג אותו.
ה-kit טוען כל מוד בקריאה הראשונה של הבדיקה על $. כשהמוד שלכם מסרב לאחד, הקריאה הזו זורקת, וההודעה מציינת את המוד שסורב, את המוד שסירב לו, ואת הסיבה שלכם. בבדיקה השנייה לא מסרבים לכלום, ולכן reader עונה לקריאת הכלי לפני שהיא מגיעה ל-stub.
#הצעדים הבאים
- איתור תקלות במוד: לגלות למה מוד לא עושה כלום בסשן
- מדריך העזר למודים: הקלט והתוצאה של כל אירוע, לכתיבת stubs