/** * 所有腳本共用的介面契約:具名 flag 輸入、單行 JSON 輸出、--dry-run、 * 以及「從自身檔案位置回推 plugin 根」而非依賴 cwd。 * 以 labels-list 當代表,因為契約由 lib 提供,每支腳本表現一致。 */ import test from 'node:test'; import assert from 'node:assert/strict'; import { runScript } from './helpers/run-script.js'; import { startStubGitea, healthyRoutes } from './helpers/stub-gitea.js'; const REPO = 'plugins/tea-sdlc'; async function withStub(t, overrides = {}) { const stub = await startStubGitea(healthyRoutes(REPO, overrides)); t.after(() => stub.close()); return stub; } const envFor = (stub) => ({ TEA_SDLC_API_BASE: stub.base, TEA_SDLC_TOKEN: 'stub-token' }); // ── 輸入:具名 flag ──────────────────────────────────────────────── test('缺少必填 flag 時失敗,並指名缺的是哪一個', async (t) => { const stub = await withStub(t); const { code, json } = await runScript('labels-list.js', [], { env: envFor(stub) }); assert.equal(code, 1); assert.equal(json.ok, false); assert.equal(json.error.code, 'MISSING_FLAG'); assert.match(json.error.message, /--repo/); }); test('不認得的 flag 一律拒絕,不默默忽略', async (t) => { const stub = await withStub(t); const { json } = await runScript('labels-list.js', ['--repo', REPO, '--verbose'], { env: envFor(stub), }); assert.equal(json.error.code, 'UNKNOWN_FLAG'); assert.match(json.error.message, /--verbose/); }); test('位置參數一律拒絕:輸入只接受具名 flag', async (t) => { const stub = await withStub(t); const { json } = await runScript('labels-list.js', [REPO], { env: envFor(stub) }); assert.equal(json.error.code, 'UNKNOWN_FLAG'); }); test('--repo 格式不是 owner/name 時失敗', async (t) => { const stub = await withStub(t); const { json } = await runScript('labels-list.js', ['--repo', 'tea-sdlc'], { env: envFor(stub) }); assert.equal(json.error.code, 'BAD_REPO'); }); test('--key=value 與 --key value 兩種寫法等價', async (t) => { const stub = await withStub(t); const a = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); const b = await runScript('labels-list.js', [`--repo=${REPO}`], { env: envFor(stub) }); assert.deepEqual(a.json, b.json); }); // ── 輸出:單行 JSON ─────────────────────────────────────────────── test('成功時輸出 {ok:true, data},且沒有 error 欄位', async (t) => { const stub = await withStub(t); const { json } = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(json.ok, true); assert.ok(json.data); assert.equal('error' in json, false); }); test('失敗時輸出 {ok:false, error:{code,message}},且沒有 data 欄位', async (t) => { const stub = await withStub(t); const { json } = await runScript('labels-list.js', [], { env: envFor(stub) }); assert.equal(json.ok, false); assert.equal(typeof json.error.code, 'string'); assert.equal(typeof json.error.message, 'string'); assert.equal('data' in json, false); }); test('失敗時 exit code 為 1,成功為 0', async (t) => { const stub = await withStub(t); const bad = await runScript('labels-list.js', [], { env: envFor(stub) }); const good = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); assert.equal(bad.code, 1); assert.equal(good.code, 0); }); test('例外不會以 stack trace 洩漏出去,一律收斂成 JSON', async (t) => { // 指向一個沒有人在聽的 port,讓 fetch 直接拋連線錯誤 const { code, json, stderr } = await runScript('labels-list.js', ['--repo', REPO], { env: { TEA_SDLC_API_BASE: 'http://127.0.0.1:1/api/v1', TEA_SDLC_TOKEN: 'x' }, }); assert.equal(code, 1); assert.equal(json.ok, false); assert.equal(json.error.code, 'NETWORK_ERROR'); assert.equal(stderr, '', 'stderr 必須乾淨,訊息一律走 stdout 的 JSON'); }); // ── --dry-run ───────────────────────────────────────────────────── test('--dry-run 印出將發出的請求,且完全不碰 Gitea', async (t) => { const stub = await withStub(t); const { code, json } = await runScript('labels-list.js', ['--repo', REPO, '--dry-run'], { env: envFor(stub), }); assert.equal(code, 0); assert.equal(json.ok, true); assert.equal(json.data.dryRun, true); assert.deepEqual(json.data.requests, [ { method: 'GET', path: `/repos/${REPO}/labels` }, ]); assert.equal(stub.requests.length, 0, '--dry-run 不得發出任何請求,連前置檢查也不發'); }); // ── 路徑定位 ─────────────────────────────────────────────────────── test('在任意 cwd 下執行結果相同:腳本從自身位置回推 plugin 根', async (t) => { const stub = await withStub(t); const fromRoot = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub) }); const fromSlash = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub), cwd: '/', }); const fromTmp = await runScript('labels-list.js', ['--repo', REPO], { env: envFor(stub), cwd: '/tmp', }); assert.equal(fromRoot.code, 0); assert.deepEqual(fromSlash.json, fromRoot.json); assert.deepEqual(fromTmp.json, fromRoot.json); }); test('--dry-run 回報的 templates/references 路徑為絕對路徑,且不隨 cwd 改變', async (t) => { const stub = await withStub(t); const a = await runScript('labels-list.js', ['--repo', REPO, '--dry-run'], { env: envFor(stub) }); const b = await runScript('labels-list.js', ['--repo', REPO, '--dry-run'], { env: envFor(stub), cwd: '/tmp', }); assert.match(a.json.data.paths.templates, /^\/.*\/templates$/); assert.match(a.json.data.paths.references, /^\/.*\/references$/); assert.deepEqual(b.json.data.paths, a.json.data.paths); });