Files
tea-sdlc/test/helpers/prompt-doc.js
T
jiantw83andClaude Opus 5 2acdcea755 refactor(測試): 模板結構的三項檢查抽到共用斷言
段落順序、正本的編號清單、圖表段落不寫死圍欄——這三件事需求議題與工作包議題
都要驗,第二份模板出現時就該抽出來。兩份測試原本各抄一份,其中佔位數的斷言
還悄悄長成不同寫法(一邊 >=、一邊 ==);抽出來之後這種分歧會被逼著講清楚。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 13:43:07 +08:00

93 lines
3.3 KiB
JavaScript

/**
* 流程正本與規則正本的共用檢查。
*
* 這些檔案是文件不是程式,但它們是各指令實際交付的東西:正本的平台中立性決定
* 轉接檔能不能一份寫到底,段落結構決定下游解析得到什麼。靠人記不牢,用測試釘住。
*/
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { repoRoot } from './run-script.js';
/** 讀一份流程正本 */
export function readPrompt(name) {
return readFileSync(join(repoRoot, 'prompts', `${name}.md`), 'utf8');
}
/** 讀一份規則正本 */
export function readReference(name) {
return readFileSync(join(repoRoot, 'references', `${name}.md`), 'utf8');
}
/** 讀一份輸出模板 */
export function readTemplate(name) {
return readFileSync(join(repoRoot, 'templates', `${name}.md`), 'utf8');
}
/**
* 正本裡不該出現的字樣:任何一家助理的工具名、目錄名或呼叫語法。
* 出現任何一個,就代表這份正本已經綁死在某個平台上。
*/
export const PLATFORM_SPECIFIC = [
'AskUserQuestion',
'Claude',
'Codex',
'Antigravity',
'Copilot',
'Kiro',
'OpenCode',
'oh-my-pi',
'.claude',
'.codex',
];
/**
* 斷言一份正本是平台中立的,且 description 帶上指定前綴。
* @param {string} prompt 正本內容
* @param {string} command 指令名,例如 sdlc-plan
*/
export function assertNeutralPrompt(prompt, command) {
const description = prompt.match(/^description:\s*(.+)$/m)?.[1];
assert.ok(description, '正本需要一行 description 供轉接檔取用');
assert.ok(
description.startsWith(`僅由 /${command} 指令叫用。`),
`description 前綴不符:${description}`,
);
assert.equal(prompt.startsWith('---'), false, '正本不該有 YAML frontmatter,那是轉接檔的事');
for (const token of [...PLATFORM_SPECIFIC, `$${command}`]) {
assert.equal(prompt.includes(token), false, `正本不該出現平台專屬字樣:${token}`);
}
}
/**
* 斷言模板的 `## 標題` 就是這組段落,順序一致。
* 段落順序即下游抽取契約的解析依據,兩邊必須一起改。
*/
export function assertTemplateSections(template, sections) {
const headings = [...template.matchAll(/^## (.+)$/gm)].map((m) => m[1].trim());
assert.deepEqual(headings, sections);
}
/**
* 斷言正本的編號清單逐一交代了這組段落,順序與模板一致。
* @param {string} text 正本內容,或其中相關的一段
*/
export function assertPromptListsSections(text, sections) {
const listed = [...text.matchAll(/^\d+\.\s+\*\*(.+?)\*\*/gm)].map((m) => m[1].trim());
assert.deepEqual(listed, sections);
}
/**
* 斷言圖表段落只有佔位、沒有寫死的 mermaid 圍欄。
* 正本允許「乾脆不畫」,圍欄寫死在模板裡的話,不畫時會在議題頁留下一塊渲染失敗的空區塊。
* @param {string} heading 圖表段落的標題,例如 '流程圖'
* @param {string} nextHeading 其後一個段落的標題,用來框出範圍
*/
export function assertDiagramPlaceholderOnly(template, heading, nextHeading) {
assert.equal(template.includes('```mermaid'), false);
const section = template.slice(template.indexOf(`## ${heading}`), template.indexOf(`## ${nextHeading}`));
assert.match(section.trim(), new RegExp(`^## ${heading}\\s+\\{\\{${heading}\\}\\}$`));
}