模板的段落順序決定 wp-extract 解析得到什麼,待辦的巢狀寫法決定實作階段勾得到 哪一行——這兩件事寫死在測試裡,改動時才會被逼著一起改。 巢狀範例的斷言不比對固定縮排量,而是比對「最深的一層比最淺的深」,這樣重排 外層清單的縮排不會弄壞測試。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
132 lines
5.3 KiB
JavaScript
132 lines
5.3 KiB
JavaScript
/**
|
|
* 工作包議題的模板,以及正本裡「產生工作包」那一段的規則。
|
|
*
|
|
* 模板的段落順序決定 #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, /不寫人天估算/);
|
|
});
|