/** * 直接 import lib 的兩支測試之一(另一支是 worktree-path),其餘一律走子行程的 CLI 邊界。 * * 理由:冪等查重與 git 執行點是 lib 對「其他腳本」公開的契約,而 issue-create 與 * branch-prep 落地之後,它們的 CLI 測試才是這兩件事的主場——這一支已經是備位的, * 留著是因為直接測 export 仍比讓契約完全沒有測試好。 */ import test from 'node:test'; import assert from 'node:assert/strict'; import { findIssueByTitle, runGit, pluginRoot, templatesDir, referencesDir } from '../scripts/lib.js'; import { startStubGitea } from './helpers/stub-gitea.js'; import { makeTempRepo } from './helpers/temp-repo.js'; import { existsSync } from 'node:fs'; import { join } from 'node:path'; const REPO = 'plugins/tea-sdlc'; // ── 冪等查重 ─────────────────────────────────────────────────────── test('查重:標題完全相同的既有議題會被找到', async (t) => { const stub = await startStubGitea({ [`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: [ { number: 7, title: '建立工作包的抽取契約' }, { number: 8, title: '產生圖解版總覽網頁並寫回議題' }, ], }, }); t.after(() => stub.close()); const found = await findIssueByTitle( { base: stub.base, token: 'stub-token' }, REPO, '產生圖解版總覽網頁並寫回議題', ); assert.equal(found.number, 8); }); test('查重:沒有同名議題時回 null,讓呼叫端自行決定要不要建立', async (t) => { const stub = await startStubGitea({ [`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: [{ number: 7, title: '別的議題' }] }, }); t.after(() => stub.close()); const found = await findIssueByTitle({ base: stub.base, token: 'x' }, REPO, '找不到的標題'); assert.equal(found, null); }); test('查重:同時看 open 與 closed,避免把已關閉的議題再建一次', async (t) => { const stub = await startStubGitea({ [`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: [] }, }); t.after(() => stub.close()); await findIssueByTitle({ base: stub.base, token: 'x' }, REPO, '任何標題'); assert.equal(stub.requests[0].query.state, 'all'); }); test('查重:標題只差前後空白仍視為同一顆', async (t) => { const stub = await startStubGitea({ [`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: [{ number: 12, title: ' 走通腳本契約 ' }], }, }); t.after(() => stub.close()); const found = await findIssueByTitle({ base: stub.base, token: 'x' }, REPO, '走通腳本契約'); assert.equal(found.number, 12); }); // ── git 執行點 ───────────────────────────────────────────────────── test('git 執行點:在臨時 repo 上跑真實 git,取得目前分支', (t) => { const repo = makeTempRepo(); t.after(() => repo.cleanup()); const branch = runGit(['rev-parse', '--abbrev-ref', 'HEAD'], { cwd: repo.dir }); assert.equal(branch, 'master'); }); test('git 執行點:輸出前後空白被修掉,呼叫端不必自己 trim', (t) => { const repo = makeTempRepo(); t.after(() => repo.cleanup()); const log = runGit(['log', '--oneline'], { cwd: repo.dir }); assert.equal(log.endsWith('\n'), false); assert.match(log, /initial$/); }); test('git 執行點:git 失敗時拋出帶有 stderr 的錯誤,不回傳空字串', (t) => { const repo = makeTempRepo(); t.after(() => repo.cleanup()); assert.throws(() => runGit(['rev-parse', 'no-such-ref'], { cwd: repo.dir }), /no-such-ref/); }); // ── 路徑定位 ─────────────────────────────────────────────────────── test('plugin 根由自身檔案位置回推,templates 與 references 都在底下', () => { assert.equal(templatesDir(), join(pluginRoot(), 'templates')); assert.equal(referencesDir(), join(pluginRoot(), 'references')); assert.ok(existsSync(templatesDir())); assert.ok(existsSync(referencesDir())); }); test('查重:翻頁有上限,查不完時報錯而不是無聲回 null', async (t) => { // 無聲回 null 會讓呼叫端把既有議題再建一次,正好是查重要防的事 const full = Array.from({ length: 50 }, (_, i) => ({ number: i, title: `不相干的議題 ${i}` })); const stub = await startStubGitea({ [`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: full }, }); t.after(() => stub.close()); await assert.rejects( () => findIssueByTitle({ base: stub.base, token: 'x' }, REPO, '永遠找不到的標題'), (error) => error.code === 'DEDUPE_LIMIT', ); });