dsh-ci-test-reliability について
CI の並行実行やプラットフォーム差異に耐える DSH テストを設計するスキルです。リソース分離、ライフサイクルの後始末、状態同期、根拠に基づく回帰検証を扱います。
主な機能
- 01重複する CI 実行トポロジーの把握
- 02ポートとパスのアトミックな割り当て
- 03グローバル状態の隔離と復元
- 04観測可能な状態に基づく同期
- 05リソース終了の完了待ち
- 06不安定さを隠さない回帰検証
活用シーン
- 01並列テストプロセス間の競合防止
- 02子プロセスやサーバーの終了処理の検証
- 03プラットフォーム固有の CI テスト障害の診断
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ deepseek-ai/ deepseek-harness --skill dsh-ci-test-reliabilityインストール後、Claude Code で/dsh-ci-test-reliabilityを入力して読み込まれたか確認します。
同梱ファイルreferences/ci-flake-diagnosis.md
詳細
信頼性の高い DSH CI テスト
静かなワークステーションで単独実行した場合だけでなく、リポジトリの実際の CI トポロジーでも正しく動作するテストを構築します。このスキルは分離と信頼性に関する判断を担いますが、リポジトリのテスト階層ポリシーを置き換えたり、プッシュ時に実行するすべてのコマンドを選択したりするものではありません。
適用されるルールを確認する
- 単体テスト、カバレッジ、期待される出力、スナップショット、ブラウザー、または実際の API によるエビデンスの選択には、テストポリシーを使用します。
- ライフサイクル、サブプロセス、キャンセル、ティアダウンの動作には、防御的なパターンを使用します。
- ワーカーまたはジョブのトポロジーがテストに影響する場合は、有効な Vitest 設定と GitHub ワークフローを確認します。
- 記録済みセッションのシナリオでは、スナップショットの手順にも従います。
- テスト設計が固まったら、送信する検証項目の選択に dsh-pre-push-checks を使用します。
実行トポロジーをモデル化する
有効な設定でそうでないことが確認されない限り、次の各層は重複して実行される可能性があると考えます。
- 1 つの Vitest ファイル内のテスト。
- 別々の Vitest ファイルまたはワーカープロセス。
- 1 つのジョブ内の独立した Vitest またはリポジトリゲートのプロセス。
- ランナーが同じホストを共有する、異なる Actions ジョブ。
プロセスの分離だけでは、ホストのポート、予測可能なファイルシステムパス、外部サービス、データベース、ソケット、継承された子プロセスは分離されません。取得するリソースごとに、その所有者、アトミックな割り当て方法、観測可能な準備完了のシグナル、登録済みのクリーンアップ、静止状態に達したことを示す完了シグナルを特定します。
1 つのフィクスチャに分離の問題があるという理由だけで、スイート全体を直列化してはいけません。まず、排他的な範囲を狭めるか、リソースの割り当て方法を変更します。Vitest の逐次実行ブロックでは、別のファイル、プロセス、ジョブ、またはランナーからホストリソースを保護できません。
リソースをアトミックに割り当てる
利用可能か確認してから後で取得するのではなく、リソースの所有者が提供するアロケーターを使用します。
- ネットワークフィクスチャでは
listen(0)でループバックにバインドし、サーバーがリスニング中と報告した後にのみ割り当てられたアドレスを読み取ります。空きポートを走査して、後からバインドしてはいけません。 mkdtempでテストごとに専用の一時ルートを作成します。予測可能な共有パスを取得してはいけません。- 共有データベース、ソケット、セッション、出力先には、テストごとに一意の名前空間を割り当てます。
- 既存であってはならないパスには、排他的な作成方法を使用します。
- 安定した記録済み識別子と、一時的なトランスポートアドレスは分けて扱います。ライブリソースに記録済みの値を無理に使用させるのではなく、フィクスチャ内で変換します。
パーサーの入力または期待値としてのみ使用するリテラルパスや URL は、取得するリソースではありません。固定値に見えるという理由だけで書き換えてはいけません。
プロセス全体の状態を隔離する
process.env、cwd、フェイクタイマー、ロケールとタイムゾーン、モックされたモジュール、レジストリ、コンソールフック、globalThis、グローバルな fetch のインターセプトは、排他的な可変リソースとして扱います。
依存関係の注入またはインスタンスローカルのアダプターを優先します。変更が必要な場合は、次のようにします。
- 元の値が未設定だったか、設定されていたかを記録します。
- その状態を正確に復元します。
- 復元処理をただちに登録します。
- 変更範囲を最小限にするため、
try/finallyを使用します。 - ローカルの
finallyより前に失敗する可能性がある場合は、afterEachにフォールバックを用意します。 - フィクスチャが所有する、最も範囲の狭い正確なリクエストまたは呼び出しだけをインターセプトします。
プラットフォームが所有するセマンティクスを尊重する
CI は Windows と POSIX ホストで同じテストスイートを実行します。オペレーティングシステムが所有する値は、テストで書き込んだとおりに戻るとは限りません。
- 値を書き戻す場合は、書き戻しに失敗してもアサーションが許容する場合に限り安全です。フィンガープリントが無効化されることを証明するためにファイルの
mtimeを復元する方法は、どの環境でも成立します。一方、レコードが有効なままであることを証明するために復元する方法は、損失のない往復を前提としますが、NTFS の 100 ナノ秒刻みではミリ秒未満の精度が保たれません。アサーションが復元に依存する場合は、記憶していた値ではなく、新たに読み取った値を期待値として使用してください。 - Windows では環境変数名の大文字と小文字が区別されないため、
http_proxyとHTTP_PROXYを別々のキーとして設定するフィクスチャは、1 つのエントリーになります。 - Windows ではファイルハンドルが非同期に解放されるため、POSIX ホストですぐに完了する名前変更や削除には、観測された競合状況に応じた上限付きの再試行が必要です。
- Windows には POSIX の権限やシグナルのセマンティクスがありません。それらに依存するケースでは、どこでもアサーションを弱めるのではなく、理由を明記した明示的なプラットフォームスキップを指定してください。
すべてのプラットフォームで成立する観測方法を優先してください。ケースが実際に特定のプラットフォームでは実行できない場合は、そのプラットフォームで明示的に除外してください。
タイムアウトを実行環境の予算に合わせる
describe またはケースのタイムアウトは、ランナーの --testTimeout に従うのではなく、それを上書きします。そのため、実行環境の予算より小さい値を設定すると、CI がすでに割り当てた時間が短くなります。また、デフォルト値がより小さいホストでは、同じ数値がタイムアウトの延長になります。プロセス作成に制約されるスイートには、実行環境の予算を設定してください。より短い値を使う場合は、その理由を添えてください。
テストの予算とともにフックの予算も増やしてください。セットアップとティアダウンにも同じ競合が発生するため、ケースの予算だけを増やすと、競合による失敗が afterEach に移るだけです。
タイムアウト自体がテスト対象の場合、外側の待機時間はテスト対象のタイムアウトより大幅に長くしてください。20 ms の期限が発生することを証明するケースで、ハーネス自身の待機時間と競合させてはいけません。そうしないと、どちらの期限が先に報告されるかが負荷によって決まります。
状態に基づいて同期する
一定時間のスリープは、セットアップが完了したことやクリーンアップが落ち着いたことの証拠にはなりません。
- 明示的な準備完了イベント、ハンドシェイク、状態遷移、所有するプロミス、または外部から観測可能な条件を待ってください。
- 遅延プロミスやバリアを使って競合を決定論的な時点で発生させ、関連する操作が重なり合うことを証明してください。
- タイムアウトは待機を制限するためにのみ使用し、アサーションを正しくする条件としては使用しないでください。
- テスト対象の動作でない限り、スケジューラーに依存する順序をアサーションしないでください。
- 時間自体がテスト対象の場合は、クロックを注入するか偽装し、必ず実際のタイマーを復元してください。
リソースが静止するまで破棄する
リソースを取得したらすぐにクリーンアップを登録し、アサーションが失敗した場合もリソースを解放できるようにしてください。クリーンアップでは、新しいコールバックやリクエストを停止し、リスナーを切り離し、グローバルフックを復元し、所有する処理を終了して、子プロセスの終了、サーバーのクローズ、ワーカーの終了、または同等の完了シグナルを待ちます。
所有する完了シグナルを待たずに abort()、close()、または kill() を呼び出すだけでは、ティアダウンとして不完全です。遅れて完了する可能性がある場合は、破棄によって別のテストへの変更が防止されることを証明してください。
想定したリグレッションを実証する
- 実用上可能であれば、修正前に通常のリグレッションテストが失敗することを確認します。
- 新たな静的ガードやコーパスガードの場合は、拒否されるケースを一時的に導入し、想定どおり失敗することを確認します。
- 競合状態の場合は、バリアを使って処理が重なることを実証します。繰り返し実行するだけでは競合状態のテストになりません。
- ポート、ソケット、共有パス、サブプロセス、その他のホストリソースについて、修正にプロセス間の分離が含まれる場合は、独立したテストプロセスを並行して実行します。
- フィクスチャが独自の期限を設けて子プロセスを起動する場合は、終了ステータスを確認する前に、シグナルやタイムアウトによって子プロセスが終了していないことを確認します。これにより、強制終了された子プロセスをステータス不一致ではなくタイムアウトとして報告できます。
- コンポーネントの自己申告を信用するのではなく、外部の状態、イベント、ファイル、ログ、終了結果、または破棄を検証します。
ストレス実行は決定的なリグレッションテストを補完するものであり、置き換えるものではありません。
Flaky テストを覆い隠す修正を避ける
決定的なローカルテストの根本原因を修正する手段として、次のようなものを提示しないでください。
- 待機している状態を特定せずにタイムアウトを延ばすこと。
- 再試行を追加すること。
- すべてのファイルを直列実行にすること。
- エラーや未処理の拒否を握りつぶすこと。
- アサーションを弱めること。
- 不安定な挙動を正規化して取り除くこと。
- クリーンアップやアサーションの前にスリープを追加すること。
実際の API に関するポリシーのもとで、文書化された一時的な外部プロバイダーのテストに再試行を使用することは引き続き有効です。この例外は外部との境界に限定してください。
予算を元に戻すことは、問題を覆い隠すことではありません。スイートを、すでに設定されていたレーンの予算まで引き上げることや、ランナーで実測した競合に合わせて上限付きの再試行を設定することは、待機対象を明確にし、レーンに割り当てられた余裕を返すものです。どちらも、調査されていない待機に余裕を勝手に追加するものではありません。
既存の Flaky テストを診断する
既存の確率的な CI 失敗については、CI の Flaky テスト診断ワークフローを確認してください。診断のみを求める依頼では、読み取り専用の対応を行います。ユーザーが修正も求めていない限り、原因と根拠を報告してください。
検証して報告する
影響を受ける動作に対して、最小限の対象を絞ったリグレッションテストを実行します。トポロジー固有のリスクを変更が伴う場合に限り、次の根拠を追加します。
- グローバルな変更には、状態を復元した根拠が必要です。
- ライフサイクルやサブプロセスの処理には、停止完了を確認した根拠が必要です。
- ポート、ソケット、共有パスには、独立したプロセスを並行実行した根拠が必要です。
- 新たなガードには、負の対照が必要です。
プッシュする前に、dsh-pre-push-checksを使用してください。実行したコマンドと確認できた結果を正確に報告し、再試行したテスト、スキップしたテスト、保留中の CI を成功したと説明しないでください。