/** * 工時報表的流程正本與輸出模板。 * * 報表的難處是規則而不是程式:期間怎麼切、落差怎麼讀、印到哪裡為止。 * 規則寫在正本裡,錯了不會有任何測試自己爆掉,所以在這裡釘住。 */ 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, /週報沒有分段/); });