Claude Media
claude plugin evalのフィクスチャとモック — scaffold_scriptとmocks/の使い方

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つの条件があります。

  1. claude plugin eval . --scaffoldのように、フラグを明示したときだけ走ります
  2. 0以外の終了コードで終わると、そのrunはscaffold failedエラーでスコア0になります
  3. 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のリリースノートにあります。

この記事を共有:XはてブLinkedIn