review 抓到一個沉默的失效:rollback 最後那一手「git 清不乾淨時把目錄本身也清掉」呼叫 rmSync,但把這段從 branch-prep 搬進 lib 時,import 留在了原處。那一行落在自己的 try/catch 裡,所以 ReferenceError 被吞掉——註解承諾的事從來沒發生過,而且測試全綠。 下一次重跑會撞上 WORKTREE_PATH_TAKEN,人看到的是一句與真正原因無關的錯誤。 順手收掉兩支腳本各寫一次的 origin 檢查(只有「為什麼需要它」那一句不同,由呼叫端給), 並把 lib 檔頭「負責六件事」改成七件——工作樹的一生現在整個住在這裡。 正本三處跟著改: - 查現況那一行補上 --dry-run。pr-watch 在 PR 已終止時會順手清掉工作樹,而「我想看一下 留言」不該把清理順便做掉——清不清理是使用者的決定。 - 拿掉「使用者堅持要繼續就繼續」:它與同一份正本的邊界(已合併或關閉時不繼續處理留言) 直接矛盾,而在一棵該被清掉的工作樹上改東西,那些改動不會進到任何 PR 裡。 - worktree-ensure 的指令補上 --path:目標專案多半不是當前目錄,而「不必先手動 cd」 正是這一步要解決的問題。 議題 #42 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
181 lines
7.6 KiB
JavaScript
181 lines
7.6 KiB
JavaScript
/**
|
|
* /sdlc-fix 的流程正本。
|
|
*
|
|
* 這一段有三件事只有正本做得到,腳本擋不住:分類必改/建議、不確定時停下來問、
|
|
* 以及最後那則摘要。寫漏任何一件,reviewer 的意見就會被靜靜跳過——而那正是這個指令
|
|
* 存在的理由。
|
|
*/
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { assertNeutralPrompt, readPrompt } from './helpers/prompt-doc.js';
|
|
|
|
const prompt = readPrompt('sdlc-fix');
|
|
const steps = prompt.slice(prompt.indexOf('## 1.'), prompt.indexOf('## 邊界'));
|
|
/** 定位那一步,與後面處理留言的步驟分開看 */
|
|
const locate = steps.slice(0, steps.indexOf('## 2.'));
|
|
|
|
test('第一步先看 PR 現況,終止狀態就停下來不白做工', () => {
|
|
assert.match(locate, /pr-watch\.js/);
|
|
assert.match(locate, /terminal/);
|
|
assert.match(locate, /不要繼續處理留言/);
|
|
assert.match(locate, /白做工/, '要說明為什麼停:那棵工作樹已經該被清掉了');
|
|
});
|
|
|
|
test('查現況帶 --dry-run,看一下留言不該順手清掉工作樹', () => {
|
|
const 指令 = locate.slice(locate.indexOf('pr-watch.js'));
|
|
assert.match(指令.slice(0, 120), /--dry-run/);
|
|
assert.match(locate, /清不清理是使用者的決定/);
|
|
});
|
|
|
|
test('三個建議動作的意思都交代了,不留一個讓 agent 自由發揮', () => {
|
|
for (const 值 of ['nothing-to-do', 'cleanup', 'blocked-dirty']) {
|
|
assert.match(locate, new RegExp(值));
|
|
}
|
|
});
|
|
|
|
test('工作樹由工作包推導,不要求使用者自己切目錄', () => {
|
|
assert.match(locate, /worktree-ensure\.js/);
|
|
assert.match(locate, /--path/, '目標專案不是當前目錄時要指得出來,否則會找錯 repo');
|
|
assert.match(locate, /branch/, '輸入是 pr-watch 給的分支名');
|
|
assert.match(locate, /不必也不該由使用者自己去記/);
|
|
assert.match(locate, /都在那棵工作樹裡做/);
|
|
});
|
|
|
|
test('不存在就重建,並說明那是常態不是例外', () => {
|
|
assert.match(locate, /常態不是例外/);
|
|
assert.match(locate, /換一台機器/, '要說明為什麼工作樹常常不在');
|
|
assert.match(locate, /同一套建立方式/, '重建不另寫一套');
|
|
});
|
|
|
|
test('兩種擋下來的情況各自寫明處置,且不替使用者決定', () => {
|
|
assert.match(locate, /BRANCH_NOT_FOUND/);
|
|
assert.match(locate, /WORKTREE_PATH_TAKEN/);
|
|
assert.match(locate, /不要自己刪/);
|
|
});
|
|
|
|
test('邊界擋住「回主工作區處理留言」與「PR 結束了還繼續改」', () => {
|
|
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
|
|
assert.match(boundary, /不在主工作區處理留言/);
|
|
assert.match(boundary, /已經合併或關閉時不繼續處理留言/);
|
|
});
|
|
|
|
test('正本平台中立,description 前綴正確', () => {
|
|
assertNeutralPrompt(prompt, 'sdlc-fix');
|
|
});
|
|
|
|
test('指名兩支腳本,順序為先讀再回', () => {
|
|
const order = ['pr-comments.js', 'pr-reply.js'];
|
|
const positions = order.map((name) => steps.indexOf(name));
|
|
assert.equal(positions.every((p) => p >= 0), true, '兩支腳本都要被指名');
|
|
assert.deepEqual([...positions].sort((a, b) => a - b), positions);
|
|
});
|
|
|
|
test('三類留言都點名,且交代行內的 diff 要看', () => {
|
|
for (const kind of ['一般留言', 'review 總評', '行內留言']) {
|
|
assert.match(steps, new RegExp(kind), `缺少:${kind}`);
|
|
}
|
|
assert.match(steps, /不要略過不看/);
|
|
});
|
|
|
|
test('三類的標記機制都交代了,並說明 +1 要是自己打的', () => {
|
|
assert.match(steps, /三類都標記得了/);
|
|
assert.match(steps, /resolve/);
|
|
assert.match(steps, /自己打的/);
|
|
assert.match(steps, /我同意/, '要說出為什麼別人的 \+1 不算');
|
|
});
|
|
|
|
test('標不了的那幾則要在摘要裡單獨點出來', () => {
|
|
assert.match(steps, /可標記/);
|
|
assert.match(steps, /單獨點出來|單獨\n? 列出來|單獨列出來/);
|
|
});
|
|
|
|
test('留言指向的程式碼已被改掉時的處置有交代', () => {
|
|
assert.match(steps, /位置對不上/);
|
|
assert.match(steps, /不要硬試/);
|
|
});
|
|
|
|
// ── 分類 ───────────────────────────────────────────────────────────
|
|
|
|
test('必改與建議各有判斷依據,不是只給兩個詞', () => {
|
|
assert.match(steps, /\*\*必改\*\* — .{10,}/);
|
|
assert.match(steps, /\*\*建議\*\* — .{10,}/);
|
|
});
|
|
|
|
test('分類結果要先呈現給使用者再動手', () => {
|
|
assert.match(steps, /先呈現給使用者/);
|
|
});
|
|
|
|
test('拿不準時往必改那邊靠,並說明為什麼', () => {
|
|
assert.match(steps, /歸到必改/);
|
|
assert.match(steps, /代價不對稱/);
|
|
});
|
|
|
|
// ── 不確定就問 ─────────────────────────────────────────────────────
|
|
|
|
test('一次問一題,選項含手動輸入', () => {
|
|
assert.match(steps, /一次問一題/);
|
|
assert.match(steps, /手動輸入/);
|
|
});
|
|
|
|
test('該問的情況有列舉,不是一句「不確定就問」', () => {
|
|
const section = steps.slice(steps.indexOf('## 4.'), steps.indexOf('## 5.'));
|
|
const bullets = section.match(/^- /gm) ?? [];
|
|
assert.ok(bullets.length >= 3, `該問的情況要列得出來,只找到 ${bullets.length} 條`);
|
|
assert.match(section, /推了新 commit/, '位置對不上是最常見的一種,要點名');
|
|
});
|
|
|
|
test('說明了硬改的代價', () => {
|
|
assert.match(steps, /比多問一題貴得多/);
|
|
});
|
|
|
|
// ── 逐則處理 ───────────────────────────────────────────────────────
|
|
|
|
test('要一則一則回,不是全部改完才一起回', () => {
|
|
assert.match(steps, /不要全部改完才一起回/);
|
|
assert.match(steps, /中途斷掉/);
|
|
});
|
|
|
|
test('三類與 --kind 的對應寫出來了,並警告 id 各自獨立', () => {
|
|
assert.match(steps, /行內.*inline/);
|
|
assert.match(steps, /一般.*general/);
|
|
assert.match(steps, /總評.*review/);
|
|
assert.match(steps, /id 各自獨立/);
|
|
});
|
|
|
|
test('回覆要說出做了什麼,且決定不改的也要回', () => {
|
|
assert.match(steps, /不是「已修正」/);
|
|
assert.match(steps, /決定不改的也要回/);
|
|
assert.match(steps, /沉默會讓 reviewer 以為被忽略/);
|
|
});
|
|
|
|
test('回覆失敗就不標記的理由有寫', () => {
|
|
assert.match(steps, /謊稱處理過/);
|
|
});
|
|
|
|
test('寫入前要求先試跑', () => {
|
|
assert.match(steps, /--dry-run/);
|
|
});
|
|
|
|
// ── 摘要 ───────────────────────────────────────────────────────────
|
|
|
|
test('摘要要逐則列出,並單獨點出沒有記號的那幾則', () => {
|
|
const section = steps.slice(steps.indexOf('## 6.'));
|
|
assert.match(section, /逐則一行/);
|
|
assert.match(section, /沒有留下記號的那幾則/);
|
|
assert.match(section, /以為它們被跳過/, '要說出為什麼得單獨列');
|
|
});
|
|
|
|
test('摘要不自動張貼', () => {
|
|
assert.match(steps, /不自動張貼/);
|
|
});
|
|
|
|
// ── 邊界 ───────────────────────────────────────────────────────────
|
|
|
|
test('邊界列出不做的事,含不動 review 狀態', () => {
|
|
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
|
|
assert.match(boundary, /不改與留言無關的程式碼/);
|
|
assert.match(boundary, /不跳過任何一則留言/);
|
|
assert.match(boundary, /不自動張貼修正摘要/);
|
|
assert.match(boundary, /不動 PR 的 review 狀態/);
|
|
});
|