claude plugin evalのフィクスチャとモック — scaffold_scriptとmocks/の使い方
claude plugin evalのケースに、ファイルやgit状態、前の会話、MCPツールの返り値を用意する方法。scaffold_scriptとmocks/の書き方と、実サーバーを起動する条件を解説します。
claude plugin evalの各runは、空の作業ディレクトリから始まります。コミット履歴が要るスキルも、MCPツールを呼ぶスキルも、そのままでは検証できません。そこで使うのが、ケースの隣に置くcase.yamlのcontextブロックと、evals/mocks/のモックファイルです。
コマンドの基本とgraderの選び方はclaude plugin evalの使い方にまとめています。本記事は、その手前にある「ケースが動く下地をどう作るか」を扱います。
空のワークスペースに何を足せるか
runは一時的なホームディレクトリ、作業ディレクトリ、Claude Code設定を毎回新しく作り、そこでclaude -pの子プロセスを動かします。個人のCLAUDE.md・フック・MCPサーバー・他のプラグイン・メモリー・スキルは載りません。
足せるものは、case.yamlのcontextに書く次の3つです。
contextで足せる3つの下地
scaffold_script
作業ディレクトリにファイルやgitリポジトリを作るBashスクリプトです。
--scaffoldを付けたときだけ実行されます。history_file
前の会話を
.jsonlのトランスクリプトで渡します。ケースのプロンプトは、その続きの発言として扱われます。add_dirs
Claudeが読んでよいフィクスチャのディレクトリです。読み取り専用で、ケースのディレクトリの中にあるものに限られます。
case.yamlにはschema_version: "1.1"とnameが必須です。prompt.mdと併存させる場合は、prompt.mdのfrontmatterが同じ項目のcase.yamlを上書きし、prompt.mdの本文がプロンプトになります。
schema_version: "1.1"
name: changelog-from-diff
tags: [smoke]
context:
scaffold_script: fixture.sh
add_dirs: [resources]この例は、スクリプトで作業ディレクトリを整え、resources/にある資料をClaudeに読ませる構成です。
scaffold_scriptでgit状態を作る
スキルの検証で多いのは、「コミットが数件あるリポジトリ」が前提になるケースです。次はそのためのスクリプトの一例です(ファイル名はcase.yamlのscaffold_scriptに合わせます)。
#!/usr/bin/env bash
set -euo pipefail
git init -q
git config user.email "eval@example.com"
git config user.name "eval"
echo "export const getUser = () => null" > user.ts
git add user.ts && git commit -q -m "add getUser"
sed -i.bak 's/getUser/fetchUser/' user.ts && rm user.ts.bakスクリプトは空のワークスペースの中で始まります。渡る環境は小さく固定されています。
PATHはシェルのものがそのまま引き継がれますHOMEは、そのrun用の一時ホームになりますTMPDIRと、TERM=dumbのようないくつかの定数が入ります- ケースの
EVAL_*変数は、スクリプトには届きません
上のスクリプトがgit configをリポジトリ内に書いているのは、HOMEがrun専用の一時ディレクトリで、普段のグローバル設定を当てにできないためです。
実行には3つの条件があります。
claude plugin eval . --scaffoldのように、フラグを明示したときだけ走ります- 0以外の終了コードで終わると、そのrunは
scaffold failedエラーでスコア0になります - 120秒を超えた場合も、同じく失敗です
スクリプトは、エージェントのサンドボックスの外で、あなた自身の権限で動きます。他人が書いたスイートに--scaffoldを付けると、そのスクリプトを自分の権限で実行することになります。自分や自組織が書いたスイートにだけ使います。
もう一つの落とし穴は、スクリプトで.claude/やCLAUDE.md、.mcp.jsonを書いても読み込まれないことです。作業ディレクトリの中にあっても、その上にあっても、プロジェクト単位の設定はどこからも読まれません。スクリプトで用意できるのはファイルとgitの状態だけです。スキル、エージェント、フック、MCPサーバーに依存するケースなら、それらをテスト対象のプラグインに同梱します。
history_fileで会話の途中から始める
「前の発言を受けて動くスキル」を検証するには、.jsonlのトランスクリプトをケースのディレクトリに置き、context.history_fileに指定します。ケースのプロンプトが、次のユーザー発言になります。
schema_version: "1.1"
name: follow-up-edit
context:
history_file: previous-session.jsonl注意が要るのはベースラインです。対象を.のようなパスで渡した場合、history_fileを持つケースは、既定でプラグインありのアームだけを実行します。記録済みの会話にすでにプラグインの影響が入っている、という前提だからです。実行時にはstderrにsingle-arm (no Δ)という通知が出ます。
再開した1ターンをプラグインあり・なしで比べたいなら、--ablation with-withoutを渡します。W/OUT列やΔが出ない原因を探しているときは、まずこの既定を疑います。
MCPツールをモックする
プラグインのスキルがMCPツールを呼ぶ場合、実サービスにつながなくても検証できます。モックは1ツールにつき1つのMarkdownファイルです。
evals/
├── mocks/ # スイート全体で共有
│ └── tracker/ # プラグインのMCP設定に書いたサーバー名
│ ├── create_issue.md
│ └── fixtures/
└── <case>/
└── mocks/ # このケースだけ。同名ファイルはスイート側を上書き<server>には、プラグインの.mcp.jsonなどのMCP設定に書いたサーバー名を使います。配置の考え方は、プラグインに同梱されたMCPサーバーの記事で扱っているサーバー名とツール名の付き方と対応します。
モックファイルの例です。trackerサーバーのcreate_issueを置き換え、入力の形を検査し、タイトルを返します。
---
expect:
title: string
priority: [low, medium, high]
---
Created issue #4821: {{input.title}}本文がそのままツールの返り値になります。frontmatterと本文で使える仕組みは次のとおりです。
| 仕組み | 書き方 | 何が起きるか |
|---|---|---|
| 入力の埋め込み | 書き方{{input.<field>}} | 何が起きるか呼び出しの入力フィールドが返り値に入る |
| ファイルの埋め込み | 書き方{{file:fixtures/<name>}} | 何が起きるかモックの隣にあるファイルの中身が入る |
| 入力の検査 | 書き方expect: | 何が起きるか違反するとrunがスコア0で中断され、abortedとして報告される |
| エラー返却 | 書き方error: true | 何が起きるか本文をツールエラーとして返す(fixed型のみ) |
| judgeによる応答 | 書き方type: agent | 何が起きるか本文を指示として、judgeモデルがサーバー役を演じる |
モックが無いツールはClaudeから見えない
ここが挙動の核です。runはプラグインの実サーバーを起動しません。代わりに、各サーバーと同じ名前の代役サーバーが登録されます。
- モックファイルのあるツールは、その内容で応答します。
--allow-toolsの許可も不要です - モックファイルのないツールは、Claudeから使えません
- モックが1つも無いサーバーは、進捗の
mocked:行にplugin_<plugin>_<server>[not started: no mock]と出ます
スキルが3つのツールを呼ぶなら、3つ分のファイルが要ります。1つ足りないと、そのツールはClaudeから呼べないままrunが進みます。実サーバーの説明文と入力スキーマを保存した_tools.jsonを置くと、モック側のツールにも本物の説明と入力定義が載ります。
expect:の正規表現は小さな方言
expect:には型名(string・number・boolean・array・object)、リテラル、リテラルのリスト、/regex/を書けます。正規表現は独自の限定された文法で、読み込み時に検査されます。
- 使えるのは、リテラル、
.、\dのようなエスケープ、[a-z]のような文字クラスです - 量指定は
*・+・?・{m,n}で、1文字・エスケープ・文字クラスの直後にだけ付けられます - 先頭の
^と末尾の$は使えます。フラグはiとsだけです
グループ、|による選択、後方参照、先読みを含む式は、ケースが読み込めず、スコア0になります。複数の固定値を許したいときは、|ではなくリテラルのリストを使います。
返り値を固定するか、judgeに演じさせるか
type: fixedは常に同じ本文を返すので、結果が再現できます。type: agentは応答のたびに--judge-modelのモデルを呼ぶため、runごとに出力が変わり、judgeを変えても変わります。
その揺れを抑える仕組みがあります。エラーや中断なしに終わったrunは、結果ディレクトリのmock-recordings/にagentモックの回答を保存します。ADOPT.txtに、各記録と「どこへコピーするか」が書かれています。指示どおりmocks/.replay/<server>/にコピーすれば、同じ呼び出しにはモデル呼び出しなしで同じ回答が返ります。CIで再現性が要るなら、mocks/.replay/をコミットしておきます。
agentモックには呼び出し回数の予算があります。run内のすべてのagentモックで、ケースのmax_turnsの4倍(既定10なら40回)を共有します。.replay/からの回答も数えます。超えたrunはスコア0で中断されるので、呼び出しが多いスキルはmax_turnsを上げます。
1つのagentモックで複数ツールをまとめて答えさせたいなら、サーバーのディレクトリに_server.mdを置き、frontmatterのtools:に対象を並べます。同じツールの<tool>.mdがあれば、そちらが優先されます。_server.mdにexpect:を書くのは、tools:が1ツールだけの場合に限られます。
実サーバーを起動したいとき
モックではなく本物のMCPサーバーに当てる方法も2つあります。
実サーバーを使う2つのフラグ
--allow-real-servers
モックを置いたツールは、引き続きファイルから応答します。モックしていないサーバーだけ、実プロセスを起動します。
--mocks off
mocks/を読まず、プラグインが宣言するサーバーをすべて起動します。
どちらも、そのプロセスはあなたの権限で、runのサンドボックスの外で動きます。さらに、ツールを使うには--allow-toolsでの名前指定の許可も要ります。プラグインのMCPツール名はmcp__plugin_<plugin>_<server>__<tool>の形です。
claude plugin eval . \
--allow-real-servers \
--allow-tools "mcp__plugin_my-plugin_tracker__*"実サーバーやフックを走らせると、graderが読むファイルをそれらが書き換えられます。コンテナやCIランナーのような隔離環境で回していない限り、スコアは参考値として扱うことになります。
スコアへの影響 — mock_callsのgraderはΔから外れる
モックを使うケースでは、呼び出し内容を採点するtarget: mock_callsのregexや、focus: mock_callsのllmのgraderが使えます。ただしプラグインあり・なしの2アーム実行では、モック対象のサーバーがすべてそのプラグイン自身の宣言したものである場合、これらのgraderはscored: falseになります。プラグインなしでは原理的に通らないため、Δを水増ししないよう、スコア計算から外れるのです。
結果のJSONでpassed: falseなのにrunが1.0になっていても、この仕様です。ケースのgraderが全部この除外対象なら、除外されずに通常どおり採点されます。モック経由の呼び出しを必ず採点に含めたい場合は、そのgraderにarm: bothを指定します。
つまずいたときの切り分け
- モックを置いたのにツールが使えない:
<server>のディレクトリ名が、プラグインのMCP設定のサーバー名と一致しているかを確認します - runがスコア0で
abortedになる:expect:の違反か、agentモックの呼び出し予算の超過です。結果に出るサーバー名・ツール名・理由を読みます - ケースが読み込めない:
expect:の正規表現に、グループや|を使っていないかを見ます。case.yamlのschema_versionとnameの抜けも同じ症状になります - Bashを付けたら全runが失敗する: サンドボックスの前提が足りない可能性があります。Docker Desktop入りのMacでの拒否はclaude plugin evalがDocker入りMacでBash付き実行を拒否する原因で扱っています
- そもそも設定が壊れていないか: プラグイン側のMCP設定の取りこぼしはclaude plugin validateでMCP設定の取りこぼしを見つけるで先に潰せます
claude plugin eval自体はClaude Code v2.1.269以降で使えます。コマンドの追加時点の内容はv2.1.269のリリースノートにあります。