test(工時報表): 釘住週次歸屬的跨月、跨年與五個週五

時區在測試裡固定為 Asia/Taipei:週界是以人在的時區切的,不釘住時區就等於沒釘住答案。
--today 讓「本週」在 CLI 接縫上釘得住,否則預設期間會跟著系統時鐘漂走。

寫入請求那則斷言誠實列出唯一的非 GET——四層前置檢查的寫入權探針,
而不是先把它濾掉再宣稱整趟沒有寫入。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-17 06:50:35 +00:00
co-authored by Claude Opus 5
parent 57e45dd63e
commit b56216e0f0
2 changed files with 623 additions and 0 deletions
+109
View File
@@ -0,0 +1,109 @@
/**
* 工時報表的流程正本與輸出模板。
*
* 報表的難處是規則而不是程式:期間怎麼切、落差怎麼讀、印到哪裡為止。
* 規則寫在正本裡,錯了不會有任何測試自己爆掉,所以在這裡釘住。
*/
import test from 'node:test';
import assert from 'node:assert/strict';
import {
assertNeutralPrompt,
assertTemplateSections,
readPrompt,
readTemplate,
} from './helpers/prompt-doc.js';
const template = readTemplate('report');
const prompt = readPrompt('sdlc-report');
/** 報表的四個段落,順序即印出來的順序 */
const SECTIONS = ['總計', '分段小計', '逐議題', '附註'];
// ── 輸出模板 ───────────────────────────────────────────────────────
test('模板依序包含四個段落', () => {
assertTemplateSections(template, SECTIONS);
});
test('模板以 {{變數}} 佔位,不留任何空白待填欄位', () => {
const placeholders = [...template.matchAll(/\{\{([^}]+)\}\}/g)].map((m) => m[1]);
assert.ok(placeholders.length >= SECTIONS.length, '每個段落至少要有一個佔位');
for (const name of placeholders) {
assert.match(name, /^[a-z一-龥]+$/u, `佔位名稱 ${name} 應為單一詞,不含空白或符號`);
}
});
test('模板不含邏輯:沒有任何條件或迴圈語法', () => {
assert.equal(/\{\{[#/^]/.test(template), false, '分段與逐議題由呼叫端展開,模板不做迴圈');
});
test('總計把實際、估算與落差擺在同一列,落差不必自己算', () => {
const totals = template.slice(template.indexOf('## 總計'), template.indexOf('## 分段小計'));
for (const placeholder of ['{{實際工時}}', '{{估算人天}}', '{{已估實際}}', '{{落差}}']) {
assert.ok(totals.includes(placeholder), `總計缺少 ${placeholder}`);
}
});
// ── 流程正本 ───────────────────────────────────────────────────────
test('正本平台中立,description 前綴正確', () => {
assertNeutralPrompt(prompt, 'sdlc-report');
});
test('正本交代三種期間,並指明預設是本週', () => {
assert.match(prompt, /--week/);
assert.match(prompt, /--month YYYY-MM/);
assert.match(prompt, /--year YYYY/);
assert.match(prompt, /預設/);
});
test('正本釘住期間定義的三句話', () => {
assert.match(prompt, /一週為週一至週日/);
assert.match(prompt, /該週週五所屬月份/);
assert.match(prompt, /W1[–-]W5.*第幾個週五/s);
});
test('正本以實例說明跨月與跨年那一週落在哪邊', () => {
assert.match(prompt, /2025-12-29/, '跨年的例子要寫出具體日期,規則才驗得出來');
assert.match(prompt, /2026-02-01/, '跨月的例子要寫出具體日期');
});
test('正本指名由 report.js 取數字,並要求直接用算好的時分', () => {
assert.match(prompt, /report\.js/);
assert.match(prompt, /不要自己再乘|已經算好/);
});
test('正本明令只印在終端,不張貼到任何管道', () => {
assert.match(prompt, /只印在終端/);
assert.match(prompt, /不(要)?張貼/);
});
test('正本說明落差的正負方向,以及一人天等於幾小時', () => {
assert.match(prompt, /正數.*超出估算/);
assert.match(prompt, /8\s*小時/);
assert.match(prompt, /--day-hours/);
});
test('正本交代總計的落差只涵蓋有估算的議題', () => {
assert.match(prompt, /總計的落差只涵蓋有估算的議題/);
assert.match(prompt, /已估實際秒/, '要指名分子是哪一個欄位,否則會被讀成全部實際');
});
test('正本交代沒寫估算時落差是空的,不是零', () => {
assert.match(prompt, /不是零|非零|空的/);
assert.match(prompt, /null/);
});
test('正本交代略過的工時筆數要講出來', () => {
assert.match(prompt, /略過/);
});
test('正本的邊界寫明這是唯讀流程', () => {
const boundary = prompt.slice(prompt.indexOf('## 邊界'));
assert.match(boundary, /不寫入|只發 GET/);
assert.match(boundary, /不補登|不動碼錶/);
});
test('正本交代週報沒有分段小計時整段不印', () => {
assert.match(prompt, /週報沒有分段/);
});