Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
166 lines
5.7 KiB
JavaScript
166 lines
5.7 KiB
JavaScript
/**
|
||
* 議題相依:把工作包之間的阻擋/先決關係建到 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);
|
||
});
|