テストは通る。だが代償があった。
========================================== 結果: 363 / 363 テスト通過 ==========================================
最高の気分でした。英語対応のテストを53件追加して、全部グリーン。完璧。
「よし、次の作業に...」と Claude Code に話しかけたら、こう返されました。
「ログインしてください」
は? さっきまで普通に動いてたよね?
再ログインして作業を続け、しばらくしてまたテストを回す。全パス。Claude Code に戻る。
「ログインしてください」
ここで気づきました。テストを回すたびにログイン情報が消えている。363個のテストは全部通っているのに、テスト自体が Claude Code を殺していたのです。
テストが自分の開発環境を自己破壊していました。
何が起きていたか
Claude Code 用の /todo コマンドを開発中、テストを実行するたびに ~/.claude/ ディレクトリごと消滅していました。ログイン情報(.credentials.json)、セッション、設定ファイル、全部。
犯人のコード
テンプレート機能の英語出力テストで、本物の ~/.claude/todo-templates.json を汚染しないよう HOME を一時ディレクトリに差し替えていました。
# HOME を一時ディレクトリに差し替え
REAL_HOME="$HOME"
export HOME=$(mktemp -d /tmp/todo-test-home-XXXXXX)
mkdir -p "$HOME/.claude"
printf '{}' > "$HOME/.claude/todo-templates.json"
# ... テスト実行 ...
# HOME を戻す
export HOME="$REAL_HOME"
# 一時ディレクトリをクリーンアップ
rm -rf "$HOME/.claude" # ← ここ
最後の行、お気づきでしょうか。
HOME を本物に戻した後に $HOME/.claude を削除しています。つまり 本物の ~/.claude/ が消えます。
ログイン情報(.credentials.json)、セッション情報、設定ファイル、すべて消滅。
修正
一時ディレクトリのパスを別変数で保持するだけです。
REAL_HOME="$HOME"
FAKE_HOME=$(mktemp -d /tmp/todo-test-home-XXXXXX) # パスを保持
export HOME="$FAKE_HOME"
mkdir -p "$HOME/.claude"
printf '{}' > "$HOME/.claude/todo-templates.json"
# ... テスト実行 ...
export HOME="$REAL_HOME"
rm -rf "$FAKE_HOME" # 一時ディレクトリだけ消す
直して安心...と思ったら、テストを回すとまだテンプレートファイルが汚染されていました。
罠その2: Windows の os.homedir() は HOME を無視する
テスト対象のエンジン(Node.js)はテンプレートのパスを os.homedir() で計算しています。
function getTemplatePath() { return path.join(os.homedir(), '.claude', 'todo-templates.json'); }
Linux/macOS なら os.homedir() は HOME 環境変数を参照します。しかし Windows では USERPROFILE を参照します。
# Windows の bash (Git Bash) で検証
HOME=/tmp/fake node -e "console.log(require('os').homedir())"
# → C:\Users\saito ← HOME を無視!
テストで HOME を差し替えても、Node.js は本物のホームディレクトリを見続けていたのです。
修正
HOME と USERPROFILE の両方を差し替えます。
REAL_HOME="$HOME"
REAL_USERPROFILE="${USERPROFILE:-}"
FAKE_HOME=$(mktemp -d /tmp/todo-test-home-XXXXXX)
export HOME="$FAKE_HOME"
export USERPROFILE="$FAKE_HOME"
# ... テスト実行 ...
export HOME="$REAL_HOME"
if [ -n "$REAL_USERPROFILE" ]; then
export USERPROFILE="$REAL_USERPROFILE"
else
unset USERPROFILE
fi
rm -rf "$FAKE_HOME"
この USERPROFILE 問題は英語対応テスト(53件)を追加した作業と同日に発見しました。英語対応の実装詳細は「Claude Code の /todo コマンドを英語対応した(LANG_ENV=en)」で紹介しています。
おまけ: grep が絵文字を食えない
テスト修正中にもう1件バグを見つけました。
echo '## 📥 Inbox' | grep -aq '## 📥 Inbox' && echo MATCH || echo NO_MATCH # → NO_MATCH
Windows の bash 環境で grep が4バイト UTF-8 絵文字(📥 = U+1F4E5)をマッチできません。3バイト絵文字の ✅ (U+2705) は動くのに、4バイトの 📥 はダメ。grep -P(Perl正規表現)なら動きます。
テストの意図は「英語モードで日本語ヘッダーが出ない」ことの確認だったので、アプローチを変更しました。
# Before: 絵文字を含むパターンでマッチ(失敗する) assert_contains "en: Inbox header" "## 📥 Inbox" "$OUTPUT" # After: 日本語が含まれないことを確認 assert_not_contains "en: no JP in header" "受信トレイ" "$OUTPUT"
教訓
1. テストの後片付けは「差し替え前のパス」で行う
環境変数を差し替えたら、クリーンアップは差し替え先の変数で。復元後の変数を使うと本番環境を破壊します。
# NG export HOME="$REAL_HOME" rm -rf "$HOME/.claude" # ← 本物を消す # OK export HOME="$REAL_HOME" rm -rf "$FAKE_HOME" # ← 偽物を消す
2. Windows + Node.js では USERPROFILE も差し替える
os.homedir() の挙動は OS で異なります。
| OS | os.homedir() が参照する値 |
|---|---|
| Linux / macOS | HOME 環境変数 |
| Windows | USERPROFILE 環境変数 |
クロスプラットフォームのテストでは両方を差し替えましょう。
3. テストが何を壊すかは、テスト自身は教えてくれない
テストは全部グリーンでした。「363テスト通過」と表示された直後に、Claude Code が再ログインを求めてきます。テストの成功は「テスト対象が正しい」ことしか保証しません。テスト自体の副作用は別の話です。
この失敗が直接のきっかけで、V2 設計(テストスイートをモジュール分離して副作用を封じ込める設計)に発展しました。その経緯は「Claude Code の /todo スキルをOSS化するまで——V1モノリスからV2三層設計へ」で詳しく書いています。
まとめ
テストを安全に書くのは、テスト対象のコードを書くのと同じくらい大事です。特にファイルシステムや環境変数を操作するテストでは、「何を触って、何を戻すか」を慎重に設計する必要があります。
幸い、~/.claude/ は再ログインで復元できるものだったので実害は限定的でしたが、これが ~/.ssh/ や ~/.gnupg/ だったらと考えると背筋が冷えます。
Windows 固有の Claude Code 環境での初期設定 TIPS(TZ設定・USERPROFILE 等)は「Claude Code 実用TIPS集 — 知っていれば5秒、知らなければ1時間」にまとめています。
テスト設計を含むAIエージェントチームの構築・運用全体は、電子書籍にまとめています。
コードを書けない私が、AIに「チーム」を持たせるまで(序章・第1部無料)
この記事のテーマを深掘りした本
Claude Codeで作る1人エージェントチーム エンジニア向け。writer + reviewer の2体から始めるサブエージェント設計(序章無料)
シリーズ全6冊: Vol.1 作るまで / Vol.2 回すまで / Vol.3 書き続けるまで / Vol.4 仕組みを渡すまで / Vol.5 仕事を任せるまで / Vol.6 1人エージェントチーム