本文へスキップ
hdknr blog
戻る

テスト失敗を「人が直す」から「キューを自律消化する」へ — 監査ツール + 自律修正スキルの設計

ある大規模 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 を作る。マージは必ず人手という安全境界を引いているのがポイントだ。

pytest 失敗を Issue キューに変換し AI エージェントが自律修正する3フェーズ・パイプラインの図。Phase1 の監査ツールが triage(bulk 実行で候補を絞る)→ verify(per-file 単独実行で real と pollution を切り分け)→ report(real 失敗を per-file Issue に冪等同期)と流れ、Phase2 で Issue キューに変換され、Phase3 の自律修正スキルが Issue を 1 件取得 → 単独再現 → A/B/C/D 分類し、A/B/C はテスト修正して PR 作成、D は実バグ疑いとして needs-human にエスカレーション、PR は人手レビューを経てマージするループを示している

2-1. Phase 1・2 — 監査ツールと Issue 起票:triage → verify → report

Phase 2(Issue 起票)は独立した工程ではなく、監査ツールの report 段(--report)が担う。 つまり監査ツールが「失敗の検出」から「per-file Issue への変換」までを一気通貫でやる。スクリプトは 3 段で動く。「速い荒い絞り込み」と「正確な単独検証」を分離しているのが肝。

実際の使い勝手(オプションのイメージ。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 相当の環境で実行

設計上の要点:

2-2. Phase 3 — 自律修正スキル:1 イテレーション = 1 Issue

エージェント(Claude Code)を「ループ実行」で起動すると、以下を 1 件ずつ回す:

  1. 対象を 1 件取得:open な「自動検出された失敗」ラベルのうち、needs-human / blocked を除外(actionable なものだけ)。0 件ならループ終了。

  2. 単独で再現:該当ファイルだけ pytest。すでに green なら(他 PR で解決済み等)コメントしてクローズ、次へ。

  3. 根本原因を特定し分類:トレースとコードを読み、git log -S で「いつ・なぜ壊れたか」を確認。

    分類内容対応
    A. 仕様ドリフト後発の仕様変更にテスト期待値/フィクスチャが未追従テスト側を現行仕様へ追従
    B. obsolete機能が削除/置換され import や対象が存在しないテスト(ファイル/クラス)を削除
    C. テスト基盤setUp のマスタ/前提データ欠落などsetUp を補完
    D. 実バグ疑いプロダクト挙動が誤りに見える/テスト修正が本番バグの隠蔽になる直さず needs-human でエスカレーション
  4. A/B/C はテスト側を修正 → 隔離した作業ツリー(worktree)で監査ツールが green、linter クリーンを確認 → PR を作成(本文に Closes #N)。

  5. 処理結果を 1 行報告して終了。

安全方針(ここが設計のいちばん大事なところ):

この「仕様ドリフトはテスト側を直す/実バグ疑いは触らず人へ渡す」という境界線こそが、自律修正を安全に回すための中心的な発明。AI に丸投げして本番バグを塗り潰させない歯止め。

3. 効果(現時点 — パイプラインは稼働中)

注記:このパイプラインは現在まさに稼働中で、失敗キューの消化はまだ途中。ここで挙げるのは「測定済みの成果」ではなく、設計から期待される効果と、これまでに観測できた初期シグナルである。消化件数・修正リードタイム等の定量評価は、ひととおり回し切ってから別途まとめる。

3-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 を弱める/許容幅を広げる/skipxfail を足す/対象そのものを 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. とはいえ「段階的自動マージ」は合理的

全部を人手に縛る必要はなく、安全な部分集合だけ卒業させるのが現実的。

まとめると、主たる理由は 「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 に消化させる」 ことにある。

狙いは、人間が「分類が難しいもの」と「PR レビュー」だけを見ればよくする分業。基盤は揃い、いまキューを消化している最中で、定量的な効果検証はこのあと。次は CI 突合の自動化と失敗のクラスタリングで、さらに人の判断回数を減らせる見込み。

そして「test-only なら自動マージでよいのでは?」という問い(§4)への答えは——いまは No、ただし条件付きで Yes に向かう。test-only な diff でもリスクは本番の correctness 側に出て、その境界判断をツールは間違いうる。だから当面は安い人的レビューをバックストップに置く。CI 突合・差分の静的ガードが揃えば、最も機械的な B(obsolete 削除)から自動マージへ昇格させる——という段階的な自動化が、安全と速度を両立させる現実解になる。

補足:この設計が他プロジェクトでも効く条件



前の記事
目標駆動・自律改善システムの構築ベストプラクティス — Claude Code で自分で改善しながら回るシステムを作る
次の記事
SNS 系の EC 連動を整理する: Instagram・TikTok Shop・YouTube ショッピング・LINE(Lineup)