議題 #38 的使用者故事第 30 條:使用者常以為停錶等於放棄那顆工作包,於是寧可 不停——工時就記到別顆議題去了。碼錶只管時間、工作樹只管檔案,兩者互不相干, 這件事要在擋下來的當下就講,不能指望使用者自己推論。 領取與起錶會撞到同一個擋路理由,訊息收進 lib 只寫一份。順手收掉 review 指出的 三處:planWorktree 沒用到的 repo 參數、與 path.resolve 同名而誤導的區域函式、 以及只有 lib 自己用得到卻對外 export 的兩支路徑函式。 回滾補上最後一道:git 清不掉時把目錄本身也刪掉。那條路徑在這次執行之前不存在 (不存在正是建立的前提),裡面不可能有使用者的東西,而留著它下一次重跑會直接 撞上 WORKTREE_PATH_TAKEN——一次失敗的建立不該讓人從此開不了工。 議題 #40 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
127 lines
4.8 KiB
JavaScript
127 lines
4.8 KiB
JavaScript
/**
|
|
* 直接 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',
|
|
);
|
|
});
|