勾選的測試全部繞著「會不會改錯行」打轉,五種都是 code review 抓出來的實際缺陷: 圍欄裡長得像 checkbox 的那一行不會被改到、不同段落的同一句話靠 --section 分得開、 大寫 [X] 重跑是 no-op、方框後沒有空白照樣勾得到、沒有方框的項目給的是指路的錯誤 而不是謊報已勾過。 另外釘住抽取端與勾選端的一致性:wp-extract 交得出來的每一種 raw,--tick 都要收得下。 patchOf 收進 helpers——它先前在三個測試檔裡各有一份一模一樣的定義。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
183 lines
7.9 KiB
JavaScript
183 lines
7.9 KiB
JavaScript
/**
|
|
* 正本第一段「領取與開工準備」的規則。
|
|
*
|
|
* 這些檔案是文件不是程式,但它們是指令實際交付的東西:決策表寫錯,被鎖擋下來的人
|
|
* 就會得到錯的下一步;邊界寫漏,第一段就會去做後面幾段的事。靠人記不牢,用測試釘住。
|
|
*/
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { assertNeutralPrompt, readPrompt } from './helpers/prompt-doc.js';
|
|
|
|
const prompt = readPrompt('sdlc-feat');
|
|
/** 各段的內容分開切,避免把別段的字樣誤認成這一段的規則 */
|
|
const phase1 = prompt.slice(prompt.indexOf('## 第一段'), prompt.indexOf('## 第二段'));
|
|
const phase2 = prompt.slice(prompt.indexOf('## 第二段'), prompt.indexOf('## 邊界'));
|
|
|
|
test('正本平台中立,description 前綴正確', () => {
|
|
assertNeutralPrompt(prompt, 'sdlc-feat');
|
|
});
|
|
|
|
test('第一段指名三支腳本,順序為先讀再領再備分支', () => {
|
|
const order = ['wp-extract.js', 'claim.js', 'branch-prep.js'];
|
|
const positions = order.map((name) => phase1.indexOf(name));
|
|
assert.equal(positions.every((p) => p >= 0), true, '三支腳本都要被指名');
|
|
assert.deepEqual([...positions].sort((a, b) => a - b), positions, '領取之前要先讀得懂這顆在做什麼');
|
|
});
|
|
|
|
test('未處理留言不是 0 時要先停下來提示整併', () => {
|
|
assert.match(phase1, /未處理留言數/);
|
|
assert.match(phase1, /先停下來/);
|
|
assert.match(phase1, /sdlc-sync/);
|
|
});
|
|
|
|
test('領取鎖的四種狀態各自交代了下一步,含放行那一種', () => {
|
|
for (const code of ['CLAIMED_BY_OTHER', 'STOPWATCH_ON_THIS_ISSUE', 'STOPWATCH_ON_OTHER_ISSUE']) {
|
|
assert.match(phase1, new RegExp(code), `${code} 要出現在決策表裡`);
|
|
}
|
|
assert.match(phase1, /沒有鎖/, '第四種狀態(放行)也要在表上,否則只剩擋的那幾種');
|
|
assert.match(phase1, /不要繞過去/, '被擋下來的處置要明講,不能靠 agent 自由發揮');
|
|
});
|
|
|
|
test('缺標籤是前置條件,不混進領取鎖的四種狀態裡', () => {
|
|
const table = phase1.slice(phase1.indexOf('| 狀態'), phase1.indexOf('碼錶一律由使用者自己停'));
|
|
assert.equal(table.includes('LABEL_NOT_FOUND'), false, '它不是鎖的狀態,別讓四種變五種');
|
|
assert.match(phase1, /LABEL_NOT_FOUND/, '但仍要交代它,否則使用者不知道怎麼辦');
|
|
});
|
|
|
|
test('工作包跨多個 repo 時怎麼開分支,有交代', () => {
|
|
assert.match(phase1, /repos/);
|
|
assert.match(phase1, /有多顆時逐一確認/);
|
|
});
|
|
|
|
test('工作區不乾淨時的處置寫明了,且不替使用者決定', () => {
|
|
assert.match(phase1, /DIRTY_WORKTREE/);
|
|
assert.match(phase1, /不要自己選/);
|
|
});
|
|
|
|
test('碼錶只由使用者自己停,並說明為什麼不代勞', () => {
|
|
assert.match(phase1, /由使用者自己停/);
|
|
assert.match(phase1, /工時記錯地方/);
|
|
});
|
|
|
|
test('來源分支要問過使用者,且一次一題、附理由與手動輸入', () => {
|
|
assert.match(phase1, /一次問一題/);
|
|
assert.match(phase1, /手動輸入/);
|
|
assert.match(phase1, /不要替他決定|不要替使用者決定/);
|
|
});
|
|
|
|
test('翻譯規則釘住 kebab 與 40 字元上限,並舉出可照抄的例子', () => {
|
|
assert.match(phase1, /kebab/);
|
|
assert.match(phase1, /40/);
|
|
assert.match(phase1, /wp-extract-contract/, '要有一個真的例子,不要只說規則');
|
|
assert.match(phase1, /不要把長句截斷/);
|
|
});
|
|
|
|
test('--type 什麼時候要給、什麼時候不能給,寫清楚了', () => {
|
|
assert.match(phase1, /`--type` 只在來源是開發分支時要給/);
|
|
assert.match(phase1, /沿用來源/);
|
|
});
|
|
|
|
test('兩處「不覆蓋他人進度」的保證都有寫出來', () => {
|
|
assert.match(phase1, /pull 而不是重建/);
|
|
assert.match(phase1, /接上去而不是蓋掉/);
|
|
});
|
|
|
|
test('三支腳本的寫入都要求先試跑', () => {
|
|
const dryRuns = phase1.match(/--dry-run/g) ?? [];
|
|
assert.ok(dryRuns.length >= 2, `兩支寫入型腳本各要先試跑,只找到 ${dryRuns.length} 處`);
|
|
});
|
|
|
|
test('邊界把第一段不做的事分開列,且明講不寫本機狀態檔', () => {
|
|
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
|
|
assert.match(boundary, /不改任何一行程式碼/);
|
|
assert.match(boundary, /不勾待辦/);
|
|
assert.match(boundary, /不開 PR/);
|
|
assert.match(boundary, /不自行建立標籤/);
|
|
assert.match(boundary, /不寫任何本機狀態檔/);
|
|
assert.match(boundary, /換一台機器或換一個 agent/, '要說明為什麼不留狀態檔');
|
|
});
|
|
|
|
// ── 第二段:逐項實作 ───────────────────────────────────────────────
|
|
|
|
test('第二段指名兩份規則正本,且在改檔之前就要讀', () => {
|
|
assert.match(phase2, /references\/coding-standards\.md/);
|
|
assert.match(phase2, /references\/comment-styles\.md/);
|
|
const readAt = phase2.indexOf('coding-standards.md');
|
|
const implementAt = phase2.indexOf('### 8.');
|
|
assert.ok(readAt < implementAt, '讀規則要排在動手實作之前');
|
|
});
|
|
|
|
test('認不出語言就停下來問,不自行假設', () => {
|
|
assert.match(phase2, /認不出語言就停下來問/);
|
|
assert.match(phase2, /不要猜/);
|
|
});
|
|
|
|
test('不把規範寫進目標專案的檔案', () => {
|
|
assert.match(phase2, /不寫進目標專案的任何檔案/);
|
|
});
|
|
|
|
test('規則不在正本裡複述,只指名去哪裡讀', () => {
|
|
assert.match(phase2, /這裡不複述/);
|
|
assert.match(phase2, /兩份各自演化/, '要說出複述的代價,否則下一個人還是會抄過來');
|
|
});
|
|
|
|
test('資料範例優先取自 MCP,取不到要註明未經驗證', () => {
|
|
assert.match(phase2, /優先從 MCP 取得/);
|
|
assert.match(phase2, /由邏輯推理、未經驗證/);
|
|
});
|
|
|
|
test('回報要點出哪些範例是推理來的', () => {
|
|
const report = phase2.slice(phase2.indexOf('### 11.'));
|
|
assert.match(report, /哪些資料範例是推理來的/);
|
|
});
|
|
|
|
test('--section 一定要給,並說明不給會怎樣', () => {
|
|
assert.match(phase2, /--section/);
|
|
assert.match(phase2, /一定要給/);
|
|
assert.match(phase2, /分不出要勾哪一個/);
|
|
});
|
|
|
|
test('過程不打斷:不逐項徵求同意,只印進度', () => {
|
|
assert.match(phase2, /過程不打斷/);
|
|
assert.match(phase2, /不該按二十次同意/);
|
|
assert.match(phase2, /只印進度/);
|
|
assert.match(phase2, /\[3\/12\]/, '要給一個看得出長相的進度格式,不要只說「印進度」');
|
|
});
|
|
|
|
test('真正該停下來問的情況有列舉,不是一律不問', () => {
|
|
assert.match(phase2, /真正需要停下來問的只有三種/);
|
|
assert.match(phase2, /範圍邊界/);
|
|
});
|
|
|
|
test('勾選用 issue-update --tick,且明講要用抽取契約給的 raw', () => {
|
|
assert.match(phase2, /--tick/);
|
|
assert.match(phase2, /不要自己拼那一行/);
|
|
assert.match(phase2, /raw/);
|
|
});
|
|
|
|
test('四種勾不動的錯誤各自交代了下一步', () => {
|
|
for (const code of ['RAW_NOT_FOUND', 'RAW_AMBIGUOUS', 'NOT_A_CHECKBOX', 'SECTION_NOT_FOUND']) {
|
|
assert.match(phase2, new RegExp(code), `${code} 要出現在錯誤表裡`);
|
|
}
|
|
assert.match(phase2, /重跑 `wp-extract`/);
|
|
assert.match(phase2, /不要自己改寫議題/, '議題內容是使用者的,agent 不該代為修改');
|
|
});
|
|
|
|
test('不為了勾選留留言,並說明為什麼', () => {
|
|
assert.match(phase2, /不要為了勾選在議題上留留言/);
|
|
assert.match(phase2, /洗版/);
|
|
});
|
|
|
|
test('中斷後重跑從 Gitea 的勾選狀態接續,且不看本機檔案', () => {
|
|
assert.match(phase2, /不看任何本機檔案/);
|
|
assert.match(phase2, /done` 已經是 `true`|done.*true/);
|
|
assert.match(phase2, /no-op/, '要說明重複勾選是安全的,否則會有人先查再勾');
|
|
});
|
|
|
|
test('邊界把第二段不做的事也列出來', () => {
|
|
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
|
|
assert.match(boundary, /第二段只實作與勾選/);
|
|
assert.match(boundary, /不提交、不開 PR、不停錶/);
|
|
assert.match(boundary, /不改與待辦無關的程式碼/);
|
|
});
|