---
title: "読まない技術 第1回：テスト結果を読まない技術"
author: garplab
publisher: TypingTube
license: CC BY 4.0
license_url: https://creativecommons.org/licenses/by/4.0/
license_scope: 「CC BY 4.0」の印から始まる節（仕組み・検証手順・コード）。印の無い本文は著作権を留保
canonical: https://typing-tube.net/articles/cd93e0c5cac832
series: "バイブコーディングにおける読まない技術"
language: ja
---


> この記事は、連載「読まない技術」の第 1 回です。各回はファイルやスクリプトを一つ置けば完結します。連載の全体像と各回の一覧は[序論](https://typing-tube.net/articles/34b02627c718fa)にあります。

今回は、テストの出力の話です。AI がテストを走らせるたびに、数万行の出力がそのコンテキストに流れ込みます。そのうち AI に要るのはどの行かを扱います。

まず一つだけ、確かめてみてください。AI コーディングエージェントとの直近のセッションで、テストを実行した箇所の出力を探します。何行ありますか。その出力を最後まで読んだ人はいますか。

読んだ人は、たぶんいません。ただし AI は別です。**AI はツール出力を読み飛ばせません**。届いた行は全部コンテキストに入り、次の応答の材料になり、課金の対象になります。直しながら 5 回走らせれば 5 回入ります。コンテキストが詰まるほど応答の品質は落ちるので、**テストを回すほど、テストを直す能力が下がる**構造です。

私の手元の本番プロジェクトで、テスト全量の出力は 26,147 行（2.1MB）ありました。[typingtube](https://typingtube.net) という、YouTube の音楽動画でタイピング練習ができる Web サービスで、個人で運営しています。13,950 件のテストを回すと、この量になります。この記事で置く仕組みは 1 つ、それを **3 行にする**ラッパー（本来のコマンドを包んで代わりに実行するスクリプト）です。

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

> **AI はテスト出力を全部コンテキストに入れる。だから出力の側を、失敗と件数だけが届く形にする。**

## 仕組み: 出力の入口を 1 本に変える

テスト実行の入口をラッパー 1 本に差し替えます。全文はログへ、会話には要約だけ。次のスクリプトは実物からの簡約版です（例は Rails / Minitest ですが、`grep` する行を自分のテストランナーの出力に合わせて差し替えるだけで、何にでも使えます）。

```bash
#!/bin/bash
# scripts/test.sh — 全出力をログに残し、会話には要約だけ返す
log=tmp/test_last.log
mkdir -p tmp
bin/rails test "$@" >"$log" 2>&1
status=$?

echo "--- summary (full log: ${log}) ---"
grep -E "^Finished in " "$log" | tail -1
grep -E "^[0-9]+ runs, " "$log" | tail -1   # 件数の行。⚠️ 削らない（後述）

failures=$(grep -cE "^(Failure|Error):" "$log" || true)
if [ "${failures:-0}" -gt 0 ]; then
  echo "--- ${failures} failure(s)/error(s) (first excerpt) ---"
  grep -A 3 -E "^(Failure|Error):" "$log" | head -40
fi
exit $status
```

出てくるのはこれだけです。

```
--- summary (full log: tmp/test_last.log) ---
Finished in 277.851362s, 50.2067 runs/s, 318.7712 assertions/s.
13950 runs, 88571 assertions, 0 failures, 0 errors, 228 skips
```

私の実物の要約は 29 行あり、この記事で置く最小形はその核の 3 行です。残りの行は、走らせていない領域の一覧など、この連載の後の回で足していきます。

大事なのは、**AI への指示を 1 行も足していない**ことです。「出力は要約して読むこと」と指示ファイルに書く手もありますが、それは[序論](https://typing-tube.net/articles/34b02627c718fa)で見たとおり、足すほど薄まる側の解です。指示ではなく入口を変えました。ルールは破れますが、入口はそこにしかありません。

ただし、置いただけでは AI がこの入口を通ってくれません。`rails test` のような標準コマンドは、AI が学習で骨身に染みている言葉です。あなたが置いた `scripts/test.sh` は、AI が今日初めて見る言葉です。「ラッパーを使うこと」と指示ファイルに書いても、テストを走らせようとした瞬間に出てくるのは、何億回と見てきた標準コマンドの側です。指示ファイルの 1 行は、AI の中で染み付いた習慣と優先順位を競うことになり、たいてい負けます。だから、標準コマンドの側を塞ぎます。

Claude Code なら、素の `rails test` を止めてラッパーへ案内する hook を 1 つ添えると、入口が本当に 1 本になります。hook はコマンド実行の直前に割り込むスクリプトで、`.claude/settings.json` にこう登録します。

```json
{
  "hooks": {
    "PreToolUse": [{
      "matcher": "Bash",
      "hooks": [{
        "type": "command",
        "command": "$CLAUDE_PROJECT_DIR/.claude/hooks/use_test_wrapper.sh"
      }]
    }]
  }
}
```

中身は「コマンドが素の `rails test` なら、ラッパーの使い方を表示して実行させずに終わる」だけです。

```bash
#!/bin/bash
# .claude/hooks/use_test_wrapper.sh
command=$(cat | python3 -c 'import json,sys; print(json.load(sys.stdin).get("tool_input",{}).get("command",""))')
case "$command" in
  *scripts/test.sh*|*SKIP_TEST_GUARD=*) exit 0 ;;   # ラッパー経由と、明示の逃げ道は通す
esac
if printf '%s' "$command" | grep -qE '(^|[;&|[:space:]])(bin/)?rails[[:space:]]+test([[:space:]]|$)'; then
  echo "Run tests with scripts/test.sh <same args> (full output stays in tmp/test_last.log)" >&2
  exit 2   # exit 2 = 実行させず、stderr の案内を AI に読ませる
fi
exit 0
```

AI は素のコマンドでは動けず、案内を読んだときにだけ先へ進めます。人間が気づいて「ラッパーを使って」と会話に割り込むのと違い、hook の案内は作業の流れの中に届きます。AI はその場で読み替えて、自分の優先順位を崩さずに進み直す——人間の指示を挟まないことも、仕組みの効き目の一部です。この形はシリーズで何度も出てきます。

## 読まなくなったもの

テストの生出力です。人間も AI も読みません。ただし捨ててはいません。全文は毎回 `tmp/test_last.log` に残り、要約の 1 行目がその場所を言います。原因調査で本当に全文が要るときだけ、そこを開きます。

## 注意: 失敗以外の異常が見えなくなる

この仕組みの死角は、**失敗という形をとらない異常**が隠れることです。スキップの急増、警告、実行件数の減少——テストが丸ごとロードされなくなっても、残りが全部通れば「0 failures」です。

だから件数の行だけは要約から削りません。上の実行例の `13950 runs, ... 228 skips` がそれです。前回 13,950 だった runs が 9,000 に落ちていたら、失敗が 0 でも何かが起きています。読まずに数字だけで異常を拾う話は、第 9 回（引き継ぎを読まない技術）でまとめます。

## 検証手順: 失敗が届くことを一度だけ確かめる

**前提**

- この回のラッパー（`scripts/test.sh`）を置き、素のテストコマンドを止める hook を登録し終えていること
- テストが 1 件以上あり、手元で走ること

**所要時間**: 5 分

**手順**

1. **あなたが**、テストを 1 件わざと壊します。短い時間で走るテストファイルを 1 つ開き、期待値を 1 文字だけ変えます（`assert_equal "abc", ...` を `"abd"` に）。⚠️ 後始末で元に戻すので、**ファイル名と行番号を控えておいてください**
2. **AI に**「テストを走らせて」と頼みます。あなたが直接ラッパーを叩くのではなく、AI に頼んでください——確かめたいのは、AI が入口を通ったときに何が届くかだからです

**合格条件**（返ってきた要約が、すべて満たすこと）

- **全文ログの場所**が出ている（`tmp/test_last.log` のような 1 行）
- **壊したテストの名前と、期待値・実際値の差**が出ている（`Expected: "abc"` と `Actual: "abd"` に当たる行。手順 1 で控えたファイル名が、ここに出ます）
- **件数の行**の `failures` が **1 以上**になっている（`13950 runs, 1 failures` のような行）

**合格しなかったとき**

- **`0 failures` と出た** —— 壊したテストが、そもそも走っていません。手順 1 で選んだファイルが実行の対象に入っているかを見直してから、手順 2 をやり直します
- **失敗はしたが、テスト名や差が出ない** —— 要約が拾う行が足りません。ラッパーの `grep` の範囲を広げます

**後始末**

- 壊した 1 文字を元に戻し、もう一度 AI に走らせます。件数の行が `0 failures` に戻れば終わりです
- ⚠️ 戻し忘れると、この後の作業がずっと失敗し続けます

確かめたのは「失敗したら必ず届く」の側だけです。これを見ずに運用を始めると、要約が静かな理由が「失敗がない」のか「失敗が届かないつなぎ方」なのか、区別が付かないまま使うことになります。

---

この記事は「出力をどう届けるか」だけを扱いました。テストを**いつ・何本走らせるか**（再実行、並列実行、重い E2E の選択実行）は別の問題系なので、この連載では扱いません。

次回は「メモリを読まない技術」。**AI が書き溜めた知見メモを、私は読み返していません。**それでも、メモが増えて困ったことはありません。

---

**連載「読まない技術」**

- ← 前回: [序論 AIの出力を、もうほとんど読んでいない](https://typing-tube.net/articles/34b02627c718fa)
- → 次回: [第 2 回 メモリを読まない技術](https://typing-tube.net/articles/384477896fec22)
- 全回の一覧: [序論 AIの出力を、もうほとんど読んでいない](https://typing-tube.net/articles/34b02627c718fa)
