From 3f97fe09013390009c5e67b7bc1b22de36009464 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 06:06:06 +0000 Subject: [PATCH] =?UTF-8?q?test(issue-update):=20=E8=A6=86=E8=93=8B=20Mile?= =?UTF-8?q?stone=20=E8=A7=A3=E6=9E=90=E3=80=81=E6=97=A5=E6=9C=9F=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E8=88=87=E4=BC=B0=E7=AE=97=E7=9A=84=E5=B0=B1=E5=9C=B0?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5 (1M context) --- test/issue-update.test.js | 191 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 test/issue-update.test.js diff --git a/test/issue-update.test.js b/test/issue-update.test.js new file mode 100644 index 0000000..ddc6021 --- /dev/null +++ b/test/issue-update.test.js @@ -0,0 +1,191 @@ +/** + * 掛 Milestone、寫截止日、記人天估算。 + * 三件事都經同一個 PATCH 送出,因為它們常常要一起改,分開發等於多兩次往返。 + */ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { runScript } from './helpers/run-script.js'; +import { healthyRoutes, stubEnv as envFor, withStubGitea } from './helpers/stub-gitea.js'; + +const REPO = 'plugins/tea-sdlc'; +const INDEX = 12; +const BODY = '## 這個工作包在做什麼\n\n一句話。\n\n## 關聯\n\n需求議題:#1\n'; + +function routes(overrides = {}, { body = BODY } = {}) { + return healthyRoutes(REPO, { + [`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: { + status: 200, + body: { number: INDEX, title: '領取工作包並備妥分支', body, html_url: 'https://example.com/12' }, + }, + [`GET /api/v1/repos/${REPO}/milestones`]: { + status: 200, + body: [ + { id: 3, title: '第一階段' }, + { id: 4, title: '第二階段' }, + ], + }, + [`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('issue-update.js', ['--repo', REPO, '--index', String(INDEX), ...args], { + env: envFor(stub), + }); + +const patchOf = (stub) => stub.requests.find((r) => r.method === 'PATCH' && !r.path.endsWith('/0')); + +// ── Milestone ───────────────────────────────────────────────────── + +test('以名稱指定 Milestone,送出時換成 id', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run(['--milestone', '第二階段'], stub); + + assert.equal(code, 0, JSON.stringify(json)); + assert.equal(patchOf(stub).body.milestone, 4); +}); + +test('Milestone 不存在時中止,並列出可選的', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run(['--milestone', '第三階段'], stub); + + assert.equal(code, 1); + assert.equal(json.error.code, 'UNKNOWN_MILESTONE'); + assert.match(json.error.message, /第三階段/); + assert.match(json.error.message, /第一階段/, '要列出可選的'); + assert.equal(stub.requests.some((r) => r.method === 'PATCH' && !r.path.endsWith('/0')), false); +}); + +test('不建立 Milestone:沒有任何請求寫到 milestones 端點', async (t) => { + const stub = await withStub(t); + + await run(['--milestone', '第一階段'], stub); + + const writes = stub.requests.filter((r) => r.path.endsWith('/milestones') && r.method !== 'GET'); + assert.deepEqual(writes, []); +}); + +// ── 截止日 ───────────────────────────────────────────────────────── + +test('截止日以 RFC3339 送出', async (t) => { + const stub = await withStub(t); + + await run(['--due-date', '2026-09-24'], stub); + + assert.equal(patchOf(stub).body.due_date, '2026-09-24T00:00:00Z'); +}); + +test('截止日格式不對時擋在打 Gitea 之前', async (t) => { + const stub = await withStub(t); + + const { json } = await run(['--due-date', '2026/09/24'], stub); + + assert.equal(json.error.code, 'BAD_DUE_DATE'); + assert.equal(stub.requests.length, 0); +}); + +// ── 人天估算 ─────────────────────────────────────────────────────── + +test('人天估算寫成議題 body 裡人類可讀的一行', async (t) => { + const stub = await withStub(t); + + await run(['--estimate-days', '3'], stub); + + assert.match(patchOf(stub).body.body, /^估算人天:3$/m); +}); + +test('估算寫進關聯段落,不是隨便黏在檔尾', async (t) => { + const stub = await withStub(t); + + await run(['--estimate-days', '3'], stub); + + const updated = patchOf(stub).body.body; + const relation = updated.slice(updated.indexOf('## 關聯')); + assert.match(relation, /需求議題:#1/); + assert.match(relation, /估算人天:3/); +}); + +test('重跑改估算時就地更新那一行,不會累積成兩行', async (t) => { + const stub = await withStub(t, {}, { + body: '## 關聯\n\n需求議題:#1\n估算人天:3\n', + }); + + await run(['--estimate-days', '5'], stub); + + const updated = patchOf(stub).body.body; + assert.equal((updated.match(/估算人天:/g) ?? []).length, 1); + assert.match(updated, /估算人天:5/); +}); + +test('估算沒有變時不重寫 body', async (t) => { + const stub = await withStub(t, {}, { + body: '## 關聯\n\n需求議題:#1\n估算人天:3\n', + }); + + await run(['--estimate-days', '3'], stub); + + assert.equal('body' in patchOf(stub).body, false, '沒變就不該把 body 塞進 PATCH'); +}); + +test('人天必須是正數', async (t) => { + const stub = await withStub(t); + + const { json } = await run(['--estimate-days', '0'], stub); + + assert.equal(json.error.code, 'BAD_ESTIMATE'); + assert.equal(stub.requests.length, 0); +}); + +test('議題沒有關聯段落時,估算補在 body 結尾而不是靜靜消失', async (t) => { + const stub = await withStub(t, {}, { body: '## 描述\n\n沒有關聯段落的議題。\n' }); + + await run(['--estimate-days', '2'], stub); + + assert.match(patchOf(stub).body.body, /估算人天:2/); +}); + +// ── 一起送 ───────────────────────────────────────────────────────── + +test('三件事一次 PATCH 送出', async (t) => { + const stub = await withStub(t); + + await run(['--milestone', '第一階段', '--due-date', '2026-09-24', '--estimate-days', '2'], stub); + + const patches = stub.requests.filter((r) => r.method === 'PATCH' && !r.path.endsWith('/0')); + assert.equal(patches.length, 1); + assert.equal(patches[0].body.milestone, 3); + assert.equal(patches[0].body.due_date, '2026-09-24T00:00:00Z'); + assert.match(patches[0].body.body, /估算人天:2/); +}); + +test('什麼都沒指定時中止,不發空的 PATCH', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run([], stub); + + assert.equal(code, 1); + assert.equal(json.error.code, 'NOTHING_TO_UPDATE'); + assert.equal(stub.requests.length, 0); +}); + +// ── --dry-run ───────────────────────────────────────────────────── + +test('--dry-run 列出將送出的 PATCH,且不寫入', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run(['--milestone', '第一階段', '--due-date', '2026-09-24', '--dry-run'], stub); + + assert.equal(code, 0); + assert.equal(json.data.dryRun, true); + assert.equal(json.data.requests.length, 1); + assert.equal(json.data.requests[0].body.milestone, 3, '預覽要看得出 Milestone 已解析成 id'); + assert.equal(stub.requests.some((r) => r.method !== 'GET'), false); +});