Files
tea-sdlc/test/schedule.test.js
T
jiantw83andClaude Opus 5 b09746a309 test(排程): 覆蓋重複 index
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 06:06:11 +00:00

203 lines
6.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 截止日推算:依相依關係拓撲排序,任一工作包的截止日不得早於它的先決工作包。
* 這支腳本不碰 Gitea,純算數字,所以測試只餵檔案、只看 stdout。
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { runScript, tmpRoot } from './helpers/run-script.js';
/** 把一份排程計畫寫成暫存檔,回傳路徑 */
function writePlan(plan) {
mkdirSync(tmpRoot, { recursive: true });
const dir = mkdtempSync(join(tmpRoot, 'plan-'));
const path = join(dir, 'plan.json');
writeFileSync(path, JSON.stringify(plan));
return path;
}
const run = (plan, args = []) =>
runScript('schedule.js', ['--plan-file', writePlan(plan), ...args]);
/** 依 index 取出算出來的截止日 */
const dueOf = (json) =>
Object.fromEntries(json.data.schedule.map((wp) => [wp.index, wp.dueDate]));
test('沒有相依時,每顆各自從起始日加上自己的人天', async () => {
const { code, json } = await run({
startDate: '2026-09-21',
workPackages: [
{ index: 1, title: '甲', days: 2 },
{ index: 2, title: '乙', days: 5 },
],
});
assert.equal(code, 0);
assert.deepEqual(dueOf(json), { 1: '2026-09-23', 2: '2026-09-26' });
});
test('有先決時,截止日從先決的截止日往後算', async () => {
const { json } = await run({
startDate: '2026-09-21',
workPackages: [
{ index: 1, title: '甲', days: 2 },
{ index: 2, title: '乙', days: 3, depends: [1] },
],
});
assert.deepEqual(dueOf(json), { 1: '2026-09-23', 2: '2026-09-26' });
});
test('多個先決時取最晚的那一個當起點', async () => {
const { json } = await run({
startDate: '2026-09-21',
workPackages: [
{ index: 1, title: '甲', days: 2 },
{ index: 2, title: '乙', days: 9 },
{ index: 3, title: '丙', days: 1, depends: [1, 2] },
],
});
assert.equal(dueOf(json)[3], '2026-10-01', '要等最晚的乙做完才開始');
});
test('任一工作包的截止日都不早於它的先決', async () => {
const { json } = await run({
startDate: '2026-09-21',
workPackages: [
{ index: 1, title: '甲', days: 3 },
{ index: 2, title: '乙', days: 1, depends: [1] },
{ index: 3, title: '丙', days: 1, depends: [2] },
{ index: 4, title: '丁', days: 4, depends: [1] },
{ index: 5, title: '戊', days: 2, depends: [3, 4] },
],
});
const due = dueOf(json);
const depends = { 2: [1], 3: [2], 4: [1], 5: [3, 4] };
for (const [index, prerequisites] of Object.entries(depends)) {
for (const p of prerequisites) {
assert.ok(due[index] > due[p], `#${index} 的截止日不該早於先決 #${p}`);
}
}
});
test('輸出的順序即拓撲順序,先決一定排在後續之前', async () => {
const { json } = await run({
startDate: '2026-09-21',
workPackages: [
{ index: 3, title: '丙', days: 1, depends: [2] },
{ index: 1, title: '甲', days: 1 },
{ index: 2, title: '乙', days: 1, depends: [1] },
],
});
assert.deepEqual(json.data.order, [1, 2, 3]);
});
test('相依成環時中止,並指出環上的成員', async () => {
const { code, json } = await run({
startDate: '2026-09-21',
workPackages: [
{ index: 1, title: '甲', days: 1, depends: [2] },
{ index: 2, title: '乙', days: 1, depends: [1] },
],
});
assert.equal(code, 1);
assert.equal(json.error.code, 'CYCLE_DETECTED');
assert.match(json.error.message, /1/);
assert.match(json.error.message, /2/);
});
test('先決指向不存在的工作包時中止', async () => {
const { code, json } = await run({
startDate: '2026-09-21',
workPackages: [{ index: 1, title: '甲', days: 1, depends: [99] }],
});
assert.equal(code, 1);
assert.equal(json.error.code, 'UNKNOWN_DEPENDENCY');
assert.match(json.error.message, /99/);
});
test('人天必須是正數', async () => {
const { json } = await run({
startDate: '2026-09-21',
workPackages: [{ index: 1, title: '甲', days: 0 }],
});
assert.equal(json.error.code, 'BAD_PLAN');
assert.match(json.error.message, /days/);
});
test('起始日格式不對時中止', async () => {
const { json } = await run({
startDate: '2026/09/21',
workPackages: [{ index: 1, title: '甲', days: 1 }],
});
assert.equal(json.error.code, 'BAD_PLAN');
assert.match(json.error.message, /startDate/);
});
test('計畫檔不存在時帶著路徑失敗', async () => {
const { code, json } = await runScript('schedule.js', ['--plan-file', '/nonexistent/plan.json']);
assert.equal(code, 1);
assert.equal(json.error.code, 'PLAN_FILE_MISSING');
assert.match(json.error.message, /\/nonexistent\/plan\.json/);
});
test('計畫檔不是合法 JSON 時中止', async () => {
mkdirSync(tmpRoot, { recursive: true });
const dir = mkdtempSync(join(tmpRoot, 'plan-'));
const path = join(dir, 'plan.json');
writeFileSync(path, '{ 這不是 JSON');
const { json } = await runScript('schedule.js', ['--plan-file', path]);
assert.equal(json.error.code, 'BAD_PLAN');
});
test('輸出保留標題與人天,呼叫端不必回頭對照計畫檔', async () => {
const { json } = await run({
startDate: '2026-09-21',
workPackages: [{ index: 7, title: '建立抽取契約', days: 3 }],
});
assert.deepEqual(json.data.schedule[0], {
index: 7,
title: '建立抽取契約',
days: 3,
dueDate: '2026-09-24',
});
});
test('這支腳本完全不碰 Gitea:沒有登入資訊也能算', async () => {
const { code } = await runScript(
'schedule.js',
['--plan-file', writePlan({ startDate: '2026-09-21', workPackages: [{ index: 1, title: '甲', days: 1 }] })],
{ env: { TEA_SDLC_CONFIG: '/nonexistent/tea.yml' } },
);
assert.equal(code, 0);
});
test('同一個 index 出現兩次時中止,不把兩份定義混在一起算', async () => {
// 混出來的結果會是:相依看後者、標題與人天取前者,而且完全不報錯
const { code, json } = await run({
startDate: '2026-09-21',
workPackages: [
{ index: 1, title: '第一份定義', days: 1 },
{ index: 1, title: '第二份定義', days: 100 },
{ index: 2, title: '乙', days: 1, depends: [1] },
],
});
assert.equal(code, 1);
assert.equal(json.error.code, 'BAD_PLAN');
assert.match(json.error.message, /不只一次/);
});