この記事は、連載「確かめない技術」の第 3 回です。うまくいかない症状と、その対処を一つずつ並べています。各回はファイルやスクリプトを一つ置けば完結します。その仕組みがなぜ要るのかは、置いたあとに解説を読めば分かります。連載の全体像と各回の一覧は序論にあります。
今回は、テストの再実行の話です。再実行というのは、コードを 1 文字も変えていないのに、同じテストをもう一度走らせることです。AI はこれをよくやります。「通っていることを確認します」「念のためもう一度」。そのたびには、結果が変わらないと分かっているものを、最後まで待っていました。AI の中では確認と再実行は区別されないからです。
1 作目「読まない技術」の第 1 回で、テストの実行の入口をラッパー 1 本にまとめ、全文をログに残したうえで、画面には数行の要約だけを出す形にしました。今回は、その残したログを、再実行の代わりに使います。
まず一つだけ、数えてみてください。
AI コーディングエージェントとの直近のセッションで、テストは何回走りましたか。
そのうち、コードを変えた直後ではない実行——「通っていることを確認します」「念のためもう一度」「報告の前に最終確認」——は、何回だったでしょうか。
AI はよくテストを再実行します。理由は単純で、AI は結果の確認と再実行を区別しないからです。人間は「さっき通った」を覚えているか、画面をスクロールして見返します。AI は、結果がコンテキストから流れたら(あるいはセッションが変わったら)、確かめる手段は再実行しかないと考えます。
コードが 1 文字も変わっていなくてもです。「区切りではテストで確認する」は、学習された開発の振る舞いでもあります。書き足した 1 行では、この優先順位は覆りません。
コストは時間です。の環境でテスト全量は 277.85 秒。1 作目 第 1 回で出力は数行の要約になっていますが、4 分半という時間は 1 秒も減っていません。確認のための再実行が 3 回あれば、それだけで約 14 分が消えます。おまけに、走っている 4 分半の間に別のセッションがもう 1 本走らせる事故の窓が開きます(これは次回の主題です)。
再実行を呼ぶものが、もう一つあります。絞ると名前が消える —— AI は実行結果をそのまま受け取らず、| grep や | tail で絞ってから読みます。2026-09-12 に手元の記録を数えると、全量の実行 237 回が 237 回とも絞られていて、そのまま受け取った例は 1 回もありませんでした。節約できるのは 74 行ですが、絞った要約からはどのテストが落ちたのかが消えます。失敗で返った全量 10 回のうち、失敗したテスト名が残っていたのは 0 回でした。名前が分からなければ回し直すしかなく、上の 4 分半がそのまま乗ります。絞ること自体が悪いのではありません。同じ記録で部分実行は 2,506 回中 2,362 回が絞られていて、その大半は正しい使い方です。実害が桁違いなのは全量だけでした。
この連載に出てくるの環境の数——全量の秒数、テストの件数、走らせていない領域の数——は、2026-09-08 に測ったものです。同じ環境で 2026-09-10 に測り直すと、307.80 秒・14,238 件・23 領域でした。3 つとも仕組みは同じまま動いていて、増えたのはテストの数と、それに連れた時間です。
数は日ごとに動くので、比べるときは測った日ごと見てください。
理解したことは一文にできます。
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 (run at: $(cat "${log}.time" 2>/dev/null || echo unknown) / full log: ${log}) ---"
grep -E "^Finished in " "$log" | tail -1
grep -E "^[0-9]+ runs, " "$log" | tail -1
exit 0 # 何も実行しない。読み直すだけ
fi
date '+%m/%d %H:%M' > "${log}.time" # 実行する側はここで日時を書き残す(bin/rails test の直前に)
そして 1 作目 第 1 回で置いた「素のコマンドを止める hook」の案内文に、1 行だけ足します。
To see the result only: scripts/test.sh --last (prints the last summary without re-running)
これで経路は閉じます。AI が「確認のため」に素のテストを叩く → hook が止めて案内を出す → 案内に「見るだけなら --last」がある → AI は一瞬で要約を受け取り、次の作業へ進みます。
は指示ファイルには何も足していません。確認したい瞬間の目の前に、読み直す入口を置いただけです。
大事な設計が一つ。--last の 1 行目には前回実行の日時を必ず入れます。「これは 10:41 の結果」と毎回言う。コードを変えたあとにうっかり --last で確認しても、時刻の古さが目に入る形にしておきます。
CC BY 4.0 はここまで
待たなくなったもの
確認のためのテスト実行です。4 分半の進捗表示を、はもう眺めていません。結果は変わらないと分かっているものを待つ時間が、シリーズを通して一番大きい削減です。出力の行数ではなく、実際に待つ時間が減ります。
CC BY 4.0
注意: --last は「変えていない」ときだけの近道
死角ははっきりしています。コードを変えたのに AI が --last で確認すれば、古い結果を新しい結果として読みます。守りは二つで足ります。一つは上に書いた日時の常時表示。もう一つは、--last を「実行の代わり」ではなく「再実行の代わり」としてだけ案内すること——案内文が「結果を見るだけなら」と書いているのはそのためです。変えたら流す。変えていないなら読み直す。この区別だけは残ります。
検証手順: 一瞬で返ることを確かめる
前提
- 1 作目 第 1 回のラッパーが、全文を
tmp/test_last.logに残していること - その上に、この章の再表示の入口(
--last)と、案内文への 1 行を足し終えていること
所要時間: 5 分
手順
- AI に、テストを 1 回、普通に走らせます。ここで
tmp/test_last.logと実行日時が作られます - あなたが
--lastを叩きます - あなたが、適当なファイルを 1 つ触ってから(
touchでも 1 行足すでも構いません)、もう一度--lastを叩きます
合格条件(すべて満たすこと)
- 手順 2 で、何も実行されない(テストが走り出したら、
--lastが再実行になっています) - 手順 2 で、前回と同じ件数の行が返る
- 手順 2 で、
--lastが 1 秒以内に返る - 手順 3 で、1 行目の実行日時が、手順 1 のときのままである
合格しなかったとき
- 手順 2 でテストが走り出した ——
--lastがログを読まず、実行に回っています - 手順 3 で日時が出ない —— ここが本体です。日時が無いと、コードを変えた後の
--lastが「今の結果」に見えます。1 行目に必ず出す形へ直します
後始末
- 手順 3 で触ったファイルを、変更していれば戻します
確かめたのは「読み直しが速い」ことだけです。--last は、コードを変えていないときの近道です。変えたら流す、変えていないなら読み直す——この区別だけは、仕組みでは消えません。
CC BY 4.0 はここまで
次回は「偽の失敗を読まない技術」。は、出てきた失敗を読み分けません。それでも、本物の失敗を見落としたことはありません。