Kiểm thử mod
Bạn có thể viết test tự động cho một mod và chạy chúng từ shell bằng claude plugin test. Một test kích hoạt các event mà hook của bạn xử lý và kiểm tra hook đã làm gì, nhờ đó bạn bắt được lỗi trước khi nó xuất hiện trong session. Ví dụ đầu tiên kiểm thử mod từ bài Tạo một mod.
Viết test
Phần tiêu đề “Viết test”Một test nạp mod của bạn, gửi event qua các hook của nó theo cách Claude Code sẽ gửi, và kiểm tra hook đã làm gì, mà không cần session, đăng nhập hay mạng. Bạn chạy test từ shell bằng claude plugin test, và mỗi file test import test kit, một thư viện test nằm trong module claude-code/testing.
Đặt tên file test kết thúc bằng .test.ts, như first-mod.test.ts, và lưu ở bất cứ đâu trong thư mục plugin. Mỗi file test cần ít nhất một test(), nếu không lần chạy sẽ lỗi với declares no test(): nothing ran. File test có thể import các file của chính mod và các helper .ts cùng cấp, nên bạn có thể unit test các hàm thuần, như luật chơi của một game, mà không cần test kit.
Test dưới đây kích hoạt hai tool call, chạy command /tally từ bài Tạo một mod, và kiểm tra rằng câu trả lời đếm đủ cả hai. Dòng đầu tiên của nó là một stub, trả lời các tool call thay cho Claude Code. Lưu thành 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) => { // Trả lời mỗi tool call thay cho Claude Code, nên không tool nào thực sự chạy on('tool.call', () => ({ result: 'ok' }))
// Kích hoạt hai tool call, hook tool.call của mod sẽ đếm chúng await $.tool.call({ tool: 'Bash', command: 'ls' }) await $.tool.call({ tool: 'Read', file_path: 'README.md' })
// Chạy /tally và kiểm tra đoạn text mà hook của nó trả về const answer = await $.command.run({ command: 'tally', args: '' }) expect(answer.text).toBe('Claude has made 2 tool calls since this mod loaded')})Trong shell, chạy test từ thư mục first-mod:
claude plugin testOutput ghi tên từng test và kết quả pass hay fail, kèm thời gian chạy sẽ khác nhau mỗi lần:
tests/first-mod.test.ts:(pass) /tally reports the tool calls the mod has seen [22.87ms]
1 pass 0 failRan 1 test across 1 file. [0.19s]Mỗi $.tool.call đi qua hook tool.call của mod, hook này cộng thêm một vào biến đếm và chuyển call tiếp cho stub. Không lệnh ls nào chạy và không file nào được đọc. Sau đó $.command.run đi tới hook command.run của mod, và answer là object mà hook đó trả về.
Lệnh thoát với status 1 khi có test fail, nên dùng được trong CI. Nếu mod của bạn không nạp được trong shell chạy lệnh, nó in một dòng bắt đầu bằng claude plugin test: hooks modules are turned off kèm lý do, và thoát với status 1.
Stub những gì Claude Code sẽ trả lời
Phần tiêu đề “Stub những gì Claude Code sẽ trả lời”Trong test không có model, store hay tool nào chạy, nên ở bất cứ chỗ nào mod của bạn chờ Claude Code trả lời, test sẽ cung cấp câu trả lời bằng một stub. Hàm test nhận hai tham số cho việc này:
$:$riêng của test, đóng vai Claude Code. Nó không phải là mods API mà hook nhận. Mỗi method của nó kích hoạt event cùng tên, gửi event qua các hook của mod, và trả về kết quả:$.tool.call({ tool: 'Bash', command: 'ls' })kích hoạttool.call.$.command.run,$.prompt.submit,$.session.startvà$.turn.completehoạt động tương tự, còn$.classic.Stopvà các method$.classickhác kích hoạt event của settings hook. Test không thể trực tiếp kích hoạt một lời gọi mods API nhưui.close. Hãy kích hoạt nó thông qua mod, ví dụ bằng cách bấm nút đóng pane.on: gọi nó để đăng ký stub, tức các hook trả lời thay cho Claude Code. Đặt tên stub cho một lời gọi mods API bằng tên bỏ$., nên một stub đăng ký với tênstore.getsẽ trả lời$.store.getcủa mod. Khi mod của bạn gọi$.model.completehoặc$.store.get, stub sẽ cung cấp câu trả lời.
Ví dụ này stub một lời gọi model. Hook thuộc về một mod tên grader, xử lý command /grade gửi một câu tới model và báo câu trả lời có bắt đầu bằng PASS hay không. File chỉ chứa hook đang được test, nên mod còn cần plugin.json và hooks.json, như trong Tạo một mod. Để gõ được /grade trong session, mod còn phải đăng ký command:
export function register(on) { on('command.run', { command: 'grade' }, async ($, e) => { // e.args là text gõ sau /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' } })}Test này stub lời gọi model để kiểm tra hook xử lý một câu trả lời đạt như thế nào:
import { expect, test } from 'claude-code/testing'
test('a passing grade is reported', async ($, on) => { // Trả lời lời gọi $.model.complete của mod bằng một câu cố định, nên không model nào chạy 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 }, }, }))
// Chạy /grade, khiến mod gọi model const answer = await $.command.run({ command: 'grade', args: 'The cat sat on the mat.' }) expect(answer.text).toBe('Passed')})Test pass vì reply của hook chính là object nằm dưới value, có text bắt đầu bằng PASS. Để kiểm tra nhánh còn lại, thêm một test thứ hai có stub trả về text bắt đầu bằng FAIL, và chờ đợi kết quả Try again.
Stub cho một lời gọi mods API trả về một object có field value, chứa giá trị mà lời gọi trả về trong mod của bạn: { value: 7 } khiến $.store.get trả về 7. Stub cho một event của Claude Code, như turn.step hay tool.call, trả về kết quả riêng của event đó, như { result: 'ok' }. $.session.send và $.prompt.fill cũng nhận kết quả theo event của chúng, như bảng bên dưới cho thấy. Phần Tra cứu stub trả về gì cho biết mỗi tên thường gặp dùng dạng nào. Các lỗi sau nghĩa là một stub bị sai hoặc bị thiếu. Output của một test fail có một khối mở đầu bằng the engine reported:, và mỗi lỗi xuất hiện ở đó:
returned neither { value } nor { deny }: một stub cho lời gọi mods API trả về giá trị trầnno implementation fortheo sau là một tên: mod của bạn đã thực hiện lời gọi đó và không có stub nào trả lời
Test kit cũng export các mock trong bộ nhớ, trả lời cả một namespace giúp bạn. mock.clock(on) trả lời $.clock, mock.store(on, { count: 7 }) trả lời $.store từ một store khởi đầu với các mục đó, và mock.env(on, { CI: 'true' }) trả lời $.env.get từ các biến đó. mock.clock trả về một đồng hồ giả mà test của bạn tự cho chạy tới, nên test cho timer không phải chờ. mock.store không trả về gì, nên để kiểm tra mod đã lưu gì, hãy tự viết hai stub store như test phần vẽ làm.
Tuân thủ quy tắc của test kit
Phần tiêu đề “Tuân thủ quy tắc của test kit”Test kit có vài quy tắc riêng, và vi phạm chúng sinh ra những lỗi mà người mới viết test hay gặp nhất:
-
Đăng ký mọi stub trước lần gọi đầu tiên trên
$. Gọionsau đó sẽ ném lỗi nhưon("ui.render") after the test first called $. -
session.startkhông tự chạy. Mỗi test bắt đầu với module vừa được nạp mới và chưa hook nào được gọi, nên các biến cấp module giữ giá trị khởi tạo. Nếu một hook phụ thuộc vào những gìsession.startthiết lập, hãy kích hoạt nó trước:// Trả lời event sau khi hook của bạn chuyển nó đi bằng next(e)on('session.start', () => ({ cwd: '/work' }))// Trả lời lời gọi $.command.register mà hook của bạn thực hiệnon('command.register', () => ({ value: undefined }))// Kích hoạt event, chạy hook session.start của bạnawait $.session.start({ surface: 'terminal', isInteractive: true, cwd: '/work' })Stub thứ hai trả lời lời gọi
$.command.registermà một hooksession.startnhư của bài hướng dẫn thực hiện. Không có nó, lời gọi đó reject vớino implementation for command.registervà test kit bỏ qua hook của bạn, nên mọi thứ sau lời gọi trong hook không chạy. Test không fail tại thời điểm đó. Hook bị bỏ qua chỉ được liệt kê dướithe engine reported:nếu một bước kiểm tra phía sau fail. -
Hook trả về
next(e)cần một stub để trả lời. Khi hookui.rendercủa bạn trả vềnext(e), ví dụ để không vẽ gì khi Claude đang rảnh, việc mount nó sẽ lỗi vớino implementation for ui.render. Đăng ký một stub trả về một element dưới dạng dữ liệu thuần:// Đại diện cho những gì Claude Code sẽ vẽ tại siteon('ui.render', () => ({ type: 'Text', props: {}, children: ['drawn by Claude Code'] }))Với stub này, việc mount thành công, và
ui.find({ type: 'Text' })trả về element đó mỗi khi hook của bạn trả vềnext(e). -
Stub cho
turn.steplà một async generator, và test đọc stream đến hết để lấy kết quả:on('turn.step', async function* ($, e) {// Mỗi yield là một phần trong câu trả lời được stream của modelyield { kind: 'text', index: 0, text: 'ok' }// Giá trị trả về là kết quả của toàn bộ requestreturn { turnId: e.turnId, index: e.index, answer: 'ok', toolUses: [], stopReason: 'end_turn', usage: null }})// Kích hoạt một request tới model, chạy hook turn.step của bạnconst stream = $.turn.step({ turnId: 't', index: 0, model: 'claude-test', messageCount: 1 })// Đọc từng phần cho đến khi stream báo đã xonglet step = await stream.next()while (step.done !== true) step = await stream.next()const result = step.valueKhi vòng lặp kết thúc,
resultlà object mà stub đã trả về, sau khi hookturn.stepcủa bạn đã có cơ hội thay đổi nó. Ở đâyresult.answerlà'ok'. -
Kích hoạt tool call với tên và tham số của tool là các field, như
await $.tool.call({ tool: 'Bash', command: 'ls' }), và đăng ký một stubtool.calltrả về{ result }.
Tra cứu stub trả về gì
Phần tiêu đề “Tra cứu stub trả về gì”Mỗi lời gọi mods API mà mod thực hiện trong test đều cần một stub trả lời thay cho Claude Code, trừ vài lời gọi test kit tự trả lời: $.ui.invalidate và các lời gọi $.state. Với các lời gọi $.clock, hãy dùng mock.clock(on), nếu không $.clock.now() của mod sẽ lỗi với no implementation for clock.now.
Bảng dưới liệt kê những lời gọi mod dùng nhiều nhất. Cột đầu là lời gọi mod của bạn thực hiện hoặc event nó chuyển đi bằng next(e). Cột thứ hai là hàm cần truyền cho on dưới tên đó, nên dòng $.store.get sẽ thành on('store.get', ($, e) => ({ value: saved.get(e.key) })). Dấu '...' trong một stub đánh dấu chỗ bạn tự điền text:
| Mod của bạn gọi hoặc chuyển đi | Stub |
|---|---|
$.command.register, $.tool.register, $.ui.toast, $.ui.log, $.ui.status, $.ui.close, $.store.set | () => ({ value: undefined }). Với ui.toast và ui.log, text nằm ở e.text. |
$.store.get | ($, e) => ({ value: saved.get(e.key) }) |
$.fs.read | ($, e) => ({ value: e.path.endsWith('notes.md') ? '# Notes' : '' }). e.path đến dưới dạng đường dẫn tuyệt đối, nên so sánh bằng endsWith. |
$.ui.open | () => ({ value: { isPlaced: true } }) |
$.ui.ask | Một stub tool.call, vì câu hỏi tới nó dưới dạng một call tới tool AskUserQuestion: ($, e) => ({ result: { answers: { [e.questions[0].question]: 'Run it' } } }). Kiểm tra e.tool trước nếu mod của bạn còn chuyển đi các tool call khác. |
$.model.complete | () => ({ value: { isAnswered: true, text: '...', usage } }) |
$.process.run | ($, e) => ({ value: { exitCode: 0, stdout: '...', stderr: '' } }). e.argv là danh sách tham số và e.init chứa cwd và timeoutMs. |
| Bất kỳ lời gọi mods API nào cần thất bại | () => ({ deny: 'the reason' }), khiến lời gọi reject trong mod của bạn. Stub ném lỗi thì sẽ bị bỏ qua thay vì vậy. |
session.start | () => ({ cwd: '/work' }) |
turn.start | ($, e) => ({ turnId: e.turnId }) |
tool.call | () => ({ result: '...' }) |
turn.complete | () => ({ text: '' }). Kích hoạt nó bằng $.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 đến dưới dạng chuỗi kể cả khi mod của bạn truyền { sessionId }. |
session.receive | ($, e) => ({ text: e.text }). Kích hoạt nó bằng $.session.receive({ origin: { kind: 'peer-send-message' }, text }). |
ui.render | () => ({ type: 'Text', props: {}, children: ['...'] }) |
expect có các assertion toBe, toEqual, toMatch, toMatchObject, toContain, toBeDefined, toBeUndefined và toThrow, cùng .not đặt trước bất kỳ cái nào trong số đó.
Kiểm thử timer
Phần tiêu đề “Kiểm thử timer”Mod chạy tác vụ theo timer cần một đồng hồ do test điều khiển, để test cho thời gian chạy tới thay vì phải chờ. const clock = mock.clock(on) trả về một đồng hồ giả bắt đầu từ 0 và chỉ chạy khi test cho nó chạy. Để bắt đầu từ thời điểm khác, truyền vào số mili giây, như mock.clock(on, { now: 5000 }). Đồng hồ có các method sau:
| Method | Chức năng |
|---|---|
await clock.advance(1000) | Cho thời gian chạy tới thêm số mili giây đó và chạy mọi timer đến hạn |
await clock.set(5000) | Cho thời gian chạy tới giá trị đó, giống advance |
clock.now() | Trả về thời gian hiện tại, chính là giá trị mà $.clock.now() của mod trả về |
await clock.settle() | Chạy các timer đã đến hạn, như một chuỗi lời gọi $.clock.after có độ trễ bằng 0, mà không cho thời gian chạy tới |
await clock.sleep(2000) | Bên trong một stub, khiến stub đó chỉ trả lời khi test đã cho thời gian chạy tới mốc đó, đây là cách bạn mô phỏng một model hay process chạy chậm |
Hook dưới đây thuộc về một mod tên countdown, xử lý command /countdown nhận một số giây, khởi động một timer $.clock.every một giây, và hiện toast khi về 0. Giống grader, file chỉ chứa hook đang được test và không đăng ký command:
export function register(on) { on('command.run', { command: 'countdown' }, async ($, e) => { // e.args là text gõ sau /countdown let left = Number(e.args) const timer = $.clock.every(1000, () => { left -= 1 if (left === 0) { timer.cancel() $.ui.toast('Time is up') } }) // Không in gì ra transcript return {} })}Test này chạy /countdown 3 và cho đồng hồ giả chạy tới, nên nó kiểm tra ba giây hoạt động mà không phải chờ ba giây:
import { expect, mock, test } from 'claude-code/testing'
test('the countdown ends with a toast', async ($, on) => { // Trả lời mọi lời gọi $.clock bằng một đồng hồ do test điều khiển const clock = mock.clock(on) // Thu thập text của mỗi toast mà mod hiển thị const toasts: string[] = [] on('ui.toast', ($, e) => { toasts.push(e.text) return { value: undefined } })
await $.command.run({ command: 'countdown', args: '3' }) // Sau hai giây timer đã chạy hai lần, chưa đến lúc có toast await clock.advance(2000) expect(toasts).toEqual([]) // Giây thứ ba đưa bộ đếm về 0 await clock.advance(1000) expect(toasts).toEqual(['Time is up'])})expect đầu tiên cho thấy toast không đến sớm, và expect thứ hai cho thấy nó đến đúng một lần. Mỗi advance trả về sau khi các timer đến hạn đã chạy, nên phép kiểm tra ở dòng tiếp theo thấy được tác động của chúng.
Kiểm thử phần vẽ
Phần tiêu đề “Kiểm thử phần vẽ”Test có thể vẽ một trong các render site của mod, rồi bấm, gõ vào, và tìm các element mà nó đã vẽ. $.ui.mount vẽ site thông qua hook ui.render của mod và trả về một handle có method cho từng thao tác đó. Để bao quát nhiều ứng dụng trong một test, đặt surface là ứng dụng cần vẽ. Test dưới đây mở pane từ phần Xây dựng pane có tab, chuyển tab, bấm nút, và kiểm tra con số trong cả terminal lẫn ứng dụng Desktop:
import { expect, test } from 'claude-code/testing'
// Những gì Claude Code truyền cho một hook ui.render của pane này, trừ ứng dụngconst 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 bằng một Map, để test đọc được những gì mod đã lưu 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 } })
// Vẽ pane một lần cho mỗi ứng dụng for (const surface of ['terminal', 'desktop'] as const) { const ui = await $.ui.mount({ ...PANE, surface }) // Bấm các nút theo key mà mod đã đặt cho chúng await ui.press({ key: 'tab-two' }) await ui.press({ key: 'more' }) // Dòng đếm của tab thứ hai có trong phần vẽ expect(await ui.find({ type: 'Text', text: /^Count: \d+$/ })).toBeDefined() await ui.unmount() }
// Mỗi ứng dụng bấm một lần, tổng là hai expect(saved.get('count')).toBe(2)})Trong shell, chạy claude plugin test từ thư mục hello-tabs. Test pass khi cả hai ứng dụng đều vẽ dòng đếm và mod đã lưu 2. Con số được giữ từ ứng dụng thứ nhất sang ứng dụng thứ hai vì cả hai lần mount dùng cùng một module đã nạp.
Handle mà $.ui.mount trả về có các method sau, chúng xác định element theo key bạn đã đặt:
| Method | Chức năng |
|---|---|
press({ key: 'more' }) | Bấm Button có key đó |
input({ key: 'new-note', text: 'buy milk' }) | Gõ text vào Input có key đó rồi nhấn Enter. Thêm kind: 'change' để gõ mà không submit. |
select({ key: 'size', value: 'large' }) | Chọn lựa chọn có value đó trong Select có key đó |
find({ key: 'more' }) hoặc find({ type: 'Text', text: 'Count: 2' }) | Trả về element khớp đầu tiên dưới dạng { type, props, children }, hoặc undefined. text có thể là chuỗi hoặc regular expression. |
unmount() | Gỡ phần vẽ |
Mỗi method trả về sau khi handler của bạn chạy xong, nên bạn có thể kiểm tra kết quả ngay ở dòng tiếp theo. Đặt props đúng như những gì Claude Code sẽ truyền cho site đó. Bảng render site liệt kê props của từng site, và type cho bản build của bạn có type của chúng.
Test phần vẽ kiểm tra cây mà hook của bạn trả về và cây đó có hợp lệ với ứng dụng hay không. Nó không kiểm tra ứng dụng vẽ cây ra sao, nên với layout mới, hãy xem thêm trong một session thật.
Kiểm thử phần vẽ sau /clear
Phần tiêu đề “Kiểm thử phần vẽ sau /clear”Mỗi test bắt đầu với mọi giá trị $.state ở mặc định, đúng như trạng thái mà /clear để lại. Để kiểm thử mod của bạn làm gì tiếp theo, hãy bỏ qua session.start, kích hoạt classic.SessionStart với source: 'clear', và kiểm tra mod vẽ gì.
Test này kiểm tra module từ phần Nạp lại giá trị đã lưu sau /clear. Thêm nó vào file ở phần Kiểm thử phần vẽ, nơi PANE đã được định nghĩa. Test đầu tiên của file đó chờ đợi nút sẽ lưu con số, như nút trong phần Lưu từ nhiều session làm:
test('the saved count comes back after /clear', async ($, on) => { // Store đã chứa con số 7 on('store.get', () => ({ value: 7 })) // Trả lời event sau khi hook của bạn chuyển nó đi bằng next(e) on('classic.SessionStart', () => ({}))
// Kích hoạt event diễn ra sau /clear, chạy hook của bạn await $.classic.SessionStart({ source: 'clear' })
const ui = await $.ui.mount({ ...PANE, surface: 'terminal' }) await ui.press({ key: 'tab-two' }) // Pane hiển thị con số đã lưu, không phải giá trị mặc định 0 expect(await ui.find({ type: 'Text', text: 'Count: 7' })).toBeDefined()})Test pass khi hook classic.SessionStart của bạn đã copy số 7 đã lưu vào $.state trước khi pane được vẽ. Nếu module không có hook đó, pane vẽ Count: 0, find trả về undefined, và test fail tại toBeDefined.
Kiểm thử policy mod
Phần tiêu đề “Kiểm thử policy mod”Một mod mà tổ chức của bạn liệt kê trong prependPlugins có thể từ chối mod khác trước khi nó được nạp. Để kiểm thử một mod như vậy, hãy đặt tier cho mod của bạn và cho test một mod thứ hai để mod của bạn cho phép hoặc từ chối:
tier: gọi nó một lần ở đầu file test, nhưtier('prepend'), để nạp mod của bạn ở tierprepend,appendhoặcbuiltin, tức vị trí của nó trong thứ tự chạy của các mod. Không có nó, mod của bạn được nạp ở tieruser.plugins: truyền chotestmột object option, đặt trước thân test. Mảngpluginscủa nó chứa các mod bạn viết trực tiếp, mỗi mod có mộtnamevà một hàmregister. Để nạp một mod ở tier khácuser, thêmtiercho nó.
File test này nạp policy mod từ trang quản trị đầu tiên. Nó kiểm tra rằng policy mod từ chối một mod có khởi chạy process và cho phép một mod không làm vậy:
import { expect, test, tier } from 'claude-code/testing'
// Nạp mod đang được test trước mọi mod kháctier('prepend')
// Mod thứ hai có code gọi $.process.run, thứ mà policy chặnconst runner = { name: 'runner', register(on) { on('tool.call', async ($, e, next) => { await $.process.run(['ls']) return { result: 'runner answered' } }) },}
// Mod thứ hai không gọi gì bị policy chặnconst 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 { // Lần gọi đầu tiên trên $ nạp các mod, nên lỗi từ chối được ném ra ở đây 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' }) // Câu trả lời đến từ reader, chứng tỏ nó đã được nạp expect(out).toEqual({ result: 'reader answered' })})Trong shell, chạy claude plugin test từ thư mục acme-guard. Cả hai test đều pass với policy mod như trang quản trị trình bày.
Test kit nạp mọi mod ở lần gọi đầu tiên trên $ của test. Khi mod của bạn từ chối một mod, lời gọi đó ném lỗi, và thông báo ghi tên mod bị từ chối, mod đã từ chối nó, và lý do của bạn. Trong test thứ hai không có gì bị từ chối, nên reader trả lời tool call trước khi nó tới stub.
Đọc tiếp
Phần tiêu đề “Đọc tiếp”- Xử lý sự cố mod: tìm hiểu vì sao một mod không làm gì trong session
- Tham chiếu Mods: input và result của mọi event, dùng khi viết stub
Bài tiếp theo: Xử lý sự cố mod - Đối chiếu triệu chứng hoặc thông báo với nguyên nhân, tra cứu thông báo từ chối và đọc debug log.