この記事は、連載「確かめない技術」の第 3 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。

今回は、テストの再実行の話です。再実行というのは、コードを 1 文字も変えていないのに、同じテストをもう一度走らせることです。AIは「最終確認をして引き継ぎます」と書いて、直前に通ったばかりのテストを走らせ直しました。AIにとって、確認と再実行は区別されないからです。私は、結果が変わらないと分かっている実行を、最後まで待っていました。

私は、1 作目「読まない技術」の第 1 回で、テストの実行の入口を、ラッパー(テストを走らせるスクリプト scripts/test.sh)1 本にまとめました。ラッパーは、テストの出力の全文をログに残し、画面には数行の要約だけを出します。

まず一つだけ、数えてみてください。

AIコーディングエージェントとの直近のセッションで、テストは何回走りましたか。

そのうち、前の実行からコードを 1 文字も変えずに走らせた実行——「念のため全量も回します」「最終確認をして引き継ぎます」——は、何回だったでしょうか。

「最終確認をして引き継ぎます」と書いたとき、AIは、同じテストが通ったことを 1 分前に自分で報告していました。結果は手元にあったのに、AIは、確認のためにテストをもう一度走らせました。

再実行のコストは、時間です。私の環境では、テストを全量(すべてのテスト)走らせると 277.85 秒、約 4 分半かかります。確認のための再実行が 3 回あれば、それだけで私の待ち時間は約 14 分です。

AIがテストを再実行する理由は、ほかにもあります。絞ると名前が消えるという問題です。AIは、テストの出力を | grep や | tail で絞ってから読みます。手元の記録では、AIは全量の実行 237 回の出力を、237 回とも絞っていました。失敗で返った 10 回のうち、絞ったあとの出力に失敗したテストの名前が残っていたのは 0 回でした。名前が分からないので、AIは同じ全量をもう一度走らせました。

あなたのAIも、テストが通ったと報告したあとで、コードを変えないまま「最終確認」と書いて、同じテストを走らせ直したとします。このとき、あなたなら次のどちらを選びますか。

  • 指示ファイルに「コードを変えていないなら、前回の結果を読む」と書き足す
  • AIがテストを走らせようとしたその場で、前回の結果を読む入口を案内する

答えは、その場で案内するほうです。結果は手元にあったのに、AIは、確かめる手段にテストを走らせることを選びました。指示ファイルの 1 行は、AIがあらかじめ読むルールです。上の「最終確認」も、同じ実行を流し直さないと書いた文書をAIが読んだあとで起きました。前回見たとおり、AIが方針に従ったのは、その場で方針を読ませたときでした。その場で案内すれば、前回の結果を読む入口は、AIが次の操作を決める前に届きます。

理解したことは一文にできます。

AIは結果の確認と再実行を区別しない。だから、AIがテストを走らせようとしたその場で、前回の結果を読む入口を示す。

CC BY 4.0

仕組み: テストを走らせようとしたその場で、前回の結果を読む入口を示す

1 作目 第 1 回のラッパーが、テストを走らせるたびに出力の全文を残す先は、tmp/test_last.log です。

つまり、結果の確認に必要なものは、このログの中にあります。ラッパーに足すのは、前回の結果を読む入口 1 つです。

# scripts/test.sh へ追記。--last の分岐は log= と mkdir の直後に置く
if [ "${1:-}" = "--last" ]; then
  if [ ! -f "$log" ]; then echo "No test run recorded yet" >&2; exit 1; fi
  echo "--- last summary (full log: ${log}) ---"
  grep -E "^Finished in " "$log" | tail -1
  grep -E "^[0-9]+ runs, " "$log" | tail -1
  exit 0   # 何も実行しない。読み直すだけ
fi

そして、1 作目 第 1 回で置いた「素のコマンドを止める hook」(AIがラッパーを通さずにテストを走らせようとしたとき、その実行の直前に割り込んで止める仕組み)が、止めたときにAIへ返す案内文に、1 行だけ足します。

To see the result only: scripts/test.sh --last (prints the last summary without re-running)

--last と案内文の 1 行を置いたあとの流れは、次のとおりです。AIが、結果を確かめるために、素のコマンドでテストを走らせようとする → hookが止めて案内文を出す → 案内文に「結果を見るだけなら --last」とある → AIは一瞬で前回の結果を受け取り、次の作業へ進みます。

指示ファイルは、この仕組みを置く前と同じです。

--last の 1 行目には、出力の全文を残したログの場所を出します。絞ったあとの出力から失敗したテストの名前が消えていても、AIは、全量を再実行する代わりに、このログからその名前を読むことができます。

CC BY 4.0 はここまで

待たなくなったもの

待たなくなったのは、確認のための再実行です。私が 4 分半の進捗表示を眺めるのは、コードを変えたあとの実行のときだけになりました。

CC BY 4.0

注意: --last が再実行の代わりになるのは、コードを変えていないときだけ

--last の死角は、AIがコードを変えたあとに --last で確認すると、古い結果を新しい結果として読むことです。案内文に「結果を見るだけなら」と書いたのは、AIに --last を案内する場面を、「再実行の代わり」に限るためです。コードを変えたらテストを走らせ、変えていないなら前回の結果を読み直すという区別は、案内文を置いたあとも、AIがつけることとして残ります。

案内文が出るのは、hookが素のコマンドを止めたときだけです。AIがラッパーを通して走らせ直した実行は、案内文なしで最後まで走ります。

検証手順: 一瞬で返ることを確かめる

前提

  • 1 作目 第 1 回のラッパーが、出力の全文を tmp/test_last.log に残していること
  • そのラッパーに --last(前回の結果を読む入口)を足し、hookの案内文に 1 行を足し終えていること

所要時間: 5 分

手順

  1. AIに、ラッパーでテストを 1 回走らせます。ここでラッパーが、tmp/test_last.log を書き残します
  2. あなたが --last を叩きます

合格条件(すべて満たすこと)

  • 手順 2 で、--last が前回の結果を表示するだけで終わる(テストが走り出したら、--last が再実行になっています)
  • 手順 2 で、手順 1 と同じ件数の行が返る
  • 手順 2 で、--last が 1 秒以内に返る

合格しなかったとき

  • 手順 2 でテストが走り出した —— --last を付けても、ラッパーがログを読む代わりにテストを実行しています

後始末

  • 要りません。手順 1 でも手順 2 でも、コードは変えていません

この検証手順で確かめたのは、「読み直しが速い」ことだけです。--last が再実行の代わりになるのは、コードを変えていないときだけです。


CC BY 4.0 はここまで

次回は「偽の失敗を読まない技術」。私は、出てきた失敗を読み分けません。 それでも、本物の失敗を見落としたことはありません。