From cbf4c3df361bf98642765e3340dcc17875574bbb Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 07:11:18 +0000 Subject: [PATCH] =?UTF-8?q?docs(README):=20=E5=89=8D=E7=BD=AE=E9=9C=80?= =?UTF-8?q?=E6=B1=82=E5=88=86=E9=96=8B=E8=AC=9B=E5=AE=89=E8=A3=9D=E8=88=87?= =?UTF-8?q?=E8=B7=91=E6=B5=81=E7=A8=8B=EF=BC=8C=E4=B8=A6=E6=8A=8A=E6=8C=87?= =?UTF-8?q?=E4=BB=A4=E6=98=AF=E5=90=A6=E7=9C=9F=E7=9A=84=E8=B7=91=E5=BE=97?= =?UTF-8?q?=E5=8B=95=E9=87=98=E4=BD=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 前置需求表原本一句「缺少時腳本印出指引並中止」套在所有需求上,但 install 只 警告不中止——文件描述的是一個不存在的行為。改成逐項寫「缺了會怎樣」,並把 「安裝只需要 Node」提到表前面:使用者不該為了裝 plugin 先去裝 tea。 另外補上三條把 #30 驗收標準真的驗起來的測試。其中一條把 README bash 區塊裡的 tea-sdlc 指令逐行拿去真的執行(家目錄指向空的暫存,所以一個檔都不會寫), 失敗理由若是「這個指令/參數我不認得」就算 README 寫錯——這比用眼睛核對可靠。 議題 #30 Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 15 ++++---- test/readme-install.test.js | 68 +++++++++++++++++++++++++++++++++++-- 2 files changed, 75 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 83c5960..e1cee56 100644 --- a/README.md +++ b/README.md @@ -49,13 +49,16 @@ tea-sdlc/ ## 前置需求 -| 需求 | 用途 | 備註 | +安裝與佈署(`tea-sdlc install`)只需要 Node;其餘是**跑流程指令**時才需要的。 +本工具一律不替你安裝任何東西。 + +| 需求 | 用途 | 缺了會怎樣 | | --- | --- | --- | -| Node.js ≥ 20 | 執行 `scripts/` 與測試 | 不自動安裝,缺少時腳本印出指引並中止 | -| git | 分支與 commit 操作 | 同上 | -| [`tea`](https://gitea.com/gitea/tea) 並已登入 | Gitea 議題、標籤、Milestone、留言、工時 | `tea login add` | -| 目標 repo 已開啟時間追蹤 | 工時碼錶 | Settings → Advanced Settings → Enable Time Tracker | -| 帳號對目標 repo 的 issues unit 有寫入權 | 建立與更新議題 | Gitea 的 unit 權限獨立於 push 權限 | +| Node.js ≥ 20 | 執行 `tea-sdlc` 與 `scripts/` | 連 `tea-sdlc` 都跑不起來 | +| git | 分支與 commit 操作 | `install` 照樣把轉接檔裝好,只在輸出裡列出缺的東西;流程指令中止並印出安裝指引 | +| [`tea`](https://gitea.com/gitea/tea) 並已登入 | Gitea 議題、標籤、Milestone、留言、工時 | 同上;登入用 `tea login add` | +| 目標 repo 已開啟時間追蹤 | 工時碼錶 | 流程指令中止,並指出 Settings → Advanced Settings → Enable Time Tracker | +| 帳號對目標 repo 的 issues unit 有寫入權 | 建立與更新議題 | 流程指令中止;Gitea 的 unit 權限獨立於 push 權限 | --- diff --git a/test/readme-install.test.js b/test/readme-install.test.js index c9c696b..deb3920 100644 --- a/test/readme-install.test.js +++ b/test/readme-install.test.js @@ -6,9 +6,9 @@ */ import test from 'node:test'; import assert from 'node:assert/strict'; -import { readFileSync } from 'node:fs'; +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; import { join } from 'node:path'; -import { repoRoot } from './helpers/run-script.js'; +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'); @@ -71,6 +71,57 @@ test('七套 marketplace 流程移進附錄保留,內容沒有被刪掉', () = } }); +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(); @@ -78,3 +129,16 @@ test('模組邊界表指得到實際存在的檔案', () => { 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, /實作|做事/); +}); -- 2.53.0