GitHub Copilot が生成したコードのバグは、なぜ原因を特定しにくいのか
インライン補完は、ファイルを書き換えるエージェントとはリスクの性質が異なります。提案の一つひとつはレビューできそうな大きさに見えるため、1秒で目を通して次に進みます。その1秒の注意は、目の前の行に対しては誠実ですが、その周囲にあるシステムに対しては盲目です。
その結果、何かが壊れたときに原因を二分探索で絞り込む作業は悲惨なものになります。疑わしいコミットが1つあるわけではなく、受け入れた時点ではどれも正しく見えた補完のロングテールが残っているだけだからです。
注意すべき3つのドリフト
規約のドリフト
補完は、学習した内容や近くにあるコードのパターンに従いますが、それが必ずしもコードベースの実際の規約とは限りません。
エラー処理、null チェック、ロギングが、モジュールごとに少しずつ食い違っていきます。
ロジックの重複
既存のヘルパーを探すよりも、生成されたヘルパーを受け入れるほうが速いため、同じルールが3か所に実装されることになります。
そして後になって、そのうち1か所だけが修正され、残りの2か所は直されません。
もっともらしいデフォルト値
提案されたデフォルト値やタイムアウト、並び順は妥当に見えますが、製品のルールとは一致していません。
どこも失敗しません。ただ、誰も意図していない振る舞いになっているだけです。
この3つはいずれもレビューでは見えず、製品を動かしたときに見えます。そのことが、チェックを置くべき場所を決めます。
補完ごとではなく、一定の間隔で振る舞いを確認する
受け入れたすべての提案を検証することは、不可能であり、有益でもありません。適切な単位はプルリクエストです。その時点ではまとまった量の変更が存在しており、それでいて、問題があったときに筋道を追える程度の小ささに収まっています。
確認したいのは新しいコードそのものではなく、そのコードが組み込まれている振る舞いです。請求モジュールに触れたプルリクエストであれば、請求処理を端から端まで動かすべきです。作成者が考えていなかった経路も含めてです。もっともらしいデフォルト値が潜んでいるのは、まさにそうした経路だからです。
検証スキルをインストールしておけば、人が思い出すのを待つのではなく、エージェント自身がこれを実行できます。
ターミナル
npm install -g @testsprite/testsprite-cli
testsprite setup
ローカルへのインストールを避けたい場合は、ダッシュボードでも同じ範囲をカバーできます。コマンドの全体像については、次を参照してください: CLI リポジトリ。
誰もが問うのが遅すぎるリグレッションの問い
補完を多用したコードベースについて有益な問いは、「このコードは良いか」ではありません。「製品は先月と同じことを今も行っているか」です。この2つは別の問いであり、ドリフトを捉えられるのは後者だけです。
その問いに答えるには、変更よりも前から存在するチェックの集合が必要ですが、チームが後回しにするのはまさにこの部分です。最初の1週間で書き出した10本のフローは、最初の障害が起きてから書いた100本よりも価値があります。どのフローが重要になるかを誰も知らないうちに定義されたのは、前者の10本だけだからです。
スケールするレビューの習慣
すべての補完をレビューすることは不可能ですが、まったくレビューしなければドリフトは蓄積していきます。うまくいく習慣は、行単位ではなくカテゴリ単位でレビューすることです。
補完がエラー処理の経路を追加したときは、それがモジュールの他の部分でのエラーの扱い方と一致しているかを確認してください。ここでの食い違いは最もよくあるドリフトであり、後からほどくのが最も面倒だからです。デフォルト値を追加したときは、そのデフォルトがどこから来たのかを問うてください。もっともらしいデフォルト値は、振る舞いを変える最も静かな手段だからです。ヘルパーを追加したときは、10秒かけて既存のヘルパーを探してください。ロジックの重複こそが、後の修正を不完全なものにするからです。
この3つの確認はそれぞれ数秒で済み、蓄積するものの大半を捉えます。それ以外は、コードを読むよりも製品を実際に動かすほうが確実に捉えられます。
自動的に動かす
ドリフトは少しずつ進むため、チェックは退屈なほど規則的でなければなりません。誰かが覚えていることに依存する仕組みは、最も多くの補完が受け入れられる忙しい週に限って飛ばされます。
パイプラインが別のチームの管轄であれば、最も抵抗の少ない方法は GitHub App です。これは webhook であり、リポジトリには何の変更も加えず、ビルドが新しいバージョンの公開を通知した時点で動きます。そうではなく、チェックをリポジトリ内で見えるようにしたい場合は、 GitHub Actions のステップで実現できます。
すべての補完を読まずにドリフトを捉える
受け入れたすべての提案をレビューすることは不可能なので、TestSprite は代わりに、そのコードが組み込まれている振る舞いを確認します。カバレッジは製品そのものから生成され、プルリクエストごとに実行され、補完を多用したコードベースにとって重要な問いに答えます。すなわち、これは先月と同じことを今も行っているか、という問いです。
これにより、3つのドリフトを直接捉えられます。並び順を変えてしまったもっともらしいデフォルト値は、フローの振る舞いの違いとして現れます。ロジックの重複は、一方のコピーだけが修正され、もう一方が修正されないときに現れます。エラー処理における規約のドリフトは、黙って失敗するようになった経路として現れます。
チェックは変更のある場所、つまりプルリクエストへのコメントやコミットチェックとして実行されます。そのためリグレッションが指し示すのは、補完全体の4分の1ではなく、ひとまとまりの補完になります。
Copilot が生成したコードは、手で書いたコードより劣るのでしょうか。
多くの場合、1行あたりで見れば劣りません。リスクは量にあります。レビュープロセスが想定していた以上の量のコードが1時間あたりにリポジトリへ入るため、欠陥の発生率が同じでも、すり抜ける数は増えます。
レビューを厳しくすれば解決しますか。
部分的には解決しますが、そもそも人が補完を使う理由と衝突します。チェックの軸を「読むこと」から「振る舞いの検証」へ移せば、速度を保ったままセーフティネットを取り戻せます。
Copilot が書いたテストはどうでしょうか。
カバレッジの面では有用ですが、独立したチェックとしては弱いものです。コードと同じ前提から生成されたテストは、その成り立ちからしてコードと一致します。
最初はいくつのフローをカバーすべきですか。
顧客にデモするであろう少数のフローから始め、金銭や権限に関わる経路を加えてください。カバレッジの目標からではなく、実際に起きた障害を起点に広げていきます。
リポジトリへのアクセスは必要ですか。
検証は、デプロイ済みのアプリケーションに対して、そのインターフェース経由で実行されます。リポジトリへのアクセスは、結果をプルリクエストやコミットに書き戻すためだけに使われます。