Files
tea-sdlc/test/timer.test.js
T
jiantw83andClaude Opus 5 74b4ca130e feat(timer): 碼錶移出領取,等工作樹建成之後才起
原本的順序是「放行 → 設 assignee 與標籤 → 起錶 → 處理分支」,而工作樹建立
失敗會中止整個領取——錶已經起了才失敗,使用者會被計一段什麼都沒做的時間,
而工時要準正是工時報表的立足點。

claim 只留領取鎖的兩件事(assignee 與標籤),起錶交給新的 timer.js,由流程
正本排在 branch-prep 之後。timer 已經跑在這顆議題上時什麼都不做:中斷後重跑
是它最常見的處境,重新起錶會把已經累積的時間切成兩段;跑在別顆上則照舊擋下,
不代勞停錶。

三支腳本讀碼錶的那段各留一份,趁這次收進 lib。

議題 #40

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:27:43 +08:00

145 lines
5.2 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.
/**
* 起錶。
*
* 錶是工時報表的唯一來源,所以這一支的價值全在「什麼時候不該起」:工作樹還沒建好
* 不該起(那由流程的順序保證),自己的錶已經跑在別顆議題上更不該起——那會把兩顆
* 工作包的時間攪在一起。停錶一律由使用者自己來,這裡不提供。
*/
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 = 40;
/** 議題上跑著的碼錶長什麼樣 */
const stopwatchOn = (index, repo = REPO) => ({
issue_index: index,
repo_owner_name: repo.split('/')[0],
repo_name: repo.split('/')[1],
});
function routes(overrides = {}, { stopwatches = [] } = {}) {
return healthyRoutes(REPO, {
[`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: {
status: 200,
body: {
number: INDEX,
title: '以 worktree 建立工作包分支並備妥隔離環境',
html_url: `https://gitea.jsc.idv.tw/${REPO}/issues/${INDEX}`,
},
},
'GET /api/v1/user/stopwatches': { status: 200, body: stopwatches },
[`POST /api/v1/repos/${REPO}/issues/${INDEX}/stopwatch/start`]: { status: 201, body: {} },
...overrides,
});
}
const withStub = (t, overrides = {}, options) => withStubGitea(t, routes(overrides, options));
const run = (args, stub) =>
runScript('timer.js', ['--repo', REPO, '--index', String(INDEX), ...args], { env: envFor(stub) });
/** 會改動 Gitea 的請求;前置檢查打在 issues/0 的探針不算(見 lib 的 checkIssueWrite) */
const writes = (stub) =>
stub.requests.filter((r) => r.method !== 'GET').filter((r) => !r.path.endsWith('/issues/0'));
test('沒有錶在跑時起錶,並回報起在哪一顆上', async (t) => {
const stub = await withStub(t);
const { code, json } = await run([], stub);
assert.equal(code, 0, json.error?.message);
assert.equal(json.data.碼錶中, true);
assert.equal(json.data.index, INDEX);
assert.deepEqual(
writes(stub).map((r) => `${r.method} ${r.path}`),
[`POST /api/v1/repos/${REPO}/issues/${INDEX}/stopwatch/start`],
);
});
test('錶已經跑在這顆議題上時什麼都不做,重跑不會把計時打斷', async (t) => {
const stub = await withStub(t, {}, { stopwatches: [stopwatchOn(INDEX)] });
const { code, json } = await run([], stub);
assert.equal(code, 0, json.error?.message);
assert.equal(json.data.碼錶中, true);
assert.equal(json.data.已在計時, true, '要如實說這顆本來就在計時,不要假裝是這次起的');
assert.deepEqual(writes(stub), [], '重新起錶會把已經累積的時間切成兩段');
});
test('錶跑在別顆議題上時擋下,並指出是哪一顆', async (t) => {
const stub = await withStub(t, {}, { stopwatches: [stopwatchOn(7)] });
const { code, json } = await run([], stub);
assert.equal(code, 1);
assert.equal(json.error.code, 'STOPWATCH_ON_OTHER_ISSUE');
assert.match(json.error.message, /#7/, '忘了停掉的是哪一顆,要指名');
assert.deepEqual(writes(stub), [], '擋下來就不該寫進任何東西');
});
test('別的 repo 上的同號碼錶也算自己有錶在跑', async (t) => {
const stub = await withStub(t, {}, { stopwatches: [stopwatchOn(INDEX, 'plugins/別的專案')] });
const { json } = await run([], stub);
assert.equal(json.error.code, 'STOPWATCH_ON_OTHER_ISSUE');
assert.match(json.error.message, /別的專案/);
});
test('不代替使用者停錶:訊息要說清楚下一步是他自己去停', async (t) => {
const stub = await withStub(t, {}, { stopwatches: [stopwatchOn(7)] });
const { json } = await run([], stub);
assert.match(json.error.message, /停/);
});
test('議題不存在時回可區分的錯誤碼', async (t) => {
const stub = await withStub(t, {
[`GET /api/v1/repos/${REPO}/issues/${INDEX}`]: { status: 404, body: { message: 'not found' } },
});
const { json } = await run([], stub);
assert.equal(json.error.code, 'ISSUE_NOT_FOUND');
assert.deepEqual(writes(stub), []);
});
test('--index 不是正整數時擋在打 Gitea 之前', async (t) => {
const stub = await withStub(t);
const { json } = await runScript('timer.js', ['--repo', REPO, '--index', '0'], {
env: envFor(stub),
});
assert.equal(json.error.code, 'BAD_INDEX');
assert.equal(stub.requests.length, 0);
});
test('--dry-run 印出將發出的寫入,但一個字都不寫進去', async (t) => {
const stub = await withStub(t);
const { code, json } = await run(['--dry-run'], stub);
assert.equal(code, 0);
assert.equal(json.data.dryRun, true);
assert.deepEqual(
json.data.requests.map((r) => `${r.method} ${r.path}`),
[`POST /repos/${REPO}/issues/${INDEX}/stopwatch/start`],
);
assert.deepEqual(writes(stub), [], '預覽不得真的寫入');
});
test('--dry-run 會先讀現況:已經在計時時預覽出來就是什麼都不做', async (t) => {
const stub = await withStub(t, {}, { stopwatches: [stopwatchOn(INDEX)] });
const { json } = await run(['--dry-run'], stub);
assert.deepEqual(json.data.requests, [], '手寫一份固定的清單會跟實作走鐘');
assert.equal(json.data.已在計時, true);
});