模板的檢查重點是「樣式與內容分離」與「全景段落能整段消失」,兩者壞掉時規劃版會 留下空標題或行內樣式散落各處,肉眼不容易發現。 順帶把 work-package 測試的 phase2 切法收斂到第三段之前——原本切到檔尾,第三段 新增的編號清單會混進段落順序的斷言裡。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
139 lines
5.4 KiB
JavaScript
139 lines
5.4 KiB
JavaScript
/**
|
|
* 工作包議題的模板,以及正本裡「產生工作包」那一段的規則。
|
|
*
|
|
* 模板的段落順序決定 #9 的 wp-extract 解析得到什麼;待辦的巢狀寫法決定實作階段
|
|
* 勾得到哪一行。這兩件事寫死在測試裡,改動時才會被逼著一起改。
|
|
*/
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
assertDiagramPlaceholderOnly,
|
|
assertPromptListsSections,
|
|
assertTemplateSections,
|
|
readPrompt,
|
|
readTemplate,
|
|
} from './helpers/prompt-doc.js';
|
|
|
|
const template = readTemplate('work-package-issue');
|
|
const prompt = readPrompt('sdlc-analyze');
|
|
/** 第二段的內容,避免把第一段的字樣誤認成這一段的規則 */
|
|
const phase2 = prompt.slice(prompt.indexOf('## 第二段'), prompt.indexOf('## 第三段'));
|
|
|
|
/** 工作包議題的九個段落,順序即議題裡的順序 */
|
|
const SECTIONS = [
|
|
'這個工作包在做什麼',
|
|
'描述',
|
|
'架構圖',
|
|
'範圍邊界',
|
|
'介面契約',
|
|
'待辦',
|
|
'整體驗收',
|
|
'repo 列表',
|
|
'關聯',
|
|
];
|
|
|
|
// ── 輸出模板 ───────────────────────────────────────────────────────
|
|
|
|
test('模板依序包含九個段落', () => {
|
|
assertTemplateSections(template, 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 圍欄寫死:不畫圖時才不會留下渲染失敗的空區塊', () => {
|
|
assertDiagramPlaceholderOnly(template, '架構圖', '範圍邊界');
|
|
});
|
|
|
|
// ── 產生工作包那一段 ───────────────────────────────────────────────
|
|
|
|
test('第二段要等使用者對共識摘要點頭才開始', () => {
|
|
assert.match(phase2, /點頭之後才開始/);
|
|
assert.match(phase2, /沒有經過確認就不要往下走/);
|
|
});
|
|
|
|
test('正本逐一交代九個段落,且順序與模板一致', () => {
|
|
assertPromptListsSections(phase2, 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, /不寫人天估算/);
|
|
});
|
|
|
|
test('「不畫圖」是有條件的退路,不是免死金牌', () => {
|
|
const limits = prompt.slice(prompt.indexOf('## 架構圖的限制'));
|
|
assert.match(limits, /只在超過上限拆不開、或畫了不會比文字更清楚時/);
|
|
});
|