為什麼 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 觸發。