/** * 議題相依:把工作包之間的阻擋/先決關係建到 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); });