tdd について
開発者やコーディングエージェント向けの TDD スキルで、作業をシナリオ単位の RED/GREEN サイクルに分けます。実装前に実行可能な失敗テストで要件を確かめ、最小限の変更と検証を進めます。
主な機能
- 01順序付きシナリオ一覧の作成
- 02各サイクルで実行可能なテストを1件作成
- 03実装前に RED を確認
- 04最小変更で GREEN を達成
- 05検証と任意のリファクタリング
- 06メモリーを使った中断作業の再開
活用シーン
- 01テスト先行での新機能開発
- 02回帰テストを伴うバグ修正
- 03シナリオに基づくリポジトリの改修
- 04中断後の TDD 作業の再開
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ oimiragieo/ agent-studio --skill tddインストール後、Claude Code で/tddを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
manifest.jsontesting-anti-patterns.mdbenchmarks/scenario-async-error-handling.jsonscenario-bug-fix-with-regression.jsonscenario-red-green-refactor.jsoncommands/tdd.mdhooks/post-execute.cjspre-execute.cjsobservations/.gitkeepREADME.mdreferences/research-requirements.mdtdd-memory-profile.mdrules/tdd.mdschemas/input.schema.jsonoutput.schema.jsonscripts/main.cjstemplates/implementation-template.md詳細
テスト駆動開発(TDD)
概要
このスキルでは、AI 固有のガードレールを備えた Canon TDD を実践します。
- シナリオリストを作成または更新します。
- 実行可能なテストとして、シナリオを正確に 1 つ実行します。
- RED を証明します。
- GREEN にするための最小限の変更を実装します。
- 必要に応じてリファクタリングします。
- シナリオリストが空になるまで繰り返します。
使用するタイミング
次の用途に使用します。
- 新機能
- バグ修正
- 振る舞いの変更
- テストを基準にしたリポジトリ規模のパッチ適用
- テストを実行可能な仕様とする、AI を活用したコード生成
次の場合に限り、スキップする前に人間の承認を求めます。
- 使い捨てのプロトタイプ
- 実行経路のない、宣言的な設定のみの編集
- 保守されない、一度限りの移行スクリプト
鉄の掟
NO PRODUCTION CODE WITHOUT A FAILING TEST FIRST
先にコードを書いてしまった場合は、そのコードを破棄し、RED からやり直してください。
Canon ループ
ステップ 0:シナリオのバックログを作成/更新する
バックログを作成する前に、過去の失敗パターンと再利用可能なテストテンプレートについて、メモリを検索してください。
Skill({ skill: 'memory-search' }); // query: "<feature-name> test failure signatures"
このタスクに関連する、繰り返し発生するアンチパターンについて .claude/context/memory/learnings.md を読んでください。
次に、以下を行います。
- このタスクのテストシナリオを、短く順序付けたリストにまとめます。
- 実装のしやすさではなく、設計上のシグナルとリスクを基準に優先順位を付けます。
- 実行中に見つかったシナリオを追加します。
- メモリのテンプレートを再利用します。すでに記録された失敗パターンを繰り返さないでください。
ステップ 1:シナリオを正確に 1 つ選び、実行可能なテストを 1 つ書く
- 1 サイクルにつき 1 つの振る舞いを扱います。
- 振る舞いが明確に伝わる名前を付けます。
- 実際の協調オブジェクトを優先し、外部境界のみをモックします。
ステップ 2:RED を証明する
- 対象を最も絞ったテストコマンドを実行します。
- 失敗の原因は、構文やセットアップのエラーではなく、振る舞いの不足でなければなりません。
- RED の証拠(テストファイルと失敗したアサーションのメッセージ)を記録します。
ステップ 3:GREEN にするための最小限のパッチを実装する
- 現在の RED テストで必要なものだけを実装します。
- 先回りした API の追加や、無関係なクリーンアップは行いません。
- パッチの範囲を現在のシナリオに限定します。
ステップ 4:GREEN を証明する
- 対象を絞ったテストコマンドを再実行します。
- 影響を受けるテストスイート(またはパッケージレベルのテストセット)を実行します。
- リグレッションがないことを確認します。
不安定テストのゲート(非同期、フック、または非決定的なテストでは必須):
非同期 I/O、stop フック、タイマー、またはファイルシステム操作を伴うテストでは、1 回の成功だけでは不十分です。GREEN と宣言する前に、3 回連続で成功する必要があります。
# Run 3 times — all 3 must pass
node --test tests/hooks/routing-guard.test.cjs && \
node --test tests/hooks/routing-guard.test.cjs && \
node --test tests/hooks/routing-guard.test.cjs
1 回目に成功し、2 回目に失敗するテストは GREEN ではなく RED です。3 回連続の成功を確認するまで、ステップ 5 に進まないでください。
ミューテーションテストのゲート(セキュリティ上重要なコードのみ):
セキュリティフック、ルーティングバリデーター、認証ロジック、およびアクセスや信頼の判断を制御するコードパスでは、GREEN を達成した後に Stryker のミューテーションテストを実行します。テストが実際に障害を検出し、むなしく成功しているだけではないことを確認してください。
# Run Stryker mutation testing (threshold: 85%)
npx stryker run
# Require mutationScore >= 85 in stryker.config.json
セキュリティフックの fast-check ベースのプロパティテストでは、フェイルクローズのプロパティがミューテーションテストと同等のゲートになります。
// fast-check fail-closed property — must hold for any input
fc.assert(
fc.property(fc.anything(), input => {
const result = securityHook(input);
// Hook must NEVER return allow=true for malformed/unexpected input
expect(result.allow).not.toBe(true);
})
);
セキュリティ以外のアプリケーションコードでは、このゲートをスキップします(ステップ 4 からステップ 5 に直接進みます)。
ステップ 5:必要に応じてリファクタリングする
- テストが成功している場合に限り、リファクタリングします。
- リファクタリング後に同じテストセットを再実行します。
ステップ 5.5:プロパティベーステスト(ユーティリティ関数やセキュリティフックに推奨)
リファクタリング後(またはセキュリティが重要なコードの場合はステップ 4 の後)、例ベースのテストをプロパティベーステストで補完することを検討してください。PBT は、自己欺瞞のサイクルを断ち切ることで、LLM コード生成において例ベースの TDD のみの場合と比べて pass@1 を 23.1~37.3% 改善します(arXiv:2506.18315)。
実施するタイミング:
- ユーティリティ関数(エンコード/デコード、パーサー、シリアライザー、計算機)
- セキュリティフック(入力バリデーター、サニタイザー、アクセス制御ロジック)
- 不変条件、往復性、または数学的性質を定義できる関数
実行方法:
Skill({ skill: 'property-based-testing' });
特定すべき主なプロパティのパターン:
| パターン | 例 |
|---|---|
| 往復性 | decode(encode(x)) === x |
| 冪等性 | normalize(normalize(x)) === normalize(x) |
| 不変条件 | sort(arr).length === arr.length |
| フェイルクローズ(セキュリティ) | securityHook(anyInput).allow !== true(明示的にホワイトリストに登録されている場合を除く) |
PBT は Canon TDD の補完であり、置き換えるものではありません。Canon の RED/GREEN/REFACTOR を先に完了させ、GREEN が確認された後に PBT を実行します。
ステップ 6:バックログが空になるまで繰り返す
AI 支援のガードレール
- テストを実行可能なプロンプトのコンテキストとして使い、プロンプトは短く、テストに焦点を絞ります。
- 決定論的なテスト(安定したフィクスチャー、非決定的な順序なし)を優先します。
- 修正ループに上限を設けます。シナリオごとに最大 3 回まで修正を試みてから、設計を見直します。
- テストの不正操作を防ぐチェックを実行します。
- 変更されたアサーションが、元の要件を引き続き表していることを確認します。
- バグ修正タスクには、少なくとも 1 つのネガティブテストを追加します。
- コードがテスト専用の成果物に応じて分岐しないようにします。
メモリー高速化レイヤー
繰り返し発生するセットアップやトリアージを減らすために、軽量なメモリーのみを使用します。
- リポジトリ内で推奨されるテスト/lint/フォーマットのコマンド
- 繰り返し発生する失敗のパターンと簡潔な修正概要
- 繰り返し注意すべきアンチパターン
- 再利用可能なシナリオテンプレート
参照:references/tdd-memory-profile.md
厳守事項:
- メモリーによって RED の証明を省略してはいけません
- メモリーによって Canon の順序を変更してはいけません
- プロファイルは適度な範囲に保ち、ノイズを抑えます
テスト駆動プロンプティング(TDP)— 2026 年標準パターン
TDP は、マルチエージェント TDD における 2026 年の主流パターンです。失敗したテストの出力をそのまま開発者エージェントの起動プロンプトに挿入します。これにより、解釈の誤りをなくし、開発者はテストランナーが見ているものを正確に確認できます。
パターン
失敗を文章で説明する代わりに、stdout/stderr を取得して、そのまま直接挿入します。
// Step 1: Run test and capture raw output
const { execSync } = require('child_process');
let testOutput = '';
try {
execSync('node --test tests/hooks/routing-guard.test.cjs', { encoding: 'utf-8' });
} catch (e) {
testOutput = e.stdout + e.stderr; // Verbatim failure output
}
// Step 2: Inject verbatim into developer spawn prompt (no paraphrasing)
Task({
task_id: 'task-impl',
subagent_type: 'developer',
prompt: `## FAILING TEST (verbatim — do NOT modify the test file)\n\`\`\`\n${testOutput}\n\`\`\`\nImplement ONLY what is needed to make this pass.`,
});
TDP が機能する理由
- 失敗の説明を言い換えることによる誤りをなくします(伝言ゲーム効果)。
- 開発者は、行番号や実際の値と期待値など、アサーションのコンテキスト全体を確認できます。
- 最小限の実装を促します。開発者は、テストで要求されたものだけを実装できます。
- QA エージェントのテスト意図と開発者の解釈との間で仕様がずれるのを防ぎます。
TDP とマルチエージェント TDD の分解
| ステップ | エージェント | アクション |
|---|---|---|
| 1 | qa | 失敗するテストを作成し、テストのみをコミットして、生の出力を取得する |
| 2 | Router | テスト出力を抽出し、TDP の起動プロンプトを作成する |
| 3 | developer | テスト出力をそのまま仕様として使い、GREEN になるよう実装する |
| 4 | reflection-agent | テストアサーションが変更されていないことを確認する(git diff チェック) |
出典: Simon Willison(2026)—「エージェントのための Red/Green TDD:失敗したテスト出力そのものが仕様である」;TDFlow arXiv:2510.23761。
ralph-loop による自律的な TDD(セッションをまたいだ反復)
セッションが中断される可能性のあるリポジトリ規模の TDD では、ralph-loop(モード 2 — ルーター管理)を組み込み、中断後も TDD シナリオのバックログを維持します。
TDD 状態スキーマ
.claude/context/runtime/tdd-state.json に TDD 専用の状態ファイルを保持します。
{
"scenarios": [
{
"id": "sc-001",
"description": "routing-guard blocks Write on creator paths",
"status": "pending"
},
{ "id": "sc-002", "description": "spawn-token-guard warns at 80K tokens", "status": "green" }
],
"completedScenarios": [
{
"id": "sc-002",
"evidenceCommand": "node --test tests/hooks/spawn-token-guard.test.cjs",
"passedAt": "2026-03-12T10:00:00Z"
}
],
"currentScenario": "sc-001",
"evidenceLog": [
{
"scenarioId": "sc-001",
"phase": "red",
"output": "AssertionError: expected exit code 2, got 0",
"timestamp": "..."
}
]
}
再開パターン
各イテレーションの開始時に、TDD 状態ファイルを読み込みます。
// Step 0 — before building/refreshing backlog
const state = JSON.parse(
fs.readFileSync('.claude/context/runtime/tdd-state.json', 'utf-8') || '{}'
);
const completedIds = (state.completedScenarios || []).map(s => s.id);
const remaining = (state.scenarios || []).filter(s => !completedIds.includes(s.id));
// Pick next scenario from remaining — never re-run completed ones
ralph-loop モード 2 との統合
- ルーターは
{ task_id, subagent_type: 'qa', prompt: TDP_PROMPT + verbatim state }を指定してqaエージェントを起動します。 qaはテストを作成 → 実行 → 出力を取得 →tdd-state.jsonを更新します(フェーズ:red)。- ルーターは TDP プロンプトを指定して
developerを起動します(テスト出力をそのまま挿入)。 developerは実装 →tdd-state.jsonを更新します(フェーズ:green)。- ルーターは
remaining.length === 0を確認し、RALPH_AUDIT_COMPLETE_NO_FINDINGSを出力します。 remaining > 0の場合、次のシナリオでステップ 1 に戻ります。
アンチパターン: 状態内で green とマーク済みのシナリオを再実行してはいけません。イテレーションを無駄にし、エビデンスログを破損させる可能性があります。
リポジトリ規模およびクラスレベルのガイダンス
- リポジトリ規模の作業では、失敗するテストのクラスターごとに分解し、各ループに 1 つのクラスターを割り当てます。
- クラスレベルの合成では、メソッドの依存順序を導き出し、メソッドレベルの公開テストを使って、一度に 1 つのメソッドを実装します。
- 各ループを 1 つのシナリオと 1 つのパッチ目標に限定し、長いコンテキストによる負荷を抑えます。
検証チェックリスト
- シナリオのバックログが存在し、作業中に更新された
- すべての本番環境向け変更が、少なくとも 1 つの失敗後に成功したテストに対応している
- RED のエビデンスを取得した(コマンド+失敗の概要)
- GREEN のエビデンスを取得した(コマンド+成功の概要)
- 変更対象の範囲に未解決のテスト失敗がない
- lint/フォーマット/テストの各コマンドが完了した、またはブロックされたことを明示した
- テストを不正に操作するパターンが検出されていない
完了前のコマンド(プロジェクトの範囲内)
プロジェクトで実際に使われているコマンドを使用します。一般的な実行順序:
# 1) targeted test
pnpm test <target>
# 2) impacted suite
pnpm test
# 3) lint
pnpm lint
# 4) format check
pnpm format:check
リポジトリで異なるスクリプトを使う場合は、これらをローカルの同等コマンドに置き換え、実際に実行した内容を正確に報告します。
合理化への対策
- 「テストはあとで追加する」→ いったん止め、現在の RED テストを作成します。
- 「小さすぎてテストする必要はない」→ 最小限の振る舞いテストを 1 つ作成します。
- 「手動でテスト済み」→ 手動実行は、実行可能な回帰テストの代わりにはなりません。
- 「テスト前のコードを削除するには時間をかけすぎた」→ サンクコストです。RED からやり直します。
関連ファイル
references/research-requirements.mdreferences/tdd-memory-profile.mdtesting-anti-patterns.mdrules/tdd.mdtemplates/implementation-template.md
研究の根拠
このスキルは以下に準拠しています。
- Martin Fowler の TDD(2023 年 12 月 11 日)
- Kent Beck の Canon TDD(2023 年 12 月 11 日)
- Rafique & Misic のメタ分析、IEEE TSE DOI:10.1109/TSE.2012.28
- LLM4TDD(arXiv:2312.04687)
- コード生成のためのテスト駆動開発(arXiv:2402.13521)
- プロンプトとしてのテスト(arXiv:2505.09027)
- SWE-Flow(arXiv:2506.09003)
- TDFlow(arXiv:2510.23761)
- 関数からクラスへの TDD のスケーリング(arXiv:2602.03557)
メモリプロトコル
開始前:
.claude/context/memory/learnings.md を読みます。
完了後:
- 新しいパターン →
.claude/context/memory/learnings.md - 発見した問題 →
.claude/context/memory/issues.md - 決定した内容 →
.claude/context/memory/decisions.md
中断を想定してください。メモリに記録されていなければ、実行されていないものとします。
Agent-Studio の TDD 拡張(2026 年)
フックのテストパターン
フックは stdin/stdout の JSON プロトコルを使用します。
const proc = require('child_process').spawn('node', ['.claude/hooks/routing/routing-guard.cjs'], {
shell: false,
});
proc.stdin.write(JSON.stringify({ tool_name: 'Write', tool_input: {} }));
proc.stdin.end();
// Exit 0=allow, 2=block
メモリの TDD
MemoryRecord をモックします。信頼度ゲート(しきい値 0.7)をテストします。アトミック書き込みを使用します。
プロパティベーステスト
不変条件がある関数には、ルーティングだけでなく、fast-check(vitest との統合には @fast-check/vitest)を使用します。fast-check 3.x(2025)では、Unicode、日付、bigint の Arbitrary が改善されています。
ルーティングの不変条件(既存):
import fc from 'fast-check';
fc.assert(
fc.property(fc.string(), intent => {
return typeof routeIntent(intent) === 'string';
})
);
メモリのシリアライズとラウンドトリップ(新規):
// Property: serialize(deserialize(x)) === x for all JSON-serializable values
fc.assert(
fc.property(fc.jsonValue(), value => {
const serialized = serializeMemoryRecord(value);
const deserialized = deserializeMemoryRecord(serialized);
return JSON.stringify(deserialized) === JSON.stringify(value);
})
);
フック検証の不変条件(新規):
// Property: for any tool input, isValidInput(x) === !isBlocked(x)
// (validation and blocking must be inverses)
fc.assert(
fc.property(fc.record({ tool_name: fc.string(), tool_input: fc.object() }), input => {
const valid = isValidInput(input);
const blocked = wouldBlock(input);
return valid !== blocked || (!valid && blocked); // blocked implies invalid
})
);
パス正規化の冪等性(新規):
// Property: normalize(normalize(path)) === normalize(path) (idempotent)
fc.assert(
fc.property(fc.string(), rawPath => {
const once = normalizePath(rawPath);
const twice = normalizePath(once);
return once === twice;
})
);
スキーマ検証の安定性(新規):
// Property: validate(schema, x) never throws uncaught exception for any input
fc.assert(
fc.property(fc.anything(), input => {
try {
validateSchema(schema, input);
return true;
} catch (e) {
return e instanceof ValidationError;
} // Only ValidationError allowed
})
);
コントラクトテスト
TaskUpdate のメタデータスキーマ(processedReflectionIds: string[])を検証します。
マルチエージェント TDD の分担(2026 年標準)
TDFlow(arXiv:2510.23761、SWE-Bench Verified で 94.3%)に基づきます。単一の TDD エージェントのスコアは 60~70% です。役割に特化したサブエージェントに分担させます。
| 役割 | エージェント | 担当内容 |
|---|---|---|
| テスト作成者 | qa | 失敗するテストを作成し、テストのみをコミット |
| 実装者 | developer | テストが成功するよう実装する — テストの変更は禁止 |
| 検証者 | reflection-agent | テストの不正操作を検出し、RED→GREEN の証拠を検証 |
パターン:
- QA エージェントがテストを作成し、テストファイルのみをコミットします(実装は含めません)
- Developer エージェントが実装し、テストを実行して、実装をコミットします
- Reflection エージェントが差分をレビューします。テストのアサーションが変更されていた場合は FAIL(テストの不正操作)
テストの不正操作の検出: reflection-agent は git diff HEAD~1 HEAD -- '*.test.*' を確認します。実装のコミット後にアサーションが変更されていた場合は、すべて REJECT します。
使用する場面: リポジトリ規模の TDD、複数の振る舞いを伴う複雑な機能、または単一のエージェントがテストの変更を正当化するおそれがあるあらゆるタスク。
TDAID フェーズの対応付け(テスト駆動の AI 支援開発、2025~2026 年)
TDAID は従来の TDD を拡張し、計画と検証のゲートを明示的に設けます。
| フェーズ | TDAID ラベル | Agent-Studio の担当者 | 説明 |
|---|---|---|---|
| 0 | Plan | planner | 思考モデルが、コードを書く前に、テストのチェックポイントを明示した構造化 TDD 計画を生成します |
| 1 | Red | qa | 望ましい動作を表す失敗するテストを作成します。人間が想定どおり失敗することを確認します |
| 2 | Green | developer | テストに合格するための最小限の実装を行います。テストのアサーションを変更してはいけません |
| 3 | Refactor | developer | すべてのテストが成功した状態で、コードの品質を改善します |
| 4 | Validate | reflection-agent + verification-before-completion スキル | 仕様の不正利用を検出し、実装が計画と一致することを確認します。人間がゲートを承認します |
Validate フェーズで検出する TDAID の主なアンチパターン:
- テストに合格させるためにテストのアサーションを削除する
- 期待値をハードコードする
- テスト対象の動作をモックで無効化する
- 仕様の意図を満たさず、表面的に実装を準拠させる
研究の根拠: TDAID(awesome-testing.com、2025)、TDAD のエージェント間バリアント(arXiv:2603.08806、2026)、TDFlow(arXiv:2510.23761、2025)
LSP による RED 前の型検証
失敗するテストを書く前に、API 契約が存在することを確認します。これにより、「動作が欠けているため失敗する」のではなく、「API が間違っているため失敗する」事態を防げます。
# Step 1: Find the target function's file + line
pnpm search:code "functionName"
# Step 2: Verify signature with LSP hover
lsp_hover({ filePath: "/abs/path/to/file.ts", line: 42, character: 10 })
# Returns: function signature, parameter types, return type
# Step 3: Write test using VERIFIED signature
# Now RED is guaranteed to fail due to missing behavior, not API mismatch
ルール: lsp_hover が空を返す場合(CJS ファイル、または LSP がアクティブでない場合)は、ripgrep の rg -n "functionName" --type ts にフォールバックして、実際のシグネチャを確認します。
不要な場合: まだ存在しない、ごく単純な新規関数(LSP から返される情報がない場合)。
契約テスト(フックの境界 — 拡張)
フックの契約では、stdin/stdout の JSON プロトコルを定義します。境界でテストします。
// Hook contract test pattern
const proc = spawn('node', ['.claude/hooks/routing/routing-guard.cjs'], { shell: false });
const input = JSON.stringify({
tool_name: 'Edit',
tool_input: { file_path: '.claude/agents/core/developer.md' },
});
proc.stdin.write(input);
proc.stdin.end();
// Assert: exit code 2 (block) for protected paths
// Assert: stdout JSON contains { allow: false, message: /Gate 4/ }
TaskUpdate メタデータ契約:
// Validate processedReflectionIds schema
const schema = {
type: 'object',
required: ['processedReflectionIds'],
properties: { processedReflectionIds: { type: 'array', items: { type: 'string' } } },
additionalProperties: false,
};
テスト対象の Agent-Studio フック契約:
routing-guard.cjs:task_id のない Task をブロックします(終了コード 2)unified-creator-guard.cjs:.claude/skills/**/SKILL.mdへの Write をブロックします(終了コード 2)spawn-token-guard.cjs:トークン数が 80K に達すると警告します(終了コード 0 + メッセージ)
テストランナーの選択(node --test と Vitest 4)
Agent Studio では、すべての .cjs CommonJS ファイル(フック、lib、スクリプト)で、組み込みの Node.js テストランナーである node --test をデフォルトとして使用します。ESM/TypeScript ファイルには Vitest 4 を推奨します。
| ランナー | 使用する場合 | コマンド |
|---|---|---|
node --test | .cjs フック、lib、CommonJS スクリプト — 現在の Agent Studio 標準 | node --test tests/**/*.test.cjs |
vitest | .ts、.mts、ESM の .js ファイル — TypeScript への移行時に使用 | pnpm vitest run |
.cjs で node --test を使う理由: Vitest には Vite の設定と ESM 互換モジュールが必要です。Agent Studio のフックは require() と CommonJS を使用するため、node --test ならトランスパイルなしで動作します。
.ts/ESM で Vitest 4 を使う理由: 起動時間が約 8 秒(Jest)から約 1.2 秒(Vitest)に短縮されます。TypeScript と ESM のファーストクラスサポート、Browser Mode(v4 で安定版)、および jest 互換の describe/it/expect API(移行時に変更が必要なのは設定のみ)を利用できます。
アンチパターン: 新しいファイルで Jest を使用しないでください。Vitest は ESM/TypeScript 向けの 2025〜2026 年の標準です。
# Current Agent Studio pattern (CJS hooks and lib)
node --test tests/lib/routing/routing-table.test.cjs
# Future ESM/TypeScript pattern
pnpm vitest run tests/lib/routing/routing-table.test.ts
AI 出力の評価テスト(非決定的なエージェント)
LLM/エージェントの出力は非決定的なため、合否を二値で判定するアサーションだけでは不十分です。代わりに、スコアベースの評価とツール呼び出しシーケンスの検証を使用します。
スコアベースのアサーションパターン
// Agent output evaluation — score dimensions 0.0-1.0
function evaluateAgentOutput(output, expectations) {
const scores = {
relevance: scoreRelevance(output, expectations.topic), // 0.0-1.0
safety: scoreSafety(output), // 0.0-1.0
faithfulness: scoreFaithfulness(output, expectations.facts), // 0.0-1.0
format: scoreFormat(output, expectations.schema), // 0.0-1.0
};
const overall = Object.values(scores).reduce((a, b) => a + b) / Object.keys(scores).length;
return { scores, overall, pass: overall >= 0.75 };
}
// Test: agent output meets quality threshold
test('researcher agent output is relevant and safe', () => {
const result = evaluateAgentOutput(agentOutput, { topic: 'TDD patterns', facts: knownFacts });
expect(result.scores.safety).toBeGreaterThanOrEqual(0.9); // Hard floor for safety
expect(result.overall).toBeGreaterThanOrEqual(0.75); // 75% overall threshold
});
ツール呼び出しシーケンスの検証
エージェントのテストでは、最終出力だけでなく、ツール呼び出しの順序と回数を検証します。
// Spy on tool calls and assert ordering
const toolCallLog = [];
const mockTaskUpdate = jest.fn(args => {
toolCallLog.push({ tool: 'TaskUpdate', args });
});
const mockBash = jest.fn(args => {
toolCallLog.push({ tool: 'Bash', args });
});
// Run agent under test with mocked tools
await runAgent({ TaskUpdate: mockTaskUpdate, Bash: mockBash });
// Assert: TaskUpdate(in_progress) called BEFORE TaskUpdate(completed)
const inProgressIdx = toolCallLog.findIndex(
c => c.tool === 'TaskUpdate' && c.args.status === 'in_progress'
);
const completedIdx = toolCallLog.findIndex(
c => c.tool === 'TaskUpdate' && c.args.status === 'completed'
);
expect(inProgressIdx).toBeLessThan(completedIdx); // Ordering enforced
expect(inProgressIdx).toBeGreaterThanOrEqual(0); // Must have been called
expect(completedIdx).toBeGreaterThanOrEqual(0); // Must have been called
ルール: LLM が生成した文章のテキスト内容をテストしてはいけません。構造、スキーマの妥当性、ツール呼び出しシーケンス、スコアのしきい値をテストします。
参考文献: Simon Willison(2025)—「エージェントの Red/Green TDD:ツール呼び出しシーケンスと構造化出力に対するアサーションを書く。」
MSW v2 による HTTP モック(API 境界テスト)
外部 HTTP 呼び出しを行うスキルやエージェントをテストするには、MSW(Mock Service Worker)v2 を使用します。MSW はネットワークレベルでインターセプトするため、fetch のモンキーパッチや本番コードの変更は不要です。
pnpm add -D msw@2
セットアップパターン(Node.js / Vitest)
import { setupServer } from 'msw/node';
import { http, HttpResponse } from 'msw';
// Define handlers — these describe the expected API contract
const handlers = [
http.get('https://api.example.com/search', ({ request }) => {
const url = new URL(request.url);
return HttpResponse.json({
results: [{ id: 1, title: `Result for: ${url.searchParams.get('q')}` }],
});
}),
];
const server = setupServer(...handlers);
beforeAll(() => server.listen({ onUnhandledRequest: 'error' }));
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
// Test: researcher skill makes HTTP call and processes response
test('researcher skill fetches and parses search results', async () => {
const results = await researcherSkill.search('TDD patterns 2026');
expect(results).toHaveLength(1);
expect(results[0].title).toContain('TDD patterns');
});
エラーケース向けのテストごとのオーバーライド
test('researcher skill handles 503 gracefully', async () => {
server.use(
http.get('https://api.example.com/search', () => HttpResponse.json({}, { status: 503 }))
);
const results = await researcherSkill.search('TDD patterns');
expect(results).toEqual([]); // Graceful empty fallback
});
手動モックと比べた主なメリット:
- 実際の HTTP クライアントのコードパスをテストできます(モックされた抽象化ではありません)
onUnhandledRequest: 'error'により、テスト中の意図しない外部呼び出しを検出できます- ハンドラーでリクエスト/レスポンスの契約を定義でき、ドキュメントとしても機能します
MSW による境界テストの対象となる Agent-Studio:
researcherスキル → WebSearch/WebFetch の HTTP 呼び出しgithub-opsスキル → GitHub API 呼び出しmcp__Exa__web_search_exaまたはWebFetchを使用するすべてのエージェント
ミューテーションテスト(Stryker JS)
ミューテーションテストでは、カバレッジだけでなくテストの品質も検証します。行カバレッジ 100% を達成した後に実行します。
Stryker + Vitest(2026年標準 — ESM/TypeScript プロジェクト)
# Install (once per project) — use vitest-runner for ESM/TypeScript
pnpm add -D @stryker-mutator/core @stryker-mutator/vitest-runner vitest
// stryker.config.mjs — working configuration for Vitest projects
/** @type {import('@stryker-mutator/api/core').PartialStrykerOptions} */
export default {
testRunner: 'vitest',
vitest: {
configFile: 'vitest.config.ts', // optional: path to your vitest config
related: true, // default: run only tests related to mutated file
},
thresholds: { high: 80, low: 60, break: 50 },
reporters: ['html', 'progress'],
};
# Run mutation tests (use incremental to speed up local loops)
pnpm stryker run --incremental
# Target threshold: >80% mutation score
# Score = (killed mutations / total mutations) × 100
Vitest ランナーの制限事項(StrykerJS 7.x):
- Browser Mode はサポートされていません — threads モードのみ
- 常に
perTestカバレッジ分析を使用します(coverageAnalysis設定は無視されます) node --testを使用する.cjsファイルでは、フォールバックとして@stryker-mutator/jest-runnerを使用してください
Stryker + node:test(CommonJS/.cjs プロジェクト)
pnpm add -D @stryker-mutator/core @stryker-mutator/jest-runner
結果の解釈:
- Killed — テストスイートがミューテーションを検出しました ✓
- Survived — テストスイートがこのコードパスを見逃しました(アサーションを追加してください)
- No coverage — この行を実行するテストがありません(テストを追加してください)
実行するタイミング: セキュリティ上重要なコード(フック、バリデーター、ルーティングロジック)の TDD サイクルを完了した後です。すべてのコードで実行する必要はありません — リスクに応じて優先順位を付けてください。
ミューテーションテストの Agent-Studio 優先対象:
.claude/hooks/routing/routing-guard.cjs.claude/hooks/safety/unified-creator-guard.cjs.claude/lib/routing/routing-table.cjs
検証フェーズ:TDAD 依存関係マップ(P0)
コミットする前に、エージェントは変更したソースファイルをどのテストファイルがカバーしているかを特定する必要があります。コンパイラー支援による参照の検出、または対象を絞った grep を使用してください。
# Find tests that import the changed file
grep -r "import.*changedFile\|require.*changedFile" tests/
# Or use LSP to find all references
lsp_findReferences({ filePath: "/path/to/changed/file.ts", line: 1, character: 1 })
依存関係マップを作成し、最初にそのテストのみを実行してください(arXiv:2603.17973 によると、回帰検出が70%高速化します)。
# 1. Run targeted tests (impacted tests only)
pnpm test tests/hooks/routing-guard.test.cjs
# 2. Verify no regressions in targeted scope
# 3. Only then run full suite
pnpm test
理由: 大規模なリポジトリでは、テストスイート全体の実行に5分以上かかることがあります。対象を絞ったテストなら15〜30秒で回帰を検出でき、長い TDD ループで次のシナリオに取り組むためのコンテキストを確保できます。
検証フェーズ:仕様の抜け道を利用したテスト対策(P0)
検証フェーズでは、実装がテストのアサーションの抜け道を利用していないことを確認してください。
チェックリスト:
- テストが実装の詳細ではなく動作を検証している(プライベート変数やクラスの内部をテストしていない)
- テストから実装にコピーされたハードコードの期待値がない
- ミューテーションスコアが80%以上であり、回帰を検出するためのテスト品質が十分である
- レビュー:コードが根本的に間違っていてもテストに合格する可能性はないか?
利用可能な場合はミューテーションテストを実行してください:
# Test suite strength validation — mutations should be caught
pnpm stryker run
# If mutation score < 80%, tests are too weak:
# - Add negative tests
# - Add boundary condition tests
# - Verify assertions are on behavior, not mocks
検出すべき仕様の抜け道を利用した例:
- ✗ テストで
42が期待されているため、実装でreturn 42をハードコードしている → ミューテーションテストで検出できます - ✗ テストが動作をアサートせずにモックしている → ミューテーションテストでミューテーション検出率が0%と判明します
- ✗ テストが動作ではなくログメッセージを確認している → アサーションを反転しても、実装は合格します
Agent-Studio の対象: セキュリティ上重要なフックやルーティングの変更を完了した後、タスクを完了とする前にミューテーションテストを実行してください。