From b45437e5275e01a0170078e1feca8fcd975de44a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Thu, 17 Sep 2026 06:06:05 +0000 Subject: [PATCH] =?UTF-8?q?test(issue-link):=20=E8=A6=86=E8=93=8B=E5=85=A9?= =?UTF-8?q?=E7=A8=AE=E7=9B=B8=E4=BE=9D=E3=80=81=E5=86=AA=E7=AD=89=E8=88=87?= =?UTF-8?q?=E8=87=AA=E6=88=91=E7=9B=B8=E4=BE=9D?= 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-link.test.js | 165 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 165 insertions(+) create mode 100644 test/issue-link.test.js diff --git a/test/issue-link.test.js b/test/issue-link.test.js new file mode 100644 index 0000000..adf4a7f --- /dev/null +++ b/test/issue-link.test.js @@ -0,0 +1,165 @@ +/** + * 議題相依:把工作包之間的阻擋/先決關係建到 Gitea 原生的相依上, + * 讓看板呈現真實的開發順序,而不是一堆平鋪的議題。 + */ +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; + +/** 預設情境:這顆議題目前沒有任何相依 */ +function routes(overrides = {}) { + return healthyRoutes(REPO, { + [`GET /api/v1/repos/${REPO}/issues/${INDEX}/dependencies`]: { status: 200, body: [] }, + [`GET /api/v1/repos/${REPO}/issues/${INDEX}/blocks`]: { status: 200, body: [] }, + [`POST /api/v1/repos/${REPO}/issues/${INDEX}/dependencies`]: { status: 201, body: {} }, + [`POST /api/v1/repos/${REPO}/issues/${INDEX}/blocks`]: { status: 201, body: {} }, + ...overrides, + }); +} + +const withStub = (t, overrides = {}) => withStubGitea(t, routes(overrides)); + +const run = (args, stub) => + runScript('issue-link.js', ['--repo', REPO, '--index', String(INDEX), ...args], { + env: envFor(stub), + }); + +const posts = (stub, kind) => + stub.requests.filter((r) => r.method === 'POST' && r.path.endsWith(`/${kind}`)); + +// ── 建立相依 ─────────────────────────────────────────────────────── + +test('--depends 建立「這顆被誰擋住」的相依', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run(['--depends', '10,11'], stub); + + assert.equal(code, 0); + assert.equal(json.ok, true); + assert.deepEqual(posts(stub, 'dependencies').map((r) => r.body.index), [10, 11]); +}); + +test('--blocks 建立「這顆擋住誰」的相依', async (t) => { + const stub = await withStub(t); + + await run(['--blocks', '13'], stub); + + assert.deepEqual(posts(stub, 'blocks').map((r) => r.body.index), [13]); +}); + +test('請求帶上 owner 與 repo:Gitea 的相依端點少了它們會回 404', async (t) => { + const stub = await withStub(t); + + await run(['--depends', '10'], stub); + + const [post] = posts(stub, 'dependencies'); + assert.deepEqual(post.body, { owner: 'plugins', repo: 'tea-sdlc', index: 10 }); +}); + +test('兩種相依可以一次建立', async (t) => { + const stub = await withStub(t); + + const { json } = await run(['--depends', '10', '--blocks', '13'], stub); + + assert.deepEqual(json.data.added, { depends: [10], blocks: [13] }); +}); + +test('回傳這顆議題最後的相依狀態', async (t) => { + const stub = await withStub(t); + + const { json } = await run(['--depends', '10'], stub); + + assert.equal(json.data.index, INDEX); + assert.equal(json.data.repo, REPO); +}); + +// ── 冪等 ─────────────────────────────────────────────────────────── + +test('已經存在的相依不重建', async (t) => { + const stub = await withStub(t, { + [`GET /api/v1/repos/${REPO}/issues/${INDEX}/dependencies`]: { + status: 200, + body: [{ number: 10 }], + }, + }); + + const { json } = await run(['--depends', '10,11'], stub); + + assert.deepEqual(posts(stub, 'dependencies').map((r) => r.body.index), [11], '只補缺的那一個'); + assert.deepEqual(json.data.added.depends, [11]); + assert.deepEqual(json.data.skipped.depends, [10]); +}); + +test('全部都已存在時一個請求都不發', async (t) => { + const stub = await withStub(t, { + [`GET /api/v1/repos/${REPO}/issues/${INDEX}/dependencies`]: { + status: 200, + body: [{ number: 10 }], + }, + }); + + const { code, json } = await run(['--depends', '10'], stub); + + assert.equal(code, 0); + assert.deepEqual(posts(stub, 'dependencies'), []); + assert.deepEqual(json.data.added.depends, []); +}); + +// ── 參數 ─────────────────────────────────────────────────────────── + +test('沒給任何相依時就不必問 Gitea', async (t) => { + const stub = await withStub(t); + + const { code, json } = await run([], stub); + + assert.equal(code, 1); + assert.equal(json.error.code, 'NOTHING_TO_LINK'); + assert.equal(stub.requests.length, 0); +}); + +test('相依編號不是正整數時擋在打 Gitea 之前', async (t) => { + const stub = await withStub(t); + + const { json } = await run(['--depends', '10,abc'], stub); + + assert.equal(json.error.code, 'BAD_INDEX'); + assert.equal(stub.requests.length, 0); +}); + +test('不能把議題設成自己的先決', async (t) => { + const stub = await withStub(t); + + const { json } = await run(['--depends', String(INDEX)], stub); + + assert.equal(json.error.code, 'SELF_DEPENDENCY'); + assert.equal(stub.requests.length, 0); +}); + +// ── --dry-run ───────────────────────────────────────────────────── + +test('--dry-run 列出將建立的相依,且不發出任何寫入請求', async (t) => { + const stub = await withStub(t, { + [`GET /api/v1/repos/${REPO}/issues/${INDEX}/dependencies`]: { + status: 200, + body: [{ number: 10 }], + }, + }); + + const { code, json } = await run(['--depends', '10,11', '--blocks', '13', '--dry-run'], stub); + + assert.equal(code, 0); + assert.equal(json.data.dryRun, true); + assert.deepEqual( + json.data.requests.map((r) => `${r.method} ${r.path} ${r.body.index}`), + [ + `POST /repos/${REPO}/issues/${INDEX}/dependencies 11`, + `POST /repos/${REPO}/issues/${INDEX}/blocks 13`, + ], + '已存在的 10 不該出現在預覽裡', + ); + assert.equal(stub.requests.some((r) => r.method !== 'GET'), false); +});