Files
tea-sdlc/test/readme-install.test.js
T
jiantw83andClaude Opus 5 cbf4c3df36 docs(README): 前置需求分開講安裝與跑流程,並把指令是否真的跑得動釘住
前置需求表原本一句「缺少時腳本印出指引並中止」套在所有需求上,但 install 只
警告不中止——文件描述的是一個不存在的行為。改成逐項寫「缺了會怎樣」,並把
「安裝只需要 Node」提到表前面:使用者不該為了裝 plugin 先去裝 tea。

另外補上三條把 #30 驗收標準真的驗起來的測試。其中一條把 README bash 區塊裡的
tea-sdlc 指令逐行拿去真的執行(家目錄指向空的暫存,所以一個檔都不會寫),
失敗理由若是「這個指令/參數我不認得」就算 README 寫錯——這比用眼睛核對可靠。

議題 #30

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

145 lines
6.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* README 的安裝段。
*
* 這一段是使用者照著打的東西,打錯一個字就裝不起來,所以指令要能逐字複製執行,
* 而且不能留下已經不成立的說法——並列一條「照著做不會出現任何指令」的流程等於在說謊。
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, readFileSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { repoRoot, runBin, tmpRoot } from './helpers/run-script.js';
const readme = () => readFileSync(join(repoRoot, 'README.md'), 'utf8');
const agents = () => readFileSync(join(repoRoot, 'AGENTS.md'), 'utf8');
/** 取出所有 bash 圍欄裡的指令行 */
function commands(text) {
return [...text.matchAll(/```bash\n([\s\S]*?)```/g)]
.flatMap((block) => block[1].split('\n'))
.map((line) => line.trim())
.filter((line) => line !== '' && !line.startsWith('#'));
}
test('安裝以 npm 為唯一建議路徑,指令含完整可複製的 git URL', () => {
const install = commands(readme()).filter((line) => line.startsWith('npm i -g'));
assert.equal(install.length > 0, true, 'README 沒有 npm 安裝指令');
assert.ok(
install.some((line) => line.includes('https://gitea.jsc.idv.tw/plugins/tea-sdlc.git')),
`安裝指令少了完整 git URL:${install.join(' / ')}`,
);
});
test('更新與移除各有完整指令表,順序講清楚先 uninstall 再 npm rm', () => {
const text = readme();
assert.match(text, /npm i -g https:\/\/gitea\.jsc\.idv\.tw\/plugins\/tea-sdlc\.git/);
assert.match(text, /tea-sdlc uninstall/);
assert.match(text, /npm rm -g tea-sdlc/);
// 先砍套件就再也刪不掉那些孤兒轉接檔,順序本身就是內容
assert.ok(
text.indexOf('tea-sdlc uninstall') < text.indexOf('npm rm -g tea-sdlc'),
'uninstall 必須寫在 npm rm -g 之前',
);
});
test('明寫不要用 npm update -g,並說出為什麼', () => {
const text = readme();
assert.match(text, /npm update -g/);
const at = text.indexOf('npm update -g');
assert.match(text.slice(at - 200, at + 200), /不要|別/);
});
test('四個子指令在 README 上都有用途說明', () => {
const text = readme();
for (const name of ['install', 'uninstall', 'prompt', 'status']) {
assert.match(text, new RegExp(`tea-sdlc ${name}`), `README 沒有交代 ${name}`);
}
});
test('七套 marketplace 流程移進附錄保留,內容沒有被刪掉', () => {
const text = readme();
const appendix = text.slice(text.indexOf('## 附錄'));
assert.ok(text.includes('## 附錄'), 'README 沒有附錄');
for (const line of ['claude plugin marketplace add', 'codex plugin marketplace add', 'agy plugin install']) {
assert.ok(appendix.includes(line), `附錄少了 ${line}`);
}
});
test('prompt 與 status 各自交代了「誰在什麼時候用它」,不是只列出指令', () => {
const text = readme();
const section = text.slice(text.indexOf('## 給 agent 的入口'), text.indexOf('## 附錄'));
// prompt 是 agent 取得流程正本的管道
assert.match(section, /tea-sdlc prompt --name/);
assert.match(section, /agent|模型/);
// status 是使用者查安裝與環境現況的入口
assert.match(section, /tea-sdlc status/);
assert.match(section, /環境/);
});
test('前置需求表分得開「安裝」與「跑流程指令」各自缺什麼會怎樣', () => {
const text = readme();
const section = text.slice(text.indexOf('## 前置需求'), text.indexOf('## 安裝'));
// install 只警告不中止,表上不能再寫成一律中止——那是文件在描述一個不存在的行為
assert.equal(
/缺少時腳本印出指引並中止/.test(section),
false,
'前置需求表還寫著一律中止,但 tea-sdlc install 只警告',
);
assert.match(section, /install/);
});
test('README 裡的 tea-sdlc 指令逐字拿去跑都認得,不會是寫給人看的假指令', async () => {
const invocations = [...readme().matchAll(/```bash\n([\s\S]*?)```/g)]
.flatMap((block) => block[1].split('\n'))
.map((line) => line.trim())
.filter((line) => line.startsWith('tea-sdlc '))
// shell 會把空白後的 # 之後當註解,貼進終端機仍然跑得動,這裡照做
.map((line) => line.replace(/\s+#.*$/, '').trim());
assert.ok(invocations.length >= 4, `README 只找到 ${invocations.length} 個 tea-sdlc 指令`);
for (const line of invocations) {
// 家目錄指到空的暫存:install/uninstall 因此偵測不到任何平台,一個檔都不會寫
const home = mkdtempSync(join(tmpRoot, 'readme-home-'));
const { code, raw } = await runBin(line.split(/\s+/).slice(1), { env: { HOME: home } });
rmSync(home, { recursive: true, force: true });
// 跑成功就沒話說(prompt 印的是原樣 markdown,本來就不是 JSON);
// 失敗的話,理由不可以是「這個指令/參數我不認得」——那代表 README 寫錯了
if (code === 0) continue;
const { error } = JSON.parse(raw.trim().split('\n').at(-1));
for (const wrong of ['UNKNOWN_SUBCOMMAND', 'UNKNOWN_FLAG', 'MISSING_FLAG', 'BAD_PROMPT_NAME']) {
assert.notEqual(error.code, wrong, `README 的「${line}」跑出 ${wrong}:${error.message}`);
}
}
});
test('模組邊界表指得到實際存在的檔案', () => {
const text = agents();
assert.match(text, /`bin\/tea-sdlc\.js`/);
assert.match(text, /`scripts\/install\.js`/);
assert.equal(/\| `install\.js` \|/.test(text), false, '表上還留著已經不存在的根目錄 install.js');
});
test('指令入口那一列把職責與邊界都寫出來了', () => {
const row = agents().split('\n').find((line) => line.includes('`bin/tea-sdlc.js`'));
assert.ok(row, 'AGENTS.md 模組邊界表沒有指令入口那一列');
const [, , duty, boundary] = row.split('|').map((cell) => cell.trim());
// 職責是子指令解析與 dispatch
assert.match(duty, /子指令/);
assert.match(duty, /dispatch|交出去/);
// 邊界是不含任何平台目錄知識與動作實作
assert.match(boundary, /平台目錄/);
assert.match(boundary, /實作|做事/);
});