install 寫完轉接檔後,把叫用鏈真的走一遍:轉接檔 → PATH 上的 tea-sdlc → 流程正本。
最脆弱的是中間那一環。套件裝在某個 Node 版本底下,換個版本就找不到了,而轉接檔本身
看起來完全正常——沒有這道驗證,使用者要到第一次打 /sdlc-plan 才發現,那時他已經離開
安裝的心智狀態很久了。所以不是查檔案在不在,而是真的到 PATH 上把 tea-sdlc 找出來執行
一次,再把取回的正本跟套件裡的那一份逐字比對:找不到、叫不動、或叫到的是另一份安裝,
三種都驗得出來。轉接檔則逐一回磁碟讀,比對存在且內容含正確的叫用行。
驗證不碰網路,也與 Gitea 登入、時間追蹤無關,所以無條件執行。
驗不過回 ok:false,但已經寫好的轉接檔一份都不刪。回滾在升級情境下是淨損失:原本有一組
能用的舊轉接檔,覆蓋後驗證失敗再刪掉,使用者就從「有點舊但能用」變成什麼都沒有;何況
最可能的病灶是「PATH 上找不到 tea-sdlc」,那不是轉接檔的問題。
為此 lib 多一個 Failure:有一種失敗是事情做完了、檔案也寫出去了,只是驗不過,那時最該
交出去的正是「已經寫了哪些、哪一段不通」。envelope 形狀不變,只是 {ok:false, error}
旁邊多一個 data,只讀 error.code 的呼叫端照常運作。
--dry-run 不寫入,也就沒有東西可驗,verify 標成 skipped。
Closes #59
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
81 lines
3.6 KiB
JavaScript
81 lines
3.6 KiB
JavaScript
/**
|
|
* 在測試暫存裡建一棵完整的假 plugin 根,並從那裡啟動指令入口。
|
|
*
|
|
* 為什麼要整棵複製而不是加一個環境變數:入口回推 plugin 根的那段明寫「不依賴 cwd
|
|
* 也不依賴環境變數」,為測試破它等於把要驗的東西驗掉。把 bin/ 與 scripts/ 原封複製
|
|
* 過去,回推就自然指向假根,跑的仍是真正的程式碼;順便把「plugin 目錄不完整」
|
|
* 那條路徑一起測得到——少給哪個目錄由測試自己決定。
|
|
*/
|
|
import { chmodSync, cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
import { repoRoot, runBin, tmpRoot } from './run-script.js';
|
|
|
|
/** 一份假的流程正本,格式與真正的正本一致:頭兩行是 name 與 description */
|
|
export const fakePrompt = (name) =>
|
|
`name: ${name}\ndescription: 僅由 /${name} 指令叫用。假的正本,只用於測試。\n\n# ${name}\n\n第一段。\n\n## 小節\n\n第二段。\n`;
|
|
|
|
/**
|
|
* @param {object} t node:test 的 TestContext
|
|
* @param {{prompts?: Record<string,string>, omit?: string[], version?: string}} options
|
|
* prompts 要放進 prompts/ 的正本,鍵為指令名;
|
|
* omit 故意不建立的目錄,用來造出「plugin 目錄不完整」;
|
|
* version 覆寫假根的套件版本
|
|
* @returns {{root: string, shim: string, run: (args: string[], opts?: object) => Promise<object>}}
|
|
* shim 是放著這份假 plugin 的 tea-sdlc 的目錄,預設已經加進 run 的 PATH
|
|
*/
|
|
export function makeFakePlugin(t, { prompts = {}, omit = [], version } = {}) {
|
|
mkdirSync(tmpRoot, { recursive: true });
|
|
const root = mkdtempSync(join(tmpRoot, 'plugin-'));
|
|
t.after(() => rmSync(root, { recursive: true, force: true }));
|
|
|
|
// 真正的程式碼,不是替身:測到的就是使用者會執行的東西
|
|
for (const dir of ['bin', 'scripts']) {
|
|
cpSync(join(repoRoot, dir), join(root, dir), { recursive: true });
|
|
}
|
|
|
|
const manifest = JSON.parse(readFileSync(join(repoRoot, 'package.json'), 'utf8'));
|
|
if (version !== undefined) manifest.version = version;
|
|
writeFileSync(join(root, 'package.json'), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
|
|
for (const dir of ['prompts', 'templates', 'references']) {
|
|
if (omit.includes(dir)) continue;
|
|
mkdirSync(join(root, dir), { recursive: true });
|
|
}
|
|
for (const [name, text] of Object.entries(prompts)) {
|
|
writeFileSync(join(root, 'prompts', `${name}.md`), text);
|
|
}
|
|
|
|
const shim = makeShim(root);
|
|
|
|
return {
|
|
root,
|
|
shim,
|
|
// 預設把這份假 plugin 的 tea-sdlc 放進 PATH:真實使用者是 npm i -g 裝的,
|
|
// 叫用鏈上本來就有這一環。要測「PATH 上找不到」的那條路徑就傳 shim: false。
|
|
run: (args, { shim: onPath = true, path, ...opts } = {}) =>
|
|
runBin(args, {
|
|
...opts,
|
|
root,
|
|
path: onPath ? [shim, path ?? process.env.PATH].join(':') : path,
|
|
}),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* 替一份假 plugin 根造出可以從 PATH 叫到的 `tea-sdlc`。
|
|
*
|
|
* install 的驗證會真的去 PATH 上把 tea-sdlc 找出來執行——那正是它要驗的那一環。
|
|
* 測試裡若沒有這個殼,驗到的就只是「測試環境沒有裝 tea-sdlc」,而不是待驗的東西。
|
|
* @param {string} root 假 plugin 根
|
|
* @returns {string} 殼所在的目錄,加進 PATH 就能叫到
|
|
*/
|
|
function makeShim(root) {
|
|
const dir = join(root, 'shim');
|
|
mkdirSync(dir, { recursive: true });
|
|
|
|
const path = join(dir, 'tea-sdlc');
|
|
writeFileSync(path, `#!/bin/sh\nexec ${JSON.stringify(process.execPath)} ${JSON.stringify(join(root, 'bin', 'tea-sdlc.js'))} "$@"\n`);
|
|
chmodSync(path, 0o755);
|
|
return dir;
|
|
}
|