All source files · Workbench

scripts/test-planner.mjs

import assert from 'node:assert/strict';
import { localTimeToUtc, scheduleItems, exportCalendar, fingerprintWorkInputs } from '../src/lib/planner.ts';

const iso=(wall,zone)=>new Date(localTimeToUtc(wall,zone)).toISOString();
assert.equal(iso('2026-10-05T01:59','America/Chicago'),'2026-10-05T06:59:00.000Z');
assert.equal(iso('2026-10-04T23:59','America/Los_Angeles'),'2026-10-05T06:59:00.000Z');
assert.equal(iso('2026-10-05T12:29','Asia/Kolkata'),'2026-10-05T06:59:00.000Z');
assert.equal(iso('2026-12-01T01:59','America/Chicago'),'2026-12-01T07:59:00.000Z');
const start=Date.parse('2026-10-02T18:00:00Z');
const items=[{id:'late',title:'Late, source; version',deadlineUtc:'2026-10-04T18:00:00Z',buildHours:8,humanMinutes:30},{id:'early',title:'早い deadline',deadlineUtc:'2026-10-03T06:00:00Z',buildHours:4,humanMinutes:0}];
const plan=scheduleItems(items,start,12);
assert.deepEqual(plan.map(item=>item.id),['early','late']);
assert.equal(plan[0].end,start+8*3600000);
assert.equal(plan[1].end,start+24.5*3600000);
assert.equal(plan[0].slackHours,4);
const changed=scheduleItems(items,start,12,{id:'late',deadline:start+4*3600000});
assert.equal(changed[0].id,'late');assert.equal(changed[0].late,true);
const calendar=exportCalendar(plan,start);
assert.match(calendar,/DTSTART:20261002T180000Z/);
assert.match(calendar,/SUMMARY:Build: Late\\, source\\; version/);
assert.equal(calendar.split('BEGIN:VEVENT').length-1,2);
for(const line of calendar.split('\r\n'))assert.ok(Buffer.byteLength(line,'utf8')<=75);
assert.throws(()=>scheduleItems(items,start,0));
const sourceItem={id:'source',title:'Source defaults',deadlineUtc:'2026-10-05T06:59:00Z',buildHours:4,humanMinutes:20};
const originalFingerprint=fingerprintWorkInputs([sourceItem]);
const originalEnd=scheduleItems([sourceItem],start,12)[0].end;
const moreHours={...sourceItem,buildHours:6};
assert.notEqual(scheduleItems([moreHours],start,12)[0].end,originalEnd);
assert.notEqual(fingerprintWorkInputs([moreHours]),originalFingerprint,'A changed remote effort default must invalidate the decision.');
const moreMinutes={...sourceItem,humanMinutes:30};
assert.notEqual(scheduleItems([moreMinutes],start,12)[0].end,originalEnd);
assert.notEqual(fingerprintWorkInputs([moreMinutes]),originalFingerprint,'Changed remote human minutes must invalidate the decision.');
const overridden=new Map([['source',8]]);
assert.equal(fingerprintWorkInputs([sourceItem],overridden),fingerprintWorkInputs([moreHours],overridden),'An unused remote effort default must not invalidate an explicit override.');
assert.equal(scheduleItems([{...sourceItem,buildHours:8}],start,12)[0].end,scheduleItems([{...moreHours,buildHours:8}],start,12)[0].end);
assert.notEqual(fingerprintWorkInputs([sourceItem],overridden),fingerprintWorkInputs([moreMinutes],overridden),'Human minutes still affect an overridden work estimate.');
console.log('Planner verification passed: deadline timezones, winter offset, ordering, capacity, revision impact, UTC calendar, text escaping, UTF-8 folding, remote effort/minutes invalidation and override preservation.');