condition-based-waiting について
condition-based-waiting は、テストのタイミングに起因する不安定さを減らすためのスキルです。任意の遅延ではなく条件成立を待つ方法を示し、ポーリング、タイムアウト、固定待機が妥当な場合を解説します。
主な機能
- 01状態やイベントの条件成立を待機
- 02一般的な条件ポーリングのパターン
- 03ポーリングのタイムアウトと明確なエラー設定
- 04固定遅延が適切な状況の判断
活用シーン
- 01テスト内の任意の sleep を置き換え
- 02非同期処理やファイル作成を待機
- 03並列実行時に不安定なテストを調査
- 04既知の間隔を持つ時間動作の検証
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ jackspace/ claudeskillz --skill condition-based-waitingインストール後、Claude Code で/condition-based-waitingを入力して読み込まれたか確認します。
同梱ファイルexample.tsSKILL.json
詳細
条件ベースの待機
概要
不安定なテストでは、根拠のない待機時間を設定してタイミングを推測しがちです。これにより、処理の速いマシンではテストが成功する一方、負荷がかかっている環境や CI では失敗するという競合状態が発生します。
基本原則: かかる時間を推測するのではなく、実際に必要な条件が満たされるまで待ちます。
使用する場面
digraph when_to_use {
"Test uses setTimeout/sleep?" [shape=diamond];
"Testing timing behavior?" [shape=diamond];
"Document WHY timeout needed" [shape=box];
"Use condition-based waiting" [shape=box];
"Test uses setTimeout/sleep?" -> "Testing timing behavior?" [label="yes"];
"Testing timing behavior?" -> "Document WHY timeout needed" [label="yes"];
"Testing timing behavior?" -> "Use condition-based waiting" [label="no"];
}
使用する場面:
- テストに根拠のない待機時間(
setTimeout、sleep、time.sleep())がある - テストが不安定である(成功するときもあれば、負荷がかかると失敗するときもある)
- 並列実行時にテストがタイムアウトする
- 非同期処理の完了を待つ
使用しない場面:
- 実際のタイミング動作(デバウンス、スロットル間隔)をテストする
- 根拠のないタイムアウトを使用する場合は、必ず理由を記載する
基本パターン
// ❌ BEFORE: Guessing at timing
await new Promise(r => setTimeout(r, 50));
const result = getResult();
expect(result).toBeDefined();
// ✅ AFTER: Waiting for condition
await waitFor(() => getResult() !== undefined);
const result = getResult();
expect(result).toBeDefined();
クイックパターン
| シナリオ | パターン |
|---|---|
| イベントを待つ | waitFor(() => events.find(e => e.type === 'DONE')) |
| 状態を待つ | waitFor(() => machine.state === 'ready') |
| 件数を待つ | waitFor(() => items.length >= 5) |
| ファイルを待つ | waitFor(() => fs.existsSync(path)) |
| 複雑な条件 | waitFor(() => obj.ready && obj.value > 10) |
実装
汎用的なポーリング関数:
async function waitFor<T>(
condition: () => T | undefined | null | false,
description: string,
timeoutMs = 5000
): Promise<T> {
const startTime = Date.now();
while (true) {
const result = condition();
if (result) return result;
if (Date.now() - startTime > timeoutMs) {
throw new Error(`Timeout waiting for ${description} after ${timeoutMs}ms`);
}
await new Promise(r => setTimeout(r, 10)); // Poll every 10ms
}
}
実際のデバッグセッションで使用した、ドメイン固有のヘルパー(waitForEvent、waitForEventCount、waitForEventMatch)を含む完全な実装については、@example.ts を参照してください。
よくある間違い
❌ ポーリング間隔が短すぎる: setTimeout(check, 1) - CPU を浪費します
✅ 修正: 10ms 間隔でポーリングします
❌ タイムアウトがない: 条件が満たされないと無限ループになります ✅ 修正: 明確なエラーを含むタイムアウトを必ず設定します
❌ 古いデータ: ループの前に状態をキャッシュします ✅ 修正: ループ内でゲッターを呼び出して最新のデータを取得します
根拠のないタイムアウトが正しい場合
// Tool ticks every 100ms - need 2 ticks to verify partial output
await waitForEvent(manager, 'TOOL_STARTED'); // First: wait for condition
await new Promise(r => setTimeout(r, 200)); // Then: wait for timed behavior
// 200ms = 2 ticks at 100ms intervals - documented and justified
要件:
- まず、トリガーとなる条件が満たされるまで待つ
- 推測ではなく、既知のタイミングに基づいている
- 理由を説明するコメントを記載する
実際の効果
デバッグセッションより(2025-10-03):
- 3 ファイルにわたる不安定なテスト 15 件を修正
- 成功率:60% → 100%
- 実行時間:40% 短縮
- 競合状態が発生しなくなった
コメント
Markdown 対応