ある大規模 Django プロジェクトで、「全テスト → 失敗を Issue 化 → AI エージェントのループで 1 件ずつ自律修正」というテスト保守パイプラインを組んだ記録。固有のドメイン情報は伏せ、設計パターンと仕組みだけを抜き出して解説する。構成は Phase 1(監査ツール)→ Phase 2(Issue 起票)→ Phase 3(自律修正スキル) の 3 段。
1. 背景:なぜ「自動テスト監査」が要るのか
1-1. テストスイートが育ち、master が「全 green」でなくなっていた
長年運用された業務システムでは、テストも機能をまたいで肥大化していく。テスト高速化のための基盤移行(in-memory DB + マイグレーション無効化、軽量なテストベースクラスへの opt-in 移行)を進めるなかで、ある事実が判明した:
ローカル高速化環境で master を流すと、全 green ではない。 監査の結論は 複数のサブパッケージにまたがって数十件のテストが「単独実行でも失敗」。その大半は「後発の仕様変更にテスト側が追従していない」既存の負債だった(バリデーション追加、メッセージ文言の変更、フィクスチャ欠落、削除済みコードの import 残り等)。
1-2. 手で数えると必ず誤検出する「pollution(汚染)」問題
監査で得たいちばん重要な教訓はこれだった:
サブパッケージ全体を 1 プロセスでまとめて流すと、本物でない失敗(pollution)が混ざる。
重いテストベースと軽量テストベースの混在、共有リソース(ロック等)の競合、コミット後フックの発火タイミング、DB / cache の状態リーク——これらのせいで「まとめて流すと落ちるが、ファイル単独では green」というテストが出る。
つまり 「失敗ファイルの判定は、必ずファイル単独で再実行して確かめる」 必要がある。これを人手でやると見落とすし、再現条件を毎回手で組むのも続かない。だから道具にした。
2. しくみ:pytest 失敗を Issue 化する 3 フェーズのパイプライン
| Phase | 成果物 | 役割 |
|---|---|---|
| 1 | 監査ツール(test_audit スクリプト) | ランナー+冪等 Issue レポーター |
| 2 | --report による per-file Issue 起票 | 失敗を 1 ファイル 1 Issue に正規化 |
| 3 | 自律修正スキル | エージェントのループで Issue を 1 件ずつ修正 → PR |
全体像は次のとおり。監査ツール(triage → verify → report)が失敗を冪等な Issue キューに変換し、自律修正スキルのループが 1 件ずつ消化して PR を作る。マージは必ず人手という安全境界を引いているのがポイントだ。
2-1. Phase 1・2 — 監査ツールと Issue 起票:triage → verify → report
Phase 2(Issue 起票)は独立した工程ではなく、監査ツールの report 段(--report)が担う。 つまり監査ツールが「失敗の検出」から「per-file Issue への変換」までを一気通貫でやる。スクリプトは 3 段で動く。「速い荒い絞り込み」と「正確な単独検証」を分離しているのが肝。
- triage:サブパッケージ単位の bulk 実行で「落ちた候補ファイル」を高速に絞る(速いが pollution 込み)
- verify:候補を per-file 単独実行で再検証し、real(単独でも失敗)と pollution(単独 green)を切り分ける ← 上記の教訓を強制
- report:real 失敗ファイルごとに安定マーカーで Issue を作成/更新し、green 復帰したファイルの Issue は自動クローズ(冪等同期)
実際の使い勝手(オプションのイメージ。test_audit は仮称で、実体はプロジェクト内スクリプト):
test_audit --dry-run # 既定。JSON 出力のみ(Issue 操作なし)
test_audit --scope <subpkg> --dry-run # 範囲を限定して高速確認
test_audit --files <path/to/test_file> # triage を省略し指定ファイルだけ単独検証
test_audit --report # 失敗を per-file Issue に冪等同期
test_audit --with-migrations ... # CI 相当の環境で実行
設計上の要点:
- 冪等同期:Issue 本文に安定マーカー(
<!-- test-audit:<path> -->のような不可視コメント)を埋め込む。再実行時はこのマーカーで既存 Issue を引き当て、重複起票せず本文を更新。green に戻ったファイルの Issue は「単独実行で green を確認」コメント付きで自動クローズ。→ テスト結果と Issue 群が常に同期し、人間が掃除する必要がない。 - close のスコープを実行範囲に厳密に限定:
--filesなら検証したファイルだけ、--scopeならその配下だけ、全体実行のときだけ全 Issue を close 対象にする。→ 部分実行で無関係な Issue を誤って閉じない。 - 失敗の証跡を Issue に同梱:失敗テストの識別子、再現コマンド、トレースバック抜粋、実行環境(高速環境 / CI 相当)を本文に書く。修正者(人 or AI)が即着手できる。
- ラベルで意味を分ける:「単独でも失敗(real)」と「隔離問題(pollution)」を別ラベルに。
2-2. Phase 3 — 自律修正スキル:1 イテレーション = 1 Issue
エージェント(Claude Code)を「ループ実行」で起動すると、以下を 1 件ずつ回す:
-
対象を 1 件取得:open な「自動検出された失敗」ラベルのうち、
needs-human/blockedを除外(actionable なものだけ)。0 件ならループ終了。 -
単独で再現:該当ファイルだけ pytest。すでに green なら(他 PR で解決済み等)コメントしてクローズ、次へ。
-
根本原因を特定し分類:トレースとコードを読み、
git log -Sで「いつ・なぜ壊れたか」を確認。分類 内容 対応 A. 仕様ドリフト 後発の仕様変更にテスト期待値/フィクスチャが未追従 テスト側を現行仕様へ追従 B. obsolete 機能が削除/置換され import や対象が存在しない テスト(ファイル/クラス)を削除 C. テスト基盤 setUp のマスタ/前提データ欠落など setUp を補完 D. 実バグ疑い プロダクト挙動が誤りに見える/テスト修正が本番バグの隠蔽になる 直さず needs-humanでエスカレーション -
A/B/C はテスト側を修正 → 隔離した作業ツリー(worktree)で監査ツールが green、linter クリーンを確認 → PR を作成(本文に
Closes #N)。 -
処理結果を 1 行報告して終了。
安全方針(ここが設計のいちばん大事なところ):
- マージしない。green でも PR 作成までで止め、人手レビューを待つ。
- 実バグ疑いは自動修正しない。「テストを直すと本番バグを隠す」ケースは分析コメントを残して
needs-humanへ。迷ったら D に倒す(誤った隠蔽より保留が安全)。 - プロダクトコードは原則変更しない。変更が必要=実バグ疑い=
needs-human。例外は挙動を変えないテスト専用ヘルパ/フィクスチャ、linter 設定、リネーム追従の import のみ。 - 修正は必ず 隔離した作業ツリーで(並行セッションとのブランチ競合を避ける)。
この「仕様ドリフトはテスト側を直す/実バグ疑いは触らず人へ渡す」という境界線こそが、自律修正を安全に回すための中心的な発明。AI に丸投げして本番バグを塗り潰させない歯止め。
3. 効果(現時点 — パイプラインは稼働中)
注記:このパイプラインは現在まさに稼働中で、失敗キューの消化はまだ途中。ここで挙げるのは「測定済みの成果」ではなく、設計から期待される効果と、これまでに観測できた初期シグナルである。消化件数・修正リードタイム等の定量評価は、ひととおり回し切ってから別途まとめる。
3-1. テスト保守が「キュー消化」になる構造になった(仕組みの確立)
- 監査結果(散文の調査メモ)が 1 ファイル 1 Issue の構造化キューに変換され、ループで順番に消化できる形になった。ここまでが「基盤が揃った」という到達点。
- 冪等同期により、修正がマージされて green に戻れば Issue は次回実行で自動クローズ。「直したのに Issue が残る」「もう直ってるのに再起票される」という手作業のノイズが原理的に出ない。
- 初期実証として、自動起票された Issue の 1 件(削除済みコードの obsolete テスト)が本スキルの手順どおり PR で消化された。これは「仕組みが端から端まで通る」ことの 1 件確認であって、全体の消化実績ではない。
3-2. 人間の役割を「分類と判断」へ寄せる狙い
機械的な部分(再現・単独検証・原因の git 追跡・テスト修正・PR 作成)はエージェントが回し、人間は PR レビュー と needs-human の判断 に集中する——という分業を意図している。「マージしない/実バグは触らない」を守らせているので、暴走で本番ロジックが書き換わるリスクがない。実際にどれだけ人の手数が減るかは、消化が進んだ段階で振り返る。
3-3. 稼働させてすぐ、ツール自身の「幻の失敗(phantom failure)」を炙り出した
運用してすぐ、監査ツール自体に false-positive があると判明した。しかも 2 系統——いずれも「テストの不具合ではなく、ツール/環境の側が生んだ幻の失敗」だった。
(a) 環境差による幻の RED。 「エラーメッセージが英語で、日本語期待のテストが落ちる」クラスタは、実はテストの不具合ではなく 翻訳ファイル .mo が VCS 管理外でチェックアウト直後に存在せず、gettext が翻訳済み文言ではなく原文(msgid)にフォールバックしていたのが原因だった。CI は compilemessages を走らせるので緑、ローカル監査環境だけ赤——という環境差。修正は、監査ツール起動時に msgfmt で .po → .mo を自前コンパイルするようにしただけ(manage.py compilemessages は DB 接続を要するため msgfmt を直叩き)。これで監査環境を CI と揃えた。
(b) パーサのバグによる幻の検出。 全体 --report 実行で、pytest のログ出力行(ERROR celery... や ERROR root:base.py:82 ... のようにログレベルが行頭に来る)をテスト結果の要約行と誤認し、logger:file.py:line を「失敗ファイル」と解釈して 存在しないファイルの Issue を生成してしまった。原因は結果行の正規表現が緩すぎたこと(^(FAILED|ERROR)\s+(\S+) がログ行にもマッチ)。修正は正規表現の厳格化——FAILED/ERROR の後は単一スペースのみ(ログのレベル表記はパディングで複数スペース)、かつ node を実テストパス(<path>.py または <path>.py::<test>)に限定し、logger:file.py:line 形式を除外。pytest サマリ本来の FAILED path::test / ERROR path(collection error / setUp error)は引き続き拾う。
どちらも **実在する失敗クラスタの判定には影響しない「起票時だけの表面的な誤り」**で、冪等同期と人的レビューが効いて実害はゼロだった。
副次効果としていちばん価値があったのはこれかもしれない。自動 Issue 化したことで、「真の失敗 vs 環境差 vs パーサ誤検出 vs テスト隔離」を切り分ける圧力が生まれ、監査基盤そのものを堅牢化する方向へ改善が連鎖した。自動化は、自分の欠陥を観測可能な成果物(誤った Issue)として吐き出すぶん、むしろ直しやすい——という学び。
4. 論点:対象が「テストコードだけ」なら自動マージでよいのでは?
最初に必ず出る問いがこれ。「いじるのは単体テストのコードだけなのだから、green かつ linter クリーンなら人を待たずに自動マージすればよいのでは?」——一見もっともだが、このパイプラインでは意図的に PR 作成までで止め、マージは人手にしている。理由は一言でいうと:
「テストコードしか変わらない」=「リスクもテスト内に閉じる」ではない。 diff はテストファイル限定でも、リスクは本番の correctness(挙動の正しさ)側に漏れ出る。
以下、当面は人手マージを維持する 8 つの理由を挙げる。
4-1. 最大の失敗モードが「分類ミス」で、その被害は本番側に出る
このスキルの安全性は A/B/C/D 分類に全面的に乗っている。いちばん危険なのは D(実バグ)を A(仕様ドリフト)と誤判定し、テストを直して green にするケース。このとき diff は「テストファイルのみ」だが、意味としては 「正しく失敗していた検知を消した」=本番バグを黙って埋めたことになる。自動マージだとこの最悪パターンこそ誰の目にも触れず land(マージされて取り込まれる)し、バグ出荷時に初めて発覚する。スキルが「迷ったら D に倒す」のはこの誤判定が怖いからで、人的レビューはその最終バックストップ。
4-2. テストは実行可能な「仕様」。テスト変更=仕様変更
assert を緩める・期待値を書き換える・テストを削除する(B)は、いずれも「何を正しい挙動とみなすか」を変える行為。プロダクト仕様の変更に sign-off(承認)が要るのと同じ理由で、テスト変更にも人の承認が要る。「テストだけ」は工数の話であって、仕様影響の話ではない。
4-3. AI は「正しく直す」より「green にする」に最適化しやすい
LLM は通りさえすればよい方向に滑りやすく、危うい green の作り方がある——assert を弱める/許容幅を広げる/skip・xfail を足す/対象そのものを mock で消す/例外を握り潰す。いずれも CI は green になるがカバレッジが空洞化する。「テストが通る」と「テストが意味を保っている」は別物で、これを見抜くのは今のところ人間が確実。
4-4. 自動マージが信用する green は、本プロジェクトが「当てにならない」と学んだ信号そのもの
判定は per-file 単独 green が条件だが、pollution は他ファイルとの組み合わせでしか出ない——これは設計の出発点になった教訓だった(§1-2)。per-file green を根拠に自動マージするのは、その教訓と正面から矛盾する(後述の CI 突合・bulk スモークが揃うまでは特に。→ 改善案は §5-5)。
4-5. ツール自身が「壊れているか」を間違えうる(幻の失敗の教訓)
§3-3 が示したのは、「そもそも壊れていたのか」というツールの判断自体が誤りうること。それも環境差(翻訳未コンパイル)とパーサのバグ(ログ行の誤検出)の 2 系統で起きた。もし初期に自動マージが有効だったら、壊れてもいないテストを「直し」に行って誤った変更を量産していたはず(特にパーサ誤検出では存在しないファイルを起点に走り出しかねなかった)。
4-6. 自動化 × 無人マージは、1 つの系統的ミスがキュー全体に伝播する
ループは無人で複数 PR を land できる。レビュー gate は systematic な誤りが連鎖する前にレート制限をかける役割も担う。
4-7. コストの非対称性
テスト PR は小さく・test-only・Closes #N・本文に根拠ありでレビューが安い。一方、誤マージは後から・本番で・高くつく。安い保険で高い事故を防ぐ、という割り切り。
4-8. とはいえ「段階的自動マージ」は合理的
全部を人手に縛る必要はなく、安全な部分集合だけ卒業させるのが現実的。
- B(obsolete)の純粋削除で、対象シンボルが import error で存在しないことを機械的に確認できるもの → リスクは最小。
- 条件付き自動マージのゲート例:
分類 = Bかつ高速環境と CI 相当の両方で green(§5-1)かつassert 弱化・skip 追加・mock 増加を検出しない(差分の静的ガード)。 - A/C は仕様判断が絡むので人手のまま、D は当然エスカレーション。
まとめると、主たる理由は 「test-only な diff でも、リスクは本番の correctness 側に出る。その境界判断をツールは間違いうるので、安い人的レビューをバックストップに置く」。逆に CI 突合・クラスタ化・差分の静的ガードが揃えば、B の機械的削除から自動マージへ昇格させるのは十分妥当。
5. さらに改善するポイント
実運用して見えてきた、次に効きそうな改善を効果順に。
5-1. CI との突合を自動化する(最優先)
現状の判定は「ローカル高速化環境(in-memory DB + マイグレーション無効化)」が基準。CI 相当に切り替えるオプションはあるが、両環境を回して差分を出す運用にはなっていない。上記の幻の失敗(phantom)問題のように「環境差で赤」は今後も出る。→ 高速環境と CI 相当の両方で verify し、両方で real のものだけを失敗ラベル、片方だけのものは env-dependent ラベルに分けると、人間の判断コストがさらに下がる。
5-2. pollution(隔離問題)にも自律トラックを用意する
今は隔離問題ラベルを検出して列挙するだけで、自律修正の対象外。pollution は「どのテストが状態を漏らしているか」の特定が本体で、二分探索的に「A を先に流すと B が落ちる」ペアを突き止める補助コマンドがあると、軽量テストベースへの移行の安全弁になる。
5-3. 失敗のクラスタリングで Issue をまとめる
監査では「同じバリデーション未追従」「同じ前提データ欠落」など、同一根本原因が複数ファイルに散ることが多い。今は 1 ファイル 1 Issue なので、同根の修正が複数 PR に分かれる。→ トレースの正規化(例外型+メッセージの指紋)で**クラスタ Issue(親)+ファイル(子)**の 2 階層にすると、1 PR で束ねて直せる。エージェントの 1 イテレーションあたり効率も上がる。
5-4. 分類の根拠を学習資産として蓄積する
A/B/C/D の分類は過去の前例に強く依存している。→ 各 Issue のクローズ時に「最終分類・決め手・参照コミット」を構造化メモに残し、スキルが次回その辞書を参照すれば、特に D(実バグ疑い)への倒し込み精度が上がる。
5-5. green 復帰の検証を「単独」から「bulk 込み」へ
green クローズの判定は per-file 単独 green が条件。だが pollution を生む変更は 他ファイルとの組み合わせでしか露見しない。→ クローズ前に「同範囲の bulk でも閉じた Issue 群が green か」を軽くスモークテスト(最小限の疎通確認)すると、「単独で直したつもりが全体を汚した」回帰を早期に捕まえられる。
5-6. 定期実行(スケジュール)でドリフトを未然に防ぐ
今は手動ループ。master への push 後や日次で --report を回し、新規 real 失敗を即 Issue 化すれば、「気づいたら数十件溜まっていた」状態に戻さない。ただし起票の洪水を避けるため、§5-1(CI 突合)と §5-3(クラスタリング)が前提。
まとめ
このパイプラインの本質は 「テスト失敗という非構造のノイズを、冪等な Issue キューに変換し、安全境界(マージしない/実バグは触らない)を引いた上で AI に消化させる」 ことにある。
- Phase 1:監査の教訓(per-file 単独検証で pollution を排除)をツールに固定化
- Phase 3:ループで 1 件ずつ安全に自律修正するスキル化
- 改善の起点:自動化が炙り出した幻の失敗(phantom failure。環境差=翻訳未コンパイル/パーサのログ行誤検出の 2 系統)を潰し、監査基盤そのものを堅牢化。自動化は欠陥を「誤った Issue」として可視化するぶん直しやすい
狙いは、人間が「分類が難しいもの」と「PR レビュー」だけを見ればよくする分業。基盤は揃い、いまキューを消化している最中で、定量的な効果検証はこのあと。次は CI 突合の自動化と失敗のクラスタリングで、さらに人の判断回数を減らせる見込み。
そして「test-only なら自動マージでよいのでは?」という問い(§4)への答えは——いまは No、ただし条件付きで Yes に向かう。test-only な diff でもリスクは本番の correctness 側に出て、その境界判断をツールは間違いうる。だから当面は安い人的レビューをバックストップに置く。CI 突合・差分の静的ガードが揃えば、最も機械的な B(obsolete 削除)から自動マージへ昇格させる——という段階的な自動化が、安全と速度を両立させる現実解になる。
補足:この設計が他プロジェクトでも効く条件
- テスト実行が ファイル単位で隔離再現できること(pollution と real を切り分けられる)。
- 失敗の多くが 「テスト側の追従漏れ」 に分類できること(実バグ比率が高すぎると自律化の旨味が薄い)。
- PR ベースのレビュー文化があること(自動マージしない安全弁が機能する前提)。
- エージェントに 作業ツリー隔離・linter・再現コマンドを渡せること(再現性のある修正ループ)。
