Cursorのバグが通常のバグと違う理由

同じ変更を人が書くとき、その人はエディタからは見えない文脈を踏まえています。設定パネルはキャッシュから読み込んでいるので無効化が必要なこと。ファイルが選択されるまでアップロードボタンは無効であること。あるエンドポイントは該当データがないとき404ではなく空の配列を返すこと。Cursorが手がかりにできるのは、開いたファイルと入力したテキストだけです。その範囲の外側はすべて推測になります。

つまり、問題の形は構文の誤りではありません。局所的には正しく、全体としては誤っている変更です。関数は説明どおりに動きます。ただし呼び出されるタイミングが誤っていたり、状態を残したままにしたり、2スプリント前にAPIが返さなくなった形式を前提にしていたりします。

ユニットテストではこれを検出できません。ユニットテストは、コードが書かれたときと同じ前提のもとで書かれているからです。エージェントが両方を書いたのなら、両者は互いに一致し、そろってプロダクトと食い違います。

Cursorのバグが実際に生まれる場所

大半は次の3か所に集中します。

インターフェースの状態

  • フォームは送信されるのに、背後のリストが更新されない。

  • モーダルを閉じても、body要素にスクロールロックが残る。

  • リクエストの送信中もボタンが有効なままで、ダブルクリックするとレコードが2件作成される。

非同期の処理フロー

  • データが届く前に画面が描画され、その後再描画されない。

  • バックグラウンドジョブは開始されるが完了を待つ処理がなく、次のステップが古い値を読む。

  • エラー経路が黙って解決され、失敗した処理に対してユーザーには成功メッセージが表示される。

連携の境界

  • ペイロードは型定義には一致しているが、サービスが実際に受け付ける形式とは一致していない。

  • エージェントが見たセッションに認証情報があったというだけで、常に存在するものとして扱われている。

  • ページネーション、空の状態、レート制限が型システムの中でだけ扱われ、ほかのどこでも扱われていない。

これらに共通するのは、アプリケーションを実際に動かして使ってみなければ見えない、という点です。差分を読んでも表には出てきませんし、ブラウザを一度も開かないテストスイートでも同じです。

マージ前にCursorのバグを検出する方法

検証は、デプロイ済みのアプリケーションに対して、人が操作するのと同じやり方で行う必要があります。そして結果は、エージェントがそのまま動ける形で返ってこなければなりません。そうでなければ、バグを見つけたとしても、直すのは結局あなたです。

満たすべき条件は3つです。どれも難しくはありませんが、1つでも欠けるとループが漏れます。

エージェント自身が検証の仕方を知っていること

セットアップでは、コーディングエージェント自体に検証用のスキルをインストールします。これにより、READMEから推測するのではなく、テストの作成・実行・切り分けの方法をエージェントが把握します。エディタが既に参照している場所に指示ファイルを書き出すため、Cursorは .cursor/rules/ から自動的に読み込みます。これは、Claude Codeが自身のスキルディレクトリを参照するのと同じ仕組みです。対応しているエディタは8種類あり、全員が同じエディタを使っているとは限らないチームでは、この点が効いてきます。

ターミナル

npm install -g @testsprite/testsprite-cli
testsprite setup

ローカルに何もインストールしたくない場合は、TestSpriteのダッシュボードから同じことを行えます。CLIで実行できるその他の操作はすべて、こちらにまとめられています: CLIリポジトリ

チェックはモックではなく、デプロイ済みのアプリに対して実行する

変更が反映されている環境を実行対象に指定します。エージェントはアプリケーションを開き、ユーザーと同じように挙動をひととおり操作し、サンドボックス内のアサーションが満たされたかどうかではなく、実際に何が起きたかを報告します。モックからはこのシグナルを得られません。だからこそ、ユニットテストはグリーンなのに機能は壊れている、という状態が平然と両立するのです。

失敗が、エージェントの使える形で返ってくること

ループが閉じるかどうかを決めるのがこの部分です。失敗がダッシュボード上のスクリーンショットとして届くなら、人がそれを読み、解釈し、プロンプトを書かなければなりません。一方、何を試し、アプリが何を返し、どこで食い違ったのかが、それ自体で筋の通った1つのまとまりとして届けば、エージェントはそれを受け取ってそのまま動けます。次のイテレーションは、証拠についてのあなたの説明ではなく、証拠そのものから始まります。

誰も覚えていなくても実行される仕組みにする

手動で実行するチェックは、急いでいる日には飛ばされます。そして、まさにその日にこそ必要になります。自動化する方法は2つあり、どちらが向いているかは、パイプラインを誰が管理しているかによります。

  • GitHub App は、TestSpriteのダッシュボードで設定するWebhookです。パイプラインが既に生成しているデプロイイベントを検知するため、リポジトリ側は何も変わりません。

  • GitHub Actions は、ターミナルから設定し、自分のワークフローの中にステップとして組み込みます。

設定を触ることがない人にも、この仕組みは理解しておく価値があります。この連携は、アプリケーションのビルドもデプロイも行わず、ワークフローファイルの追加や編集もしません。パイプラインが既に生成しているデプロイイベントを検知し、「新しいビルドがこのURLで公開された」という知らせを実行開始の合図として扱います。結果は、プルリクエストへのコメントやコミットへのチェックとして、作業しているその場所に返ってきます。

役に立つかどうかは2つの判断で決まります。どちらも設定ではなく、判断の問題です。

  • どの時点を「準備完了」とみなすか。 ビルドの開始時ではなく、デプロイが公開された後に発火するイベントを選んでください。早すぎるイベントは、まだ立ち上がっていないURLに向けて実行を開始させ、コードとは無関係な理由ですべてのテストが失敗します。セットアップがうまくいかない原因として、これが最も多いパターンです。

  • チェックを参考情報とするか、必須とするか。 プルリクエストへのコメントは情報を伝えるだけです。必須チェックはマージをブロックします。まずは1週間ほど参考情報として運用し、何を検出できるかを見てから決めてください。誰もまだ信頼していない初日から必須にするのは、有用なチェックが無効化されていく典型的な流れです。

プレビュー環境をVercelやNetlifyのようなプラットフォームに置いている場合の実務的な注意点として、プレビューURLの命名規則はホストごとに異なります。そのためこの連携では、固定のアドレスではなくパターンを指定し、実行ごとにブランチ名やプルリクエスト番号が埋め込まれます。

既存のテストとの関係

これは稼働中のプロダクトに対する挙動の検証であり、高速なローカルのユニットテストを置き換えるものではなく、補完するものです。ロジックの検証はユニットテストに任せ、その構造上どうしても見えない部分、つまり実際のユーザーが操作したときに機能が動くかどうかを、こちらでカバーします。

より広い文脈について

ここまでの話はCursorに限りません。同じギャップは、人が読むより速くコードを書くアシスタントすべてに現れます。だからこそ、検証のステップは一度作ってエディタをまたいで使い回す価値があるのです。複数のエージェントが同じアプリケーションを構築した公開リーダーボードでは、この検証ループを備えた状態で、参加した中で最も安価なモデルが最も正しく動くアプリを仕上げ、そのコストは最も高価なエントリーの半分でした。ここから得られる教訓は、あるモデルが優れているということではありません。機能するフィードバックのシグナルを持つモデルは、それを持たないより強力なモデルに勝る、ということです。

CursorのループのなかでTestSpriteが担う役割

TestSpriteは検証を担う側です。Cursorが変更を書き、TestSpriteがデプロイ済みのアプリを開き、ユーザーと同じように挙動をたどり、実際に何が起きたかを報告します。どちらも相手の仕事をしようとはしていません。その分担こそが要点です。変更を書いた本人は、それを確認する当事者としては適任ではないからです。

Cursorが書いたコードを出荷するチームにとって、そこから3つのことが導かれます。まず、テストを書く時間を誰かが確保できるかどうかにカバレッジが左右されなくなります。テストケースはプロダクトから生成され、自然言語で調整できるからです。次に、見た目だけのリデザインでスイートが赤くなることがなくなります。各ステップはDOM上の経路ではなく、意図として表現されているからです。そして失敗は、エージェントがそのまま扱える1つのまとまりとして返ってきます。次のイテレーションは、あなたの説明ではなく証拠から始まります。

初日から期待できるのは、壊れたら困るフローがすべてのプルリクエストで実行され、どこで食い違ったのかを示す結果が返ってくることです。一方で、コードをレビューするものではなく、高速なローカルのユニットテストを置き換えるものでもありません。その両方と並行して機能します。

機能が壊れているのに、Cursorが書いたテストが通るのはなぜですか?

テストとコードが同じ前提から生まれているからです。あるエンドポイントは空の配列を返すとエージェントが考え、その前提でハンドラーとテストの両方を書いたなら、両者は互いに一致します。この食い違いを断ち切れるのは、実際のサービスに対して実際のアプリケーションを動かすことだけです。

これを導入するにはQAチームが必要ですか?

必要ありません。セットアップはCLIコマンドの実行とGitHub Appのインストールだけです。テスト結果を見るのは開発者だけ、というチームのために設計されています。

ソースコードへのアクセスは必要ですか?

検証は、デプロイ済みのアプリケーションに対して、そのインターフェースを通じて実行されます。GitHubの書き込み権限は、結果をプルリクエストやコミットに返すために使うものであり、コードをプッシュしたりワークフローファイルを変更したりするためのものではありません。

意図的にインターフェースを変更した場合、テストはどうなりますか?

特定の要素ではなくビジネスフローに紐づいたテストは、フローが変わらないリデザインであればそのまま生き残ります。フロー自体が変わった場合、それは新しい挙動であり、新規または更新したテストケースが必要になります。そのケースは、変更内容からエージェントが生成できます。

メインのスイートに影響を与えずに、ブランチで実行できますか?

テストはgitのブランチではなくアプリケーションに紐づくため、正となるスイートは1つです。ブランチごとにチェックしたい場合はプルリクエストをトリガーに、マージのたびに共有環境を1つ検証したい場合はpushをトリガーに選んでください。

まとめ

差分を読むのはやめて、アプリを動かしましょう。

Cursorのバグは、状態・タイミング・連携の境界に潜みます。そこはまさに、静的に読むことやモックを使ったテストでは届かない場所です。検証の手段をエージェントに持たせ、デプロイ済みのビルドに向けて実行し、誰かがマージする前に、その変更が動くかどうかをプルリクエストに語らせましょう。