勾選的測試全部繞著「會不會改錯行」打轉,五種都是 code review 抓出來的實際缺陷: 圍欄裡長得像 checkbox 的那一行不會被改到、不同段落的同一句話靠 --section 分得開、 大寫 [X] 重跑是 no-op、方框後沒有空白照樣勾得到、沒有方框的項目給的是指路的錯誤 而不是謊報已勾過。 另外釘住抽取端與勾選端的一致性:wp-extract 交得出來的每一種 raw,--tick 都要收得下。 patchOf 收進 helpers——它先前在三個測試檔裡各有一份一模一樣的定義。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
161 lines
6.4 KiB
JavaScript
161 lines
6.4 KiB
JavaScript
/**
|
|
* 把工作包放進 Gitea 專案看板。
|
|
*
|
|
* Gitea 1.27 沒有「列出專案」的 endpoint,所以專案 id 只能反查:掃最近的議題,
|
|
* 從它們身上的 projects 欄位湊出 id→名稱對照。全空時就只能請使用者貼網址。
|
|
*/
|
|
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { runScript } from './helpers/run-script.js';
|
|
import { healthyRoutes, stubEnv as envFor, withStubGitea, patchOf } from './helpers/stub-gitea.js';
|
|
|
|
const REPO = 'plugins/tea-sdlc';
|
|
const INDEX = 12;
|
|
|
|
function routes(overrides = {}, { issueProjects = [], recent = null } = {}) {
|
|
const recentIssues = recent ?? [
|
|
{ number: 9, projects: [{ id: 7, title: '開發看板' }] },
|
|
{ number: 8, projects: [{ id: 8, title: '維運看板' }] },
|
|
{ number: 7, projects: [] },
|
|
];
|
|
return healthyRoutes(REPO, {
|
|
[`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: {
|
|
status: 200,
|
|
body: { number: INDEX, projects: issueProjects, html_url: 'https://example.com/12' },
|
|
},
|
|
[`GET /api/v1/repos/${REPO}/issues`]: { status: 200, body: recentIssues },
|
|
[`PATCH /api/v1/repos/${REPO}/issues/${INDEX}`]: (req) => ({
|
|
status: 200,
|
|
body: { number: INDEX, ...req.body },
|
|
}),
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
const withStub = (t, overrides = {}, options) => withStubGitea(t, routes(overrides, options));
|
|
|
|
const run = (args, stub) =>
|
|
runScript('project-add.js', ['--repo', REPO, '--index', String(INDEX), ...args], {
|
|
env: envFor(stub),
|
|
});
|
|
|
|
|
|
// ── 以名稱指定 ─────────────────────────────────────────────────────
|
|
|
|
test('以名稱指定看板,靠掃議題反查出 id', async (t) => {
|
|
const stub = await withStub(t);
|
|
|
|
const { code, json } = await run(['--project', '開發看板'], stub);
|
|
|
|
assert.equal(code, 0, JSON.stringify(json));
|
|
assert.deepEqual(patchOf(stub).body.projects, [7]);
|
|
assert.equal(json.data.project.id, 7);
|
|
assert.equal(json.data.project.title, '開發看板');
|
|
});
|
|
|
|
test('反查只掃最近 50 筆議題', async (t) => {
|
|
const stub = await withStub(t);
|
|
|
|
await run(['--project', '開發看板'], stub);
|
|
|
|
const scan = stub.requests.find((r) => r.method === 'GET' && r.path.endsWith('/issues'));
|
|
assert.equal(scan.query.limit, '50');
|
|
assert.equal(scan.query.state, 'all');
|
|
});
|
|
|
|
test('反查不到名稱時,要求使用者貼專案網址', async (t) => {
|
|
const stub = await withStub(t);
|
|
|
|
const { code, json } = await run(['--project', '不存在的看板'], stub);
|
|
|
|
assert.equal(code, 1);
|
|
assert.equal(json.error.code, 'PROJECT_NOT_FOUND');
|
|
assert.match(json.error.message, /網址/);
|
|
assert.match(json.error.message, /開發看板/, '要列出反查到的看板');
|
|
});
|
|
|
|
test('最近的議題全都沒有看板時,說清楚為什麼查不到', async (t) => {
|
|
const stub = await withStub(t, {}, { recent: [{ number: 9, projects: [] }] });
|
|
|
|
const { json } = await run(['--project', '開發看板'], stub);
|
|
|
|
assert.equal(json.error.code, 'PROJECT_NOT_FOUND');
|
|
assert.match(json.error.message, /沒有任何一顆掛在看板上/);
|
|
assert.match(json.error.message, /請直接貼專案網址/);
|
|
});
|
|
|
|
// ── 以網址指定 ─────────────────────────────────────────────────────
|
|
|
|
test('直接貼專案網址時,取結尾的 id,不必反查', async (t) => {
|
|
const stub = await withStub(t);
|
|
|
|
const { code } = await run(['--project', 'https://gitea.jsc.idv.tw/plugins/-/projects/7'], stub);
|
|
|
|
assert.equal(code, 0);
|
|
assert.deepEqual(patchOf(stub).body.projects, [7]);
|
|
assert.equal(
|
|
stub.requests.some((r) => r.method === 'GET' && r.path.endsWith('/issues')),
|
|
false,
|
|
'網址已經帶了 id,不該再去掃議題',
|
|
);
|
|
});
|
|
|
|
test('網址結尾不是數字時中止', async (t) => {
|
|
const stub = await withStub(t);
|
|
|
|
const { json } = await run(['--project', 'https://gitea.jsc.idv.tw/plugins/-/projects/abc'], stub);
|
|
|
|
assert.equal(json.error.code, 'PROJECT_NOT_FOUND');
|
|
});
|
|
|
|
// ── 不覆蓋既有看板 ─────────────────────────────────────────────────
|
|
|
|
test('議題原本就在別的看板上時,保留它並補上新的', async (t) => {
|
|
const stub = await withStub(t, {}, { issueProjects: [{ id: 8, title: '維運看板' }] });
|
|
|
|
await run(['--project', '開發看板'], stub);
|
|
|
|
assert.deepEqual(patchOf(stub).body.projects.sort(), [7, 8], 'projects 是整份取代,漏了就等於把人踢出原看板');
|
|
});
|
|
|
|
test('已經在目標看板上時不重發 PATCH', async (t) => {
|
|
const stub = await withStub(t, {}, { issueProjects: [{ id: 7, title: '開發看板' }] });
|
|
|
|
const { code, json } = await run(['--project', '開發看板'], stub);
|
|
|
|
assert.equal(code, 0);
|
|
assert.equal(patchOf(stub), undefined);
|
|
assert.equal(json.data.alreadyThere, true);
|
|
});
|
|
|
|
// ── 不建立專案 ─────────────────────────────────────────────────────
|
|
|
|
test('不建立專案:唯一的寫入是議題本身的 PATCH,且只帶 projects', async (t) => {
|
|
// 用「路徑含 project」當條件是空話——Gitea 的專案根本沒有 API 路徑
|
|
const stub = await withStub(t);
|
|
|
|
await run(['--project', '開發看板'], stub);
|
|
|
|
// 前置檢查的寫入權探針也是 PATCH,但它打在不存在的 /issues/0 上,不算真正的寫入
|
|
const writes = stub.requests.filter(
|
|
(r) => r.method !== 'GET' && !r.path.endsWith('/issues/0'),
|
|
);
|
|
assert.equal(writes.length, 1);
|
|
assert.equal(writes[0].method, 'PATCH');
|
|
assert.equal(writes[0].path, `/api/v1/repos/${REPO}/issues/${INDEX}`);
|
|
assert.deepEqual(Object.keys(writes[0].body), ['projects']);
|
|
});
|
|
|
|
// ── --dry-run ─────────────────────────────────────────────────────
|
|
|
|
test('--dry-run 列出將送出的 PATCH,且不寫入', async (t) => {
|
|
const stub = await withStub(t);
|
|
|
|
const { code, json } = await run(['--project', '開發看板', '--dry-run'], stub);
|
|
|
|
assert.equal(code, 0);
|
|
assert.equal(json.data.dryRun, true);
|
|
assert.deepEqual(json.data.requests[0].body.projects, [7], '預覽要看得出解析出來的 id');
|
|
assert.equal(stub.requests.some((r) => r.method !== 'GET'), false);
|
|
});
|