この記事で分かること:
- "正常終了" が実は動いていない 5 つの典型パターン
- 各パターンの検知方法とコード例(JavaScript / Playwright)
- 夜間バッチに任せる前にやっておく最低限の検証
自動化スクリプトが「成功しました」と出た。終了コードは 0。ログにエラーはない。でも 3 週間後に確認したら、何も起きていなかった——こういうことが何度かあった。
エラーが出る失敗は見つけやすい。問題は「エラーなしに失敗している」状態で、これが積み重なると「自動化してある」という思い込みが生まれる。気づくのが遅れた 5 つのパターンを整理する。
パターン 1:非表示ボタンをクリックして「完了」と報告していた
Playwright で note.com の「フォロー」ボタンを押すコードを書いた。ページ内に同じテキストの button が複数あるとき、テキストでの絞り込みは非表示の要素も対象にする。Playwright は非表示要素への click() を実行してもデフォルトではエラーを出さない。
画面では「フォローしました」と出ていた。しかし API で isFollowing を確認すると false だった。これが 2 週間続いていた。
// Bad: クリック後に画面テキストだけ見る
await page.locator('button:has-text("フォロー")').first().click();
const text = await page.locator('.follow-button').textContent();
if (text === 'フォロー中') console.log('成功'); // 非表示ボタンを押していても成功と出る
// Good: :visible を追加し、API で状態を直接確認する
await page.locator('button:has-text("フォロー"):visible').click();
const res = await fetch('https://api.example.com/creators/' + urlname, { headers });
const { isFollowing } = await res.json();
if (!isFollowing) throw new Error('フォロー未完了: ' + urlname);
要点:副作用(フォロー状態、投稿の有無、DB への書き込み)は、アクション後に外部 API やストレージで直接確認する。画面テキストはサーバー側の状態と必ずしも同期していない。
パターン 2:引数省略でデフォルト対象だけが動いていた
5 つのアカウントに毎日 1 本ずつ記事を投稿するスクリプトを Windows タスクスケジューラに登録した。数週間後に確認すると、main アカウントだけに投稿が続いており、他 4 アカウントは数週間更新が止まっていた。
# タスクスケジューラに登録していたコマンド
node post-from-pool.mjs
# --account を省略 → デフォルト = main のみ処理
スクリプト内で --account を省略したときのデフォルトを 'main' にしていた。1 アカウント分は正常に処理されるため、終了コードも 0 でログにもエラーはない。「毎日 5 アカウントに投稿している」という認識だったが、実際に動いていたのは 5 分の 1 だった。
// 修正: 処理対象を必ずログに出す
const account = process.argv
.find(a => a.startsWith('--account='))?.split('=')[1] ?? 'main';
console.log('[start] account=' + account);
// ... 処理 ...
console.log('[done] account=' + account + ' noteId=' + result.noteId);
要点:デフォルト引数は「何も指定しなかった場合」の仕様だが、タスク登録時に意図して省略したのかどうかは後から見てもわからない。アカウント・対象・パラメータは全部明示で書く。
パターン 3:遅延レンダリングを「動いていない」と誤判定した
note.com の /likes ページを Playwright で調査したとき、a[href*="/n/n"] を count() したら 0 だった。「この機能は動いていないのでは」という判断をしそうになった。
実際にはデータは取得済みで、画面への反映がマウスホイールのスクロールをトリガーにしている仕様だった。初期ロード直後に DOM を数えると 0 件になる。スクロールして 500ms 待つと 30 件以上が描画される。
// 誤判定
await page.waitForLoadState('networkidle');
const count = await page.locator('a[href*="/n/n"]').count();
// → 0(実際は 30 件以上)
// 修正:スクロール後に再確認
await page.mouse.wheel(0, 400);
await page.waitForTimeout(500);
const countAfterScroll = await page.locator('a[href*="/n/n"]').count();
// → 30 件以上
さらにこのページには「もっとみる」ボタンが 2 個あった。1 つはプロフィール文の展開、もう 1 つが記事一覧の追加読み込み。テキストが完全に同一で、DOM 順で非表示側が先に来る。.first() を使うと毎回タイムアウトになる。
// Bad: .first() は非表示の要素を掴む
await page.getByText('もっとみる', { exact: true }).first().click();
// Good: .last() または件数確認してから
const buttons = page.getByText('もっとみる', { exact: true });
if (await buttons.count() >= 2) {
await buttons.last().click();
}
要点:SPA・遅延描画ページでは「要素数 0」が「データなし」を意味しない。スクロール後に再確認する。同名要素が複数ある場合は .first() の前提を確認する。
パターン 4:タスクスケジューラ経由でシェル変数が常に空になった
Windows タスクスケジューラから wsl.exe 経由でシェルスクリプトを実行するとき、引数フィールドに直接ループを書いていた。$id が常に空文字列になっていたため、スクリプトはデフォルト動作で正常終了し続けていた。
# NG: タスクスケジューラの引数フィールドに変数展開を書く
# → $id が常に空になる(原因未特定だが複数環境で再現)
wsl.exe -d Ubuntu -- bash -lc "for id in 001 002 003; do node script.mjs $id; done"
# OK: .sh ファイルに切り出す
# run-script.sh:
# for id in 001 002 003; do
# node script.mjs $id ← ファイルの中なら正常に展開される
# done
wsl.exe -d Ubuntu -- bash -lc "cd /path/to/project && ./run-script.sh"
要点:wsl.exe に渡す文字列中のシェル変数は信用しない。変数展開は .sh ファイルで行い、タスクには .sh ファイルのパスを渡す。タスクスケジューラの引数フィールドにはリテラル値のみを書く。
パターン 5:API の使用量フィールドが常に 0 だった
claude -p --output-format json の出力で usage.server_tool_use.web_search_requests を監視していたが、実際にWeb検索が走っているセッションでもこのフィールドは常に 0 だった。
Claude Code の WebSearch ツールは Anthropic API のサーバー側 web_search ツールとは別実装で、クライアント側ツールとしての実行分は server_tool_use に記録されない。
// 信用できない
const searches = result.usage?.server_tool_use?.web_search_requests ?? 0;
// → 常に 0
// 代わりに使う指標
const turns = result.num_turns;
const cacheRead = result.usage?.cache_read_input_tokens ?? 0;
console.log('turns=' + turns + ' cacheRead=' + cacheRead.toLocaleString());
// ターン数にほぼ比例してコストが増える
さらに、トップレベルの num_turns 自体も実態を反映しないことがある。内部処理が大量のトークンを消費していても num_turns は 3 になるケースがあった。コストと乖離する行は生 JSON の modelUsage を確認する。
まとめ
| パターン | 気づくのが遅れた理由 | 検知の鍵 |
|---|---|---|
| 非表示ボタンクリック | エラーなし・画面テキストは一致 | API で副作用を直接確認 |
| 引数のデフォルト | 1 件は正常処理されていた | 処理対象をログに必ず出力 |
| 遅延レンダリング | 要素 0 件を「機能なし」と誤判定 | スクロール後に再確認 |
| シェル変数の空展開 | タスクは正常終了コードを返した | 変数展開は .sh ファイル内のみ |
| 使用量フィールド | 監視している数字が常に 0 | modelUsage の内訳を参照 |
どれも「エラーが出なかった」から気づくのが遅れた。「正常終了した」と「期待通りに動いた」は別のことだ——この認識を持つだけで、次に問題が起きたとき調査の出発点が変わる。
皆さんの自動化で「成功と表示されていたのに後で気づいたら動いていなかった」パターン、他にもあれば教えてもらえると参考になります。
参考:noteでの関連記事 https://note.com/light_tern636/n/n101d6bb74f6a
この記事の内容で詰まったことや、自分の環境に応用したいケースはありますか?Copilot・AIエージェント構築の相談を受け付けています(ご連絡の際は「Qiitaを見た」とお書きください):https://coconala.com/services/4209036