なぜ Windsurf のコードバグはレビューをすり抜けるのか
人間によるレビューは、その人が頭の中に保持できる行数の分しか機能しません。範囲が広く機械的な変更は、特定のかたちでそれを打ち負かします。どのハンクも単体ではもっともらしく見えるため、注意力が滑っていくのです。12 個目のファイルは 1 個目より甘い目で承認され、そして欠陥は、ほとんどの場合その末尾に潜んでいます。
しかも欠陥は、変更された箇所にあることはまれです。変更されなかった箇所にあります。1 か所だけ呼び出し元を取りこぼしたリネーム、3 つのコンストラクタが渡していない新しい必須フィールド、エラーパス以外のすべてで更新されたハンドラ。
範囲の広い変更が壊れる 3 つのかたち
不完全な一括変更
11 か所の呼び出し元は更新されたのに 1 か所だけ漏れ、しかもそれは、誰も毎日は通らないパス上にあります。
漏れた箇所はもともと型付けが緩いことが多く、型では検出できません。
前提のずれ
タイムスタンプが秒なのかミリ秒なのかといった具合に、ある値の意味について 2 つのファイルの認識が食い違います。
どちらのファイルも内部では一貫しているため、単体で見るかぎり誤りには見えません。
静かな挙動の変化
リファクタリングがインターフェースは保ったまま、順序やデフォルト値、エラーパスを静かに変えてしまいます。
旧実装に対して書かれたテストは、インターフェースを検証しているため、そのまま通ってしまいます。
3 つに共通するのは、コードを読むことではなく、アプリケーションを使うことでしか見つからないという性質です。だからこそ、これらは高くつきます。
差分ではなく挙動を検証する
範囲の広い変更で意味を持つ問いは、「個々の編集は正しいか」ではなく「プロダクトはこれまでどおり動いたうえで、新しいこともできるか」です。これは挙動についての問いであり、デプロイ済みのアプリケーションを、その変更が影響しうるフローに沿って実際に動かすことで答えが出ます。
役に立つために、カバレッジが網羅的である必要はありません。チェックアウト経路全体でフィールドをリネームする変更なら、チェックアウトを端から端まで一度通すだけでよく、それは 12 個の差分を丁寧に読むよりも価値があります。
検証用のスキルをエディタに導入すれば、エージェントはあなたを待たずに、自分でそのチェックを実行できます。
ターミナル
npm install -g @testsprite/testsprite-cli
testsprite setup
何もインストールしたくない場合は、ダッシュボードからでも同じ設定を行えます。CLI のそれ以外の機能をまとめているのが CLI リポジトリです。
要約ではなく、失敗そのものをエージェントに渡す
実際に何かが壊れたとき、修正のループが閉じるのは、証拠が使えるものである場合だけです。何を試み、アプリケーションが何をして、どこで食い違ったのか。それを渡されたエージェントは、狙いを定めて同じ 12 個のファイルに戻れます。スクリーンショットを渡されたエージェントは推測に走り、結局あなたが状況を説明し直すことになります。
広い差分を、読んだふりをせずに読む
12 ファイルの変更をレビューする誠実なやり方は存在しますが、それはすべてのハンクを読むことではありません。差分を開く前に、この変更が正しいと言えるためには何が成り立っていなければならないかを決め、その項目だけを確認することです。
リネームであれば、すべての呼び出し元が移動したか(これはコンパイラがおおむね答えてくれます)、そして名前を動的に組み立てている箇所はないか(こちらは答えてくれません)です。新しい必須フィールドであれば、そのオブジェクトを組み立てるすべての経路であり、不完全なオブジェクトを黙って渡し続けるテストやフィクスチャも含みます。インターフェースを保つリファクタリングであれば、順序とエラーパスです。リファクタリングが静かに変えてしまうのは、まさにそこだからです。
変更ごとに 3 つの問いを立てるほうが、400 行を眺めるよりも効果的です。そして手元には、実際に動かして確かめるしかない項目の、正直なリストが残ります。
重要度ではなく、影響範囲でフローを選ぶ
人はつい、最も重要な機能をカバーしようとします。範囲の広い変更では、その変更が影響しうるフローはどれかを問い、壊れたときにどれだけ静かに壊れるかで順位を付けるほうが有効です。
決済フローは重要ですが、同時に常に見張られてもいます。壊れれば 1 時間以内に知らせが届きます。危険なのは、重要でありながら、1 時間ごとではなく 1 週間ごとにしか使われないフローです。エクスポート、招待、設定ページ、定期配信のダイジェストといったものです。不完全な一括変更が 1 か月のあいだ誰にも気づかれずに居座るのは、こうした場所です。
ですから、範囲の広い変更のあとは、静かなパスから先に動かしてください。カバーするコストは低く、しかもほかの誰も確認していない場所です。
プルリクエストごとに実行する
範囲の広い変更は、差分が退屈だからこそ、さっとマージしたくなる類のものです。だからこそ、この検証は覚えておいて実行するものではなく、自動で走るものにすべきだと言えます。
方法は 2 つあり、選択はつまるところ、パイプラインを誰が所有しているかの問題です。ダッシュボードから GitHub App を接続する方法なら、リポジトリに手を加える必要はまったくありません。ビルドがすでに出力しているデプロイに反応する仕組みだからです。一方、 GitHub Actions のステップを追加する方法では、チェックがリポジトリの中に置かれ、ビルドのほかの部分と同じようにレビューの対象になります。
広い変更を、読まずに検証する
TestSprite は、広い差分では答えられない問いに答えます。プロダクトはこれまでどおり動いたうえで、新しいこともできるのか、という問いです。デプロイ済みのアプリケーションを開き、その変更が影響しうるフローを実際にたどり、構造ではなく挙動を報告します。
これは、この問題に対して正しいかたちです。不完全な一括変更、ずれていく前提、静かな挙動の変化は、いずれもソースコード上では見えず、使えば明らかになります。カバレッジは人が書くのではなくプロダクトから生成されるため、エクスポートや招待、設定ページといった静かなフローも、誰かが思い出さなくても対象に含まれます。
実際に何かが壊れたときは、エージェントがそのまま動ける 1 つのまとまりとして失敗が返ってきます。この場面ではそれが何より重要です。具体的な食い違いを指し示すほうが、12 個のファイルを見直すよう頼むよりも効きます。
型はなぜこれらを捕まえられないのですか?
不完全な一括変更については相当な割合を捕まえますが、前提のずれと静かな挙動の変化はほとんど捕まえられません。どちらも、その成り立ちからして型としては正しいままだからです。
もっと小さな変更を依頼すればよいのでは?
効果はありますが、エージェント型エディタを使う理由の多くを手放すことにもなります。検証のステップを挟めば、変更の広さを保ったまま、結果が動くかどうかを知ることができます。
バックエンドだけの変更でも機能しますか?
はい。インターフェースではなく、API レベルのカバレッジを通じて機能します。原則は同じで、変更を読むのではなく、挙動を実際に動かして確かめます。
この種のチェックはどれくらい時間がかかりますか?
キーを打つたびに走らせるには長く、プルリクエストに組み込むには十分に短い、という程度です。そこがちょうどよい置き場所です。
エージェントがすでにユニットテストを実行している場合はどうですか?
有用ですが、ユニットテストはたいてい、その変更がいま書き換えたばかりの前提に沿って書かれています。確認できるのは外部から見た正しさではなく、内部の一貫性です。