先に結論
AIエージェントにPythonコードの修正とテストを任せていたとき、指定されたテストコマンドではテストが1件も実行されなかったのに、AIが作業全体をPASSと報告したことがありました。
AIは、指定されたコマンドのテスト探索開始位置を変更し、17件のテストを実行していました。こちらはすべて成功していました。しかも、元のコマンドでは0件だったことも報告に書かれていました。
当時の指示には、未達項目が残るならPASSにしない規定も、必要な条件を満たせなければ止まって報告する規定もありました。
この記事では、AIのPASS報告を見るときに何を確認すべきか、そして機械的に確認できる条件をどう検証するかを、この経験から紹介します。
AIに何を頼んでいたのか
私は個人で、AIエージェントを使ってソフトウェアを開発しています。開発の目的や重要な判断は自分で決め、コードの修正やテストの実行はAIに任せています。作業の後には、別のAIによるレビューも行っていました。
単に「ここを直して」と頼むだけでは、いつ作業が完了したのか分かりません。そこで、修正してよいファイルや実行するテスト、完了を認める条件をあらかじめ決めていました。
例えば「指定した範囲のファイルだけを修正し、決めたコマンドでテストする。条件が一つでも未達なら完了とは報告しない」といった具合です。
この記事では、こうした作業完了の条件を「受入条件」と呼びます。
受入条件の中に、矛盾があった
ある修復作業では、許可されていないファイルを残さないよう求められていました。__init__.pyもその一つで、削除する必要がありました。
__init__.pyはPythonでディレクトリをパッケージとして扱うために使われるファイルです。その有無は、テストの探索方法にも関係します。
一方、同じ作業には次の2つの条件がありました。
- 指定された
__init__.pyを削除する。 - 指定された
unittestコマンドでテストを実行し、成功させる。
ところが、当時のPython環境では、1番の条件を満たす、すなわち__init__.pyを削除すると、2番のコマンドでは対象のテストを発見できなくなっていました。
つまり、受入条件そのものが矛盾していたのです。
AIはどうしたのか
指定されたコマンドでは、テストが1件も見つかりませんでした。AIの報告にも、その事実が記されていました。
そこでAIは、テストを探し始めるディレクトリを変更した、別のコマンドを使いました。
すると17件のテストが実行され、すべて成功しました。
実行担当は、元のコマンドでは0件、変更したコマンドでは17件成功したと報告し、最終結果をPASSとしました。
ここで起きたのは、「実行していないテストを実行したと偽った」という話ではありません。0件だったことも、17件成功したことも、報告に残っていました。
17件成功したなら、何が問題なのか
「別の方法でちゃんとテストできたなら、それでよいのでは?」と思うかもしれません。
私も、別のコマンドを試したこと自体は問題だと思っていません。元の方法でうまくいかないとき、原因を調べて代替方法を見つけられるのはAIエージェントの利点です。
ただ、今回依頼していたのは「何らかのテストを成功させること」だけではありません。指定した条件をすべて満たすことが作業完了の条件でした。
別のコマンドで17件成功しても、最初に指定されたコマンドによる検証は達成できていません。もともとの指示に矛盾があるのですから、なおさら、実行担当だけでは完了と判定できません。
必要だったのは、例えば「指定された方法ではテストを実行できません。別の方法では17件成功しましたが、正式なテスト条件を修正してください」という報告です。
代替方法の成功は有益な情報です。しかし、それを正式な完了条件の変更として扱うには、別の判断が必要でした。
なぜテストを探す場所だけで結果が変わるのか
では、なぜテストを探し始める場所を変えるだけで、結果が違ったのでしょうか。
Pythonの振る舞いを、小さな例で説明します。なお、元の開発のファイル名やコードではありません。
次の構成を考えます。featureディレクトリには__init__.pyを置きません。
tests/
├── __init__.py
└── feature/
└── test_example.py
test_example.pyの中身は、必ず成功する簡単なテストです。
import unittest
class ExampleTest(unittest.TestCase):
def test_example(self):
self.assertTrue(True)
Python 3.13.5で、上位のtestsから探すと、テストは0件でした。
python -m unittest discover -s tests -p 'test_*.py'
結果はNO TESTS RAN、終了コードは5です。
一方、探索開始位置をfeatureに変えると、1件のテストが成功し、終了コードは0になりました。
python -m unittest discover -s tests/feature -p 'test_*.py'
これはLinux・CPython 3.13.5で実行確認した独立した最小例です。元の開発で17件が成功した結果とは区別しています。unittestの探索と0件時の終了コードはPythonのバージョンによって挙動が異なるため、別の環境では結果も確認してください。
独立レビューでは何が分かったか
作業後、修正担当とは別のAIエージェントに、変更内容と受入条件を照合させました。
その監査では、元の指定コマンドが条件を満たしていないことが指摘されました。監査側でも、同じディレクトリ構成でテストが0件・非ゼロ終了になることを再現しています。ただし、元の実行報告に当時の終了コードの具体値は残っていません。
さらに、必要な負例テストや修正が不足しているなど、ほかの受入条件にも未達がありました。コマンドの置き換えだけが差し戻し理由ではありません。
17件のテスト成功は事実でも、作業全体の完了を意味しなかったわけです。
その後、どう修復したか
最初に、矛盾していたテスト条件を正式に修正しました。上位のディレクトリから探すのではなく、対象のテストが配置されているディレクトリを探索開始位置に指定し直したのです。
先ほどの説明用の例で書くと、次の変更に相当します。
# 変更前
python -m unittest discover -s tests -p 'test_*.py'
# 変更後(説明用のファイル名)
python -m unittest discover -s tests/feature -p 'test_*.py' -v
-vは実行したテスト名と結果を詳しく表示する指定です。
ただし、これは先ほどの17件の成功報告を、後から合格と認め直したという意味ではありません。
監査で指示の矛盾が確認されたあと、次の修復作業に向けた指示書の「必須テストコマンド」そのものを書き直しました。同時に、__init__.pyを削除した状態を維持すること、新しく指定したコマンドでテストを1件以上実行して成功させることも、改めて条件にしました。
つまり、実行担当のAIがその場で試した別の方法を、そのまま無条件で合格にしたのではありません。作業条件を定める側が矛盾を修正し、後続の作業をその新しい条件で検証する、という順番にしたのです。
AIのPASS報告では、何を確かめるのか
では、AIから「作業が完了しました」と報告されたら、具体的に何を確認すればよいのでしょうか。
今回の経験から、少なくとも次の4点を分けて見たいと思います。
| 確認すること | 具体的に見るもの |
|---|---|
| 指定された方法で確認したか | 事前に決めたテストコマンドが、そのまま実行されているか |
| 実行は成功したか | コマンドの終了コードは正常か |
| テストを実際に実行できたか | 検出・実行されたテストが0件になっていないか |
| ほかの条件も満たしたか | 必要な修正、追加テスト、変更してよいファイルの範囲などに未達が残っていないか |
今回の17件の成功報告だけでは、最初のコマンドによる検証も、ほかの受入条件も満たしたことにはなりませんでした。
機械的に確かめられる部分は、別のプログラムで検証する
そこで、指定した条件を確認するためのPythonプログラムを追加しました。
このプログラムは、AIが書いたPASSという報告を信用して終わるものではありません。検証用プログラム自身が、あらかじめ決めたコマンドを実行し、終了コードとテスト件数を調べます。
考え方だけを示すと、次のようになります。これは元の実装ではなく、説明用に簡略化した例です。
import re
import subprocess
import sys
required = [
sys.executable, "-m", "unittest", "discover",
"-s", "tests/feature", "-p", "test_*.py", "-v",
]
result = subprocess.run(required, capture_output=True, text=True)
output = result.stdout + "\n" + result.stderr
match = re.search(r"Ran\s+(\d+)\s+tests?", output)
if result.returncode != 0 or match is None or int(match.group(1)) < 1:
raise SystemExit("NOT PASS: required test check failed")
print("Required test check: PASS")
この例では、requiredで指定したコマンドを実際に動かし、終了コードとテスト件数を確かめます。終了コードが正常でない場合や、テストを1件も実行できなかった場合は、PASSと表示しません。AIが別のコマンドで17件成功させたとしても、その結果で必須コマンドの検証を置き換えることはできません。
もちろん、この短いコードだけで作業全体を検収できるわけではありません。実際に追加した仕組みでは、テスト以外の受入条件にもそれぞれ必要な確認を対応づけ、一つでも失敗・未確認が残れば全体を合格にしないようにしました。必須テストが失敗し、代替テストだけが成功したケースを使った回帰テストも追加しています。
その後の修復作業では、改訂した条件に沿った検証結果をもとに独立監査を行い、完了が確認されました。
ただし、テストが1件以上実行されたというだけで、その内容が十分だとは限りません。必要な負例や境界条件まで検証できているか、そもそも受入条件が妥当か、といった判断には別のレビューが必要です。
最後に
AIの完了報告をそのまま信用しない、という話は珍しくありません。
今回、私が記録しておきたかったのは、指定されたテストが0件だったことも報告され、止まる規定まであったのに、それでもPASSと判定されたという経験です。そして、その後に完了条件の確認方法を変えたことです。
AIが別の方法を試して問題を調べること自体は、役に立ちます。ただし、その成功によって元の受入条件まで満たしたことにはできません。
「満たせなければ止まる」と指示するだけでなく、機械的に確かめられる条件はAIの自己申告とは別に検証する。
今回の経験から、そうするようにしました。
参考資料: Python公式ドキュメント — unittest / Test Discovery
※この記事は個人開発での経験を一般化して記述しています。ディレクトリとコードは独立した合成例です。