この記事は Zenn からの転載です。元記事: https://zenn.dev/berrylove/articles/test-broke-production-data-three-times
先日公開した本『AI 国家運営』の第9章では、テストコードが本番相当のデータを3回壊した事故を扱いました。本では対処(隔離・出口の防御・回帰確認)に重心を置いたので、この記事では逆に、原因にたどり着くまでの調査プロセスだけを切り出します。最初の仮説は外れていて、正しい原因にたどり着けたのは「疑わしきは自分を疑う」という地味な裏取りのおかげでした。
起きたこと: 何度直しても壊れる
個人開発のツール群に、個々のデータを1件ずつ処理した結果を、全量まとめて1つのインデックスファイルへ書き出す関数がありました。本来なら実データ201件分の内容が反映されるはずのファイルが、ある日、テスト用に用意した合成データ1件分だけに置き換わっているのが見つかりました。
ツールを再実行して201件分を復元し、いったんは片付いたように見えました。ところが同じ壊れ方が短期間に3回連続で発生しました。1回目は操作ミスの可能性も考えましたが、同じ壊れ方が3回続いたことで「再現可能な経路がどこかにある」と疑いを強めました。
最初の仮説: 「別のエージェントが競合したのでは」
このリポジトリでは複数のエージェントが並行して作業していました。マルチエージェント運用では、複数の作業主体が同じファイルに触れる場面は実際にあります。そのため最初に立てた仮説は「別のエージェントが同時に書き込んで競合したのではないか」というものでした。
この仮説は、もっともらしく聞こえます。実際に起こりうる話ですし、「自分は悪くない」という結論のほうが調査する側としては楽です。しかし、もっともらしさと正しさは別物です。
仮説を裏取りする — 決め手は「中身の一致」
競合を疑うなら、競合の跡を探すのが筋です。ログやタイムスタンプを見ても、他のエージェントが該当ファイルに触れた形跡は見つかりませんでした。ただ、跡が見つからないことは競合の不在の証明にはなりません。ここで即断せず、もう一段確実な裏取りをしました。
壊れた後のインデックスファイルの中身と、テストコードが使っている合成データの中身を突き合わせたのです。
両者は完全に一致していました。これで「書き込まれた内容の出どころはテストの合成データである」ことが確定しました。そこから、その合成データを使って書き込み関数を呼ぶテストを洗い出し、原因の2件を特定。壊していたのは他のエージェントではなく、そのテストを実行していた自分自身でした。
原因はテストコード側にありました。実データへの書き込みを伴う全量書き出し関数を呼び出すテストが2件あり、どちらも合成データ1件だけを使う設計でした。当時の設計では、書き込み先を決めるグローバル定数を、テスト実行時だけ一時的にテスト用の場所へ差し替える(monkeypatch)必要がありました(恒久策としては、出力先を引数で渡せる設計にするのが本筋です)。この2件のテストは、その差し替えを行わないまま関数を呼んでいました。テストを実行するたびに、正規の出力先パスへ合成データ1件分の内容がそのまま上書きされていた、というのが実態です。
この調査プロセスから持ち帰れること
1. もっともらしい仮説ほど、裏取りを1段厚くする。 「他人(他のエージェント)が悪い」という仮説は、調査する側の心理的にも通りやすく、しかも一見もっともらしい状況証拠(並行作業していた事実)まであります。だからこそ、状況証拠だけで確定させず、もう一段踏み込んだ裏取りが要ります。
2. 裏取りの型は「中身の一致」を見ることです。 ログに跡がないことは「否定の証拠」にはなっても、「別の犯人がいる」ことの証明にはなりません。一方、壊れたデータの中身と疑わしい処理の出力を突き合わせて完全一致すれば、それは強い肯定の証拠になります。「跡がない」で止めず、「一致するか」まで確認する。この一段の違いが、誤診断のまま対処を始めるか、正しい原因に到達するかを分けました。
3. マルチエージェント環境では、この手の誤診断が特に起きやすいと考えたほうがいいです。 複数の作業主体が同じリポジトリに触れる環境では、「他の誰かが壊したのだろう」という仮説が常に候補に挙がります。もっともらしいからといって採用してしまうと、本当の原因(この場合はテストコードの隔離漏れ)を放置したまま、同じ事故が4回目、5回目と繰り返されることになります。実際、今回も1回目・2回目は原因を突き止めないまま復旧だけで終わらせていたため、未隔離のテストが残り続け、同じ事故が繰り返されました。
直した内容は本に書きました
隔離(monkeypatch の徹底。今回の事故への直接の再発防止はこれ)・出口側の防御(読み込み失敗時は中断・書き込みはアトミックに。こちらは別種の破損への備えで、今回の事故は防げません)・回帰確認(152件のテスト合格+テスト実行前後で実データのハッシュが変わらないことの確認)という3段構えの対処は、本の第9章に書きました。この記事で書いた「原因特定までの調査プロセス」と、本に書いた「対処の設計」は補い合う関係にあります。
なお、この事故は複数エージェントでの検収運用にも教訓を残しています。エージェントからの「大丈夫でした」という自己申告をそのまま信じず、実物(ファイルの中身・ハッシュ値)で裏取りする習慣があったからこそ、「壊れた中身とテストデータの一致」という決め手にたどり着けました。この検収運用については本の第4章で扱っています。