Files
tea-sdlc/test/work-package-assets.test.js
T
jiantw83andClaude Opus 5 0aa0439061 test(總覽網頁): 釘住模板結構與兩份正本的規則
模板的檢查重點是「樣式與內容分離」與「全景段落能整段消失」,兩者壞掉時規劃版會
留下空標題或行內樣式散落各處,肉眼不容易發現。

順帶把 work-package 測試的 phase2 切法收斂到第三段之前——原本切到檔尾,第三段
新增的編號清單會混進段落順序的斷言裡。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 06:22:29 +00:00

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, /只在超過上限拆不開、或畫了不會比文字更清楚時/);
});