/** * 指令入口的契約:第一個位置參數是子指令,其餘 argv 原樣交給子指令。 * * 入口是唯一收位置參數的地方——既有的 flag 解析明確拒絕位置參數,所以子指令必須 * 在交出去之前先被取走。這裡釘住的是「取走」這件事本身,以及取不到、取錯時的說法。 */ import test from 'node:test'; import assert from 'node:assert/strict'; import { execFileSync } from 'node:child_process'; import { repoRoot, manifest, runBin } from './helpers/run-script.js'; /** 入口必須認得的四個名字,缺一個就有轉接檔或文件會指向不存在的子指令 */ const SUBCOMMANDS = ['install', 'uninstall', 'prompt', 'status']; // ── 子指令解析 ───────────────────────────────────────────────────── test('未帶子指令時說清楚缺的是什麼,而不是印出用法就當作成功', async () => { const { code, json } = await runBin([]); assert.equal(code, 1); assert.equal(json.ok, false); assert.equal(json.error.code, 'MISSING_SUBCOMMAND'); // 訊息要列得出可用的名字,使用者才不用回頭翻文件 for (const name of SUBCOMMANDS) assert.match(json.error.message, new RegExp(name)); }); test('不認得的子指令與「flag 打錯」分得開', async () => { const { code, json } = await runBin(['instal']); assert.equal(code, 1); assert.equal(json.error.code, 'UNKNOWN_SUBCOMMAND'); assert.match(json.error.message, /instal/); }); test('四個子指令的名字都被認得,沒有一個掉進 UNKNOWN_SUBCOMMAND', async () => { for (const name of SUBCOMMANDS) { const { json } = await runBin([name, '--no-such-flag']); assert.notEqual( json.error?.code, 'UNKNOWN_SUBCOMMAND', `子指令 ${name} 沒被入口認得`, ); } }); test('子指令之後的未知 flag 沿用既有的 UNKNOWN_FLAG,不另立一套', async () => { const { code, json } = await runBin(['status', '--verbose']); assert.equal(code, 1); assert.equal(json.error.code, 'UNKNOWN_FLAG'); assert.match(json.error.message, /--verbose/); }); test('子指令當成位置參數被取走,不會再被 flag 解析當成不認得的參數', async () => { const { json } = await runBin(['status']); assert.equal(json.ok, true); }); // ── 打包 ─────────────────────────────────────────────────────────── test('manifest 可發佈:移除 private、有 bin、有 files 白名單,且沒有 prepare', () => { const pkg = manifest(); assert.equal(pkg.private, undefined); assert.equal(pkg.bin['tea-sdlc'], 'bin/tea-sdlc.js'); assert.ok(Array.isArray(pkg.files) && pkg.files.length > 0); assert.equal(pkg.scripts.prepare, undefined); }); test('零外部套件這條線沒有因為要發佈而鬆掉', () => { const pkg = manifest(); assert.equal(pkg.dependencies, undefined); assert.equal(pkg.devDependencies, undefined); }); test('打包內容以白名單決定:四個正本目錄都在,測試與暫存都不在', () => { const packed = JSON.parse( execFileSync('npm', ['pack', '--dry-run', '--json'], { cwd: repoRoot, encoding: 'utf8' }), ); const files = packed[0].files.map((file) => file.path); for (const dir of ['prompts/', 'scripts/', 'templates/', 'references/', 'bin/']) { assert.ok( files.some((file) => file.startsWith(dir)), `打包內容少了 ${dir}`, ); } for (const dir of ['test/', '.tmp/']) { assert.equal( files.some((file) => file.startsWith(dir)), false, `${dir} 不該被發給使用者`, ); } });