為什麼 Cursor Bug 不是一般的 Bug

同樣一段修改,由人來寫時會帶著編輯器永遠看不到的脈絡。他們記得設定面板是從一份必須手動失效的快取讀資料、記得上傳按鈕在選好檔案之前是停用的、記得某個端點在查不到東西時回傳的是空陣列而不是 404。Cursor 依據的只有你打開的檔案和你輸入的文字,這個範圍以外的一切,都是猜的。

所以問題的形態不是語法寫錯,而是一段局部正確、整體錯誤的修改。函式做的事跟它宣稱的完全一致,但它被呼叫的時機不對,或是留下了某個沒清掉的狀態,又或者它預期的資料結構,是 API 兩個 sprint 前就不再回傳的樣子。

單元測試抓不到這種問題,因為單元測試所依據的假設,跟程式碼所依據的是同一套。如果兩者都是 agent 寫的,它們只會彼此一致,卻和你的產品不一致。

Cursor Bug 真正的來源

大部分都出自三個地方。

介面狀態

  • 表單送出了,但後面的清單沒有更新。

  • 彈出視窗關閉了,卻在 body 上留下沒解除的捲動鎖定。

  • 請求已經在送出中,按鈕卻仍然可以按,於是連點兩下就建立了兩筆資料。

非同步流程

  • 畫面在資料回來之前就先渲染了,之後再也沒有重新渲染。

  • 背景工作啟動了,卻沒有任何人等它完成,於是下一步讀到的是過期的值。

  • 錯誤路徑無聲地被當成處理完畢,使用者因此在一件失敗的工作上看到成功訊息。

整合邊界

  • payload 符合型別定義,卻不符合服務實際接受的格式。

  • 因為 agent 看到的那個 session 裡帶著驗證,就假設驗證一定存在。

  • 分頁、空狀態和速率限制只在型別系統裡被處理過,其他地方一律沒有。

這些問題的共通點是:你只有真的把應用程式跑起來、實際操作它,才看得見。讀 diff 不會讓其中任何一個浮現,一套從不打開瀏覽器的測試也不會。

如何在 Cursor Bug 被合併之前抓到它們

驗證必須對著已部署的應用程式進行,操作方式要跟真人一樣,而結果必須以 agent 能直接接手處理的形式回傳。否則你只是找到了 bug,動手修的還是你。

有三件事必須同時成立。每一件都不難,但只要少了任何一件,這個迴圈就會漏。

agent 必須知道怎麼驗證

安裝流程會把一套驗證 skill 裝進寫程式的 agent 本身,讓它知道如何建立、執行與分類測試,而不是從 README 去猜。它會把指令檔寫進你的編輯器原本就會去找的位置,也就是說,Cursor 會從 .cursor/rules/ 讀取它,就像 Claude Code 讀取自己的 skill 目錄一樣。目前支援八款編輯器,在不是所有人都用同一款編輯器的團隊裡,這一點很重要。

終端機

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

如果你不想在本機安裝任何東西,也可以直接在 TestSprite 儀表板做同樣的事。CLI 能做的其他所有事情,請見 CLI 儲存庫

這道檢查必須打到已部署的應用程式,而不是 mock

把一次執行指向這個修改實際上線的環境。agent 會打開應用程式,像使用者一樣把行為走過一遍,回報的是實際發生了什麼,而不是沙箱裡某個斷言有沒有成立。mock 產生不出這種訊號,這也是為什麼「單元測試全綠」和「功能壞掉」可以如此和平共存。

失敗必須以 agent 用得上的形式回來

這一段決定了這個迴圈能不能收得起來。如果失敗是以儀表板上的一張截圖呈現,就得有人去看它、解讀它,再寫一段 prompt。如果失敗是以一份前後一致的完整資料回來——嘗試了什麼、應用程式做了什麼、在哪裡開始偏離——agent 就能直接接手處理。下一輪迭代的起點是證據本身,而不是你對證據的轉述。

讓它自動發生,不必靠任何人記得要做

需要手動執行的檢查,就是你趕時間那天會略過的檢查,而那天正好是你最需要它的一天。要讓它自動化有兩種做法,選哪一種,取決於你的 pipeline 由誰掌管。

  • GitHub App 是你在 TestSprite 儀表板裡設定的一個 webhook。它會監聽你的 pipeline 本來就會發出的部署事件,所以你的儲存庫完全不用改動。

  • GitHub Actions 則是把這個步驟放進你自己的 workflow,從終端機完成設定。

即使你從來不碰設定,這套運作模型也值得理解。這個整合不會建置或部署你的應用程式,也不會新增或修改 workflow 檔案。它只監聽你的 pipeline 本來就會發出的部署事件,並把「新版本已經上線在這個 URL」當成啟動一次執行的訊號。結果會回到工作真正發生的地方:pull request 上的一則留言,或 commit 上的一項檢查。

