From f0beb1ff643a3981875c5045531169d852a966ab Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 05:43:25 +0000 Subject: [PATCH] =?UTF-8?q?test(=E5=B7=A5=E4=BD=9C=E5=8C=85):=20=E9=87=98?= =?UTF-8?q?=E4=BD=8F=E6=A8=A1=E6=9D=BF=E8=88=87=E3=80=8C=E7=94=A2=E7=94=9F?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E5=8C=85=E3=80=8D=E9=82=A3=E4=B8=80=E6=AE=B5?= =?UTF-8?q?=E7=9A=84=E8=A6=8F=E5=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 模板的段落順序決定 wp-extract 解析得到什麼,待辦的巢狀寫法決定實作階段勾得到 哪一行——這兩件事寫死在測試裡,改動時才會被逼著一起改。 巢狀範例的斷言不比對固定縮排量,而是比對「最深的一層比最淺的深」,這樣重排 外層清單的縮排不會弄壞測試。 Co-Authored-By: Claude Opus 5 (1M context) --- test/work-package-assets.test.js | 131 +++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 test/work-package-assets.test.js diff --git a/test/work-package-assets.test.js b/test/work-package-assets.test.js new file mode 100644 index 0000000..e7416b3 --- /dev/null +++ b/test/work-package-assets.test.js @@ -0,0 +1,131 @@ +/** + * 工作包議題的模板,以及正本裡「產生工作包」那一段的規則。 + * + * 模板的段落順序決定 #9 的 wp-extract 解析得到什麼;待辦的巢狀寫法決定實作階段 + * 勾得到哪一行。這兩件事寫死在測試裡,改動時才會被逼著一起改。 + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { readPrompt, readTemplate } from './helpers/prompt-doc.js'; + +const template = readTemplate('work-package-issue'); +const prompt = readPrompt('sdlc-analyze'); +/** 第二段的內容,避免把第一段的字樣誤認成這一段的規則 */ +const phase2 = prompt.slice(prompt.indexOf('## 第二段')); + +/** 工作包議題的九個段落,順序即議題裡的順序 */ +const SECTIONS = [ + '這個工作包在做什麼', + '描述', + '架構圖', + '範圍邊界', + '介面契約', + '待辦', + '整體驗收', + 'repo 列表', + '關聯', +]; + +// ── 輸出模板 ─────────────────────────────────────────────────────── + +test('模板依序包含九個段落', () => { + const headings = [...template.matchAll(/^## (.+)$/gm)].map((m) => m[1].trim()); + assert.deepEqual(headings, SECTIONS); +}); + +test('模板每個段落都有 {{變數}} 佔位', () => { + const placeholders = [...template.matchAll(/\{\{([^}]+)\}\}/g)]; + assert.equal(placeholders.length, SECTIONS.length); +}); + +test('介面契約是四欄表格:介面/產出者/消費者/形狀', () => { + const section = template.slice(template.indexOf('## 介面契約'), template.indexOf('## 待辦')); + assert.match(section, /\|\s*介面\s*\|\s*產出者\s*\|\s*消費者\s*\|\s*形狀\s*\|/); + assert.match(section, /\|\s*---\s*\|/, '要有分隔列,wp-extract 以它為界找資料列'); +}); + +test('模板不把 mermaid 圍欄寫死:不畫圖時才不會留下渲染失敗的空區塊', () => { + assert.equal(template.includes('```mermaid'), false); + const section = template.slice(template.indexOf('## 架構圖'), template.indexOf('## 範圍邊界')); + assert.match(section.trim(), /^## 架構圖\s+\{\{架構圖\}\}$/); +}); + +// ── 產生工作包那一段 ─────────────────────────────────────────────── + +test('第二段要等使用者對共識摘要點頭才開始', () => { + assert.match(phase2, /點頭之後才開始/); + assert.match(phase2, /沒有經過確認就不要往下走/); +}); + +test('正本逐一交代九個段落,且順序與模板一致', () => { + const listed = [...phase2.matchAll(/^\d+\.\s+\*\*(.+?)\*\*/gm)].map((m) => m[1].trim()); + assert.deepEqual(listed, SECTIONS); +}); + +test('標題規則為動詞加名詞,且明令禁止流水編號', () => { + assert.match(phase2, /\{動詞\}\{名詞\}/); + assert.match(phase2, /禁止流水編號/); + assert.match(phase2, /WP-01/, '要舉出被禁止的寫法,不要只說「不要用編號」'); +}); + +test('待辦的巢狀寫法有具體範例,且說明上層與縮排各代表什麼', () => { + const example = phase2.match(/```[^\n]*\n([\s\S]*?)```/)?.[1] ?? ''; + const indents = example + .split('\n') + .filter((line) => line.trim().startsWith('- [')) + .map((line) => line.match(/^\s*/)[0].length); + + assert.ok(indents.length >= 3, '範例要有數行待辦才看得出結構'); + assert.ok( + Math.max(...indents) > Math.min(...indents), + '範例要真的有縮排出來的巢狀層,不能只用文字描述', + ); + assert.match(phase2, /上層是待辦、縮排一層是該項的驗收/); + assert.match(phase2, /不要再往下巢狀/); +}); + +test('範圍邊界要求明列不做什麼', () => { + assert.match(phase2, /明列\*\*不做什麼\*\*/); + assert.match(phase2, /抵抗範圍蔓延/); +}); + +test('介面契約段落交代了沒有對外介面時怎麼填', () => { + assert.match(phase2, /不要留空表/); +}); + +test('關聯段落必須指回來源需求議題', () => { + assert.match(phase2, /需求議題:#/); +}); + +test('架構圖依性質三選一,並釘住節點與字數上限', () => { + for (const kind of ['sequenceDiagram', 'flowchart', 'stateDiagram-v2']) { + assert.match(prompt, new RegExp(kind)); + } + const limits = prompt.slice(prompt.indexOf('## 架構圖的限制')); + assert.match(limits, /12/); + assert.match(limits, /8\s*字/); + assert.match(limits, /拆成多張圖|不畫/); +}); + +test('寫入前先試跑,且試跑的價值有被說明', () => { + assert.match(phase2, /--dry-run/); + assert.match(phase2, /no-op/, '要說明試跑會顯示「實跑是 no-op」,否則使用者不知道該看什麼'); +}); + +test('標籤只能從既有標籤挑,且指名用 labels-list 取得', () => { + assert.match(phase2, /labels-list/); + assert.match(phase2, /不得自行建立新標籤/); +}); + +test('冪等查重有被交代:重跑不會產生第二顆', () => { + assert.match(phase2, /重跑不會產生重複工作包/); + assert.match(phase2, /created.*false|`created` 設為 `false`/); +}); + +test('第二段明列它「不做」的事,避免搶走後續流程的工作', () => { + const boundary = prompt.slice(prompt.indexOf('## 邊界')); + assert.match(boundary, /不建相依/); + assert.match(boundary, /不掛 Milestone/); + assert.match(boundary, /不加看板/); + assert.match(boundary, /不寫人天估算/); +});