test(逐項實作): 覆蓋勾選的五種危險、兩份規則正本與正本第二段
勾選的測試全部繞著「會不會改錯行」打轉,五種都是 code review 抓出來的實際缺陷: 圍欄裡長得像 checkbox 的那一行不會被改到、不同段落的同一句話靠 --section 分得開、 大寫 [X] 重跑是 no-op、方框後沒有空白照樣勾得到、沒有方框的項目給的是指路的錯誤 而不是謊報已勾過。 另外釘住抽取端與勾選端的一致性:wp-extract 交得出來的每一種 raw,--tick 都要收得下。 patchOf 收進 helpers——它先前在三個測試檔裡各有一份一模一樣的定義。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
/**
|
||||
* 實作規範與註解格式對照表這兩份規則正本。
|
||||
*
|
||||
* 它們是 /sdlc-feat 第二段實際交付的東西:規範寫漏一條,產出的程式碼就少一種註解,
|
||||
* 而那要等 reviewer 看到才會發現。對照表少一種語言,agent 就會開始猜格式。
|
||||
*/
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { readReference } from './helpers/prompt-doc.js';
|
||||
|
||||
const standards = readReference('coding-standards');
|
||||
const styles = readReference('comment-styles');
|
||||
|
||||
/**
|
||||
* 切出一個 `## 標題` 段落。
|
||||
* 以整行比對而不是 indexOf:`## Java` 是 `## JavaScript` 的前綴,
|
||||
* 用 indexOf 會切到錯的那一節,而且切出來還是有內容的,錯得很安靜。
|
||||
*/
|
||||
function sectionOf(doc, heading) {
|
||||
const lines = doc.split('\n');
|
||||
const start = lines.findIndex((line) => line.trim() === `## ${heading}`);
|
||||
if (start === -1) return null;
|
||||
|
||||
const rest = lines.slice(start + 1);
|
||||
const end = rest.findIndex((line) => line.startsWith('## '));
|
||||
return (end === -1 ? rest : rest.slice(0, end)).join('\n');
|
||||
}
|
||||
|
||||
// ── 實作規範 ───────────────────────────────────────────────────────
|
||||
|
||||
test('六種專案檔都對得到語言', () => {
|
||||
for (const file of [
|
||||
'\\*\\.csproj',
|
||||
'composer\\.json',
|
||||
'package\\.json',
|
||||
'go\\.mod',
|
||||
'pom\\.xml',
|
||||
'pyproject\\.toml',
|
||||
]) {
|
||||
assert.match(standards, new RegExp(file), `專案檔對照缺少 ${file}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('認不出語言時要停下來問,而且說明了為什麼不猜', () => {
|
||||
assert.match(standards, /認不出來就停下來問/);
|
||||
assert.match(standards, /不要猜/);
|
||||
assert.match(standards, /比沒有註解更難清理/, '要說出猜錯的代價,否則這條規則會被當成客套話');
|
||||
});
|
||||
|
||||
test('分層判定明講看職責不看目錄', () => {
|
||||
assert.match(standards, /看職責,不看目錄/);
|
||||
assert.match(standards, /目錄名稱會騙人/);
|
||||
});
|
||||
|
||||
test('三層各自要寫哪一種註解都寫明了', () => {
|
||||
for (const [layer, comment] of [
|
||||
['控制層', '功能註解'],
|
||||
['服務層', '邏輯註解'],
|
||||
['存取層', '資料源註解'],
|
||||
]) {
|
||||
const row = standards.split('\n').find((line) => line.includes(layer) && line.includes('|'));
|
||||
assert.ok(row, `${layer}沒有出現在分層表裡`);
|
||||
assert.match(row, new RegExp(comment), `${layer}要寫的是${comment}`);
|
||||
}
|
||||
});
|
||||
|
||||
test('服務層要標註呼叫的方法,並說明理由是追呼叫鏈', () => {
|
||||
assert.match(standards, /標註它呼叫的所有方法/);
|
||||
assert.match(standards, /追得到呼叫鏈/);
|
||||
});
|
||||
|
||||
test('屬性註解要遞迴,而且明講不能只註解最外層', () => {
|
||||
assert.match(standards, /屬性本身是類別時遞迴處理/);
|
||||
assert.match(standards, /不能只註解最外層/);
|
||||
});
|
||||
|
||||
test('資料範例的來源有優先序,且未經驗證時要註明', () => {
|
||||
assert.match(standards, /優先從 MCP 取得/);
|
||||
assert.match(standards, /由邏輯推理、未經驗證/);
|
||||
assert.match(standards, /有人會照著那個格式寫解析/, '要說出不註明的代價');
|
||||
});
|
||||
|
||||
test('明講不寫入目標專案的任何檔案', () => {
|
||||
assert.match(standards, /不寫入目標專案的任何檔案/);
|
||||
assert.match(standards, /CLAUDE\.md/);
|
||||
});
|
||||
|
||||
// ── 註解格式對照表 ─────────────────────────────────────────────────
|
||||
|
||||
test('六種語言各有一節,且都附可照抄的程式碼範例', () => {
|
||||
for (const [language, marker] of [
|
||||
['C#', '///'],
|
||||
['PHP', '@var'],
|
||||
['JavaScript/TypeScript', 'JSDoc'],
|
||||
['Go', 'go doc'],
|
||||
['Java', 'Javadoc'],
|
||||
['Python', 'docstring'],
|
||||
]) {
|
||||
const body = sectionOf(styles, language);
|
||||
assert.ok(body, `對照表缺少 ${language}`);
|
||||
assert.match(body, new RegExp(marker.replace(/[/#]/g, '\\$&')), `${language} 缺少 ${marker}`);
|
||||
assert.match(body, /```/, `${language} 要有可照抄的範例,不要只用文字描述`);
|
||||
}
|
||||
});
|
||||
|
||||
test('每個語言的範例都同時示範了方法註解與屬性註解', () => {
|
||||
const sections = styles.split(/^## /m).filter((s) => s.includes('```'));
|
||||
for (const section of sections) {
|
||||
const name = section.split('\n')[0].trim();
|
||||
if (name === '未經驗證的範例怎麼標') continue;
|
||||
assert.match(section, /例:|例如/, `${name} 的範例要示範「附真實資料範例」這件事`);
|
||||
}
|
||||
});
|
||||
|
||||
test('Go 的慣例(以識別字開頭)有被指出來,不是照抄別的語言', () => {
|
||||
assert.match(sectionOf(styles, 'Go'), /以被註解的識別字開頭/);
|
||||
});
|
||||
|
||||
test('Python 的 docstring 位置有講清楚在定義的下一行', () => {
|
||||
const python = sectionOf(styles, 'Python');
|
||||
assert.match(python, /下一行/);
|
||||
assert.match(python, /不是上一行/, '這是最容易寫錯的一點,要明講');
|
||||
});
|
||||
|
||||
test('未經驗證的註明怎麼寫,兩種語言各有一個可照抄的寫法', () => {
|
||||
const section = sectionOf(styles, '未經驗證的範例怎麼標');
|
||||
assert.match(section, /不要另起一行 TODO/);
|
||||
assert.ok((section.match(/由邏輯推理、未經驗證/g) ?? []).length >= 2, '至少要有兩種語言的寫法');
|
||||
});
|
||||
|
||||
// ── 兩份的分工 ─────────────────────────────────────────────────────
|
||||
|
||||
test('規範與格式分開:對照表不重複寫一遍規範', () => {
|
||||
assert.match(styles, /這份只管\*\*格式\*\*/);
|
||||
assert.match(standards, /comment-styles\.md/, '規範要指名去哪裡查格式');
|
||||
});
|
||||
Reference in New Issue
Block a user