有兩個決定會左右它有沒有用,而這兩個都是判斷,不是設定。

  • 哪一個時間點才算準備好。 選在部署上線之後才觸發的事件,不要選建置開始時觸發的那個。太早送達的事件會讓執行指向一個還沒起來的 URL,於是每一個測試都因為跟你的程式碼毫無關係的理由而失敗。這是整套設定最常出錯的地方。

  • 這道檢查是參考性質,還是具有強制力。 pull request 上的留言只是告知,必要檢查則會擋住合併。先以參考性質跑一週,看看它抓到什麼,再做決定。在還沒有人信任它的第一天就讓它具有強制力,正是一道有用的檢查最後被關掉的原因。

如果你的預覽環境跑在 Vercel 或 Netlify 這類平台上,有一個實務上的提醒:每一家服務商的預覽 URL 命名方式都不同,所以這個整合接受的是一組樣式,而不是固定的網址,再依每次執行填入對應的分支或 pull request。

它和你現有的測試如何並存

這是針對實際運行中的產品所做的行為驗證,因此它是本機快速單元測試的補充,而不是取代。邏輯的部分交給單元測試,把單元測試在結構上看不到的那件事留給它:當真實使用者操作時,這個功能到底能不能用。

關於更廣泛的模式

這些都不是 Cursor 獨有的。任何一個寫程式碼的速度快過人類閱讀速度的助理,都會出現同樣的落差,這也是為什麼驗證這一步值得做一次,然後在各種編輯器之間重複使用。在一場讓多個 agent 打造同一個應用程式的公開排行榜上,當這套驗證迴圈就位時,參賽陣容中最便宜的模型交出了最正確的應用程式,成本只有最貴那一項的一半。這件事要說的不是某個模型比較好,而是一個擁有有效回饋訊號的模型,會勝過一個沒有回饋訊號的更強模型。

TestSprite 在 Cursor 迴圈中的位置

TestSprite 負責的是驗證那一半。Cursor 寫出修改,TestSprite 打開已部署的應用程式,像使用者一樣把行為走過一遍,回報實際發生了什麼。兩者都不試圖搶對方的工作,而這個分工本身就是重點:一段修改的作者,不該是確認它的那個人。

對一個正在交付 Cursor 所寫程式碼的團隊來說,這帶來三個結果。測試涵蓋不再取決於有沒有人擠得出時間寫測試,因為案例是從你的產品生成、再用白話文細修的。純外觀的改版不再讓整套測試變紅,因為一個步驟代表的是一個意圖,而不是一條穿過 DOM 的路徑。失敗則會以一份 agent 能直接處理的完整資料回來,於是下一輪迭代的起點是證據,而不是你對證據的轉述。

第一天你該期待的,是那些壞掉會讓你很難堪的流程,在每一個 pull request 上都跑過一遍,並得到一個明確指出偏離所在的結果。它不會做的,是審查你的程式碼,或取代本機的快速單元測試,而是和這兩者並行運作。

功能明明壞了,為什麼 Cursor 自己寫的測試還會通過?

因為測試和程式碼出自同一套假設。如果 agent 認定某個端點會回傳空陣列,並依這個認定同時寫下處理邏輯和測試,兩者當然彼此一致。唯有讓真實的應用程式對上真實的服務跑一遍,才能打破這個僵局。

導入這套做法需要 QA 團隊嗎?

不需要。整個設定就是一道 CLI 指令,加上安裝 GitHub App。它就是為了那種「唯一會去看測試結果的人只有開發者」的團隊而設計的。

它需要存取我的原始碼嗎?

驗證是透過介面,對著你已部署的應用程式執行的。GitHub 的寫入權限只用來把結果回貼到 pull request 和 commit 上,不會推送程式碼,也不會更動 workflow 檔案。

如果介面是刻意改動的,測試會怎麼樣?

綁定在業務流程、而不是綁定在特定元素上的測試,在沒有改變流程的改版之後仍然有效。當流程本身改變時,那就是新的行為,需要一個新的或更新過的案例,而 agent 可以直接從這次的修改生成它。

我可以只在某個分支上跑,而不影響主要的測試集嗎?

測試屬於應用程式,而不屬於某個 git 分支,所以只會有一套正式的測試集。如果你想要逐分支的檢查,就選 pull request 觸發;如果你想要在每次合併後驗證同一個共用環境,就選 push 觸發。

簡短版

別再盯著 diff 看了,把應用程式跑起來。

Cursor Bug 藏在狀態、時序和整合邊界裡,而這些正是靜態閱讀和 mock 測試碰不到的地方。把驗證交到 agent 手上,讓它對準已部署的版本,並讓 pull request 在任何人合併之前,就告訴你這個修改到底能不能用。