Files
Meeting-Mixer/tests/algorithm.test.mjs
T
ferdi2goandClaude Opus 4.8 e851bb2833 fix(calendar): preserve manual picks when regenerating a month (#2)
generatePlan gains a preAssigned parameter: manual entries occupy slots,
count toward eligibility (maxPerMonth/minGap) and are never double-booked;
only the remaining slots per day are auto-filled. The generate handler now
keeps manual:true entries for the month instead of wiping them.

Adds tests for slot occupation, fully-manual days, and maxPerMonth counting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 12:17:22 +00:00

218 lines
8.8 KiB
JavaScript

import assert from 'node:assert/strict';
import { getWorkdays, generatePlan } from '../js/algorithm.js';
const fixed = () => 0.5; // deterministic random for tests
function emp(id, constraints = {}) {
return {
id, name: id,
constraints: {
neverDays: [], lowPriorityDays: [], onlyDays: [],
maxPerMonth: null, minGapDays: 0, vacations: [],
...constraints
}
};
}
// getWorkdays
{
const days = getWorkdays(2026, 4, [], []);
assert.equal(days.length, 22, 'April 2026: 22 workdays');
assert.equal(days[0], '2026-04-01', 'First workday = 2026-04-01');
assert.equal(days.at(-1), '2026-04-30', 'Last workday = 2026-04-30');
}
{
const days = getWorkdays(2026, 4, ['2026-04-01'], []);
assert.equal(days.length, 21, 'Holiday reduces count by 1');
assert.ok(!days.includes('2026-04-01'), 'Holiday excluded');
}
{
const days = getWorkdays(2026, 4, [], []);
assert.ok(!days.includes('2026-04-04'), '2026-04-04 Saturday excluded');
assert.ok(!days.includes('2026-04-05'), '2026-04-05 Sunday excluded');
}
// generatePlan — basic assignment
{
const result = generatePlan(['2026-04-01'], [emp('alice'), emp('bob')], [], fixed);
assert.equal(result.assignments.length, 1, 'One workday → one assignment');
assert.ok(['alice','bob'].includes(result.assignments[0].employeeId), 'Assigned to alice or bob');
}
// neverDays blocks employee
{
// 2026-04-01 is Wednesday (3)
const result = generatePlan(['2026-04-01'], [emp('alice', {neverDays:[3]}), emp('bob')], [], fixed);
assert.equal(result.assignments[0].employeeId, 'bob', 'alice blocked on Wednesday → bob assigned');
}
// onlyDays restricts to specific days
{
// 2026-04-01 = Wednesday (3), 2026-04-02 = Thursday (4)
const employees = [emp('alice', {onlyDays:[3]}), emp('bob')];
const result = generatePlan(['2026-04-01','2026-04-02'], employees, [], fixed);
const aliceOnThursday = result.assignments.find(r => r.date === '2026-04-02' && r.employeeId === 'alice');
assert.ok(!aliceOnThursday, 'alice with onlyDays:[3] not assigned on Thursday');
}
// vacation blocks employee
{
const employees = [
emp('alice', {vacations:[{from:'2026-04-01', to:'2026-04-03'}]}),
emp('bob')
];
const result = generatePlan(['2026-04-01'], employees, [], fixed);
assert.equal(result.assignments[0].employeeId, 'bob', 'alice on vacation → bob assigned');
}
// maxPerMonth cap
{
const days = ['2026-04-01','2026-04-02','2026-04-03'];
const employees = [emp('alice', {maxPerMonth:1}), emp('bob'), emp('clara')];
const result = generatePlan(days, employees, [], fixed);
const aliceCount = result.assignments.filter(r => r.employeeId === 'alice').length;
assert.ok(aliceCount <= 1, 'alice maxPerMonth=1 → assigned at most once');
}
// minGapDays
{
// 2026-04-06 = Monday, alice last assigned 2026-04-02 (Thu) = 4 days ago, gap=5 → blocked
const history = [{date:'2026-04-02', employeeId:'alice', manual:false}];
const employees = [emp('alice', {minGapDays:5}), emp('bob')];
const result = generatePlan(['2026-04-06'], employees, history, fixed);
assert.equal(result.assignments[0].employeeId, 'bob', 'alice within minGap (4<5) → bob assigned');
}
// No candidates → day skipped
{
// 2026-04-01 = Wednesday (3), only employee blocked
const result = generatePlan(['2026-04-01'], [emp('alice', {neverDays:[3]})], [], fixed);
assert.equal(result.assignments.length, 0, 'No candidate → day skipped');
}
// Fairness: less history = higher priority
{
const history = [1,2,3,4,5].map(i => ({
date: '2026-04-0' + i, employeeId: 'alice', manual: false
}));
const result = generatePlan(['2026-05-04'], [emp('alice'), emp('bob')], history, fixed);
assert.equal(result.assignments[0].employeeId, 'bob', 'bob (0 history) beats alice (5 history)');
}
// companyClosures excluded from workdays
{
const days = getWorkdays(2026, 4, [], ['2026-04-01']);
assert.equal(days.length, 21, 'Closure reduces count by 1');
assert.ok(!days.includes('2026-04-01'), 'Closure date excluded');
}
// lowPriorityDays reduces score — bob without penalty wins over alice with penalty
{
// 2026-04-01 = Wednesday (3); alice has lowPriorityDays on Wednesday
// Both have same history (0) — alice's score gets -5 penalty, bob wins
const employees = [emp('alice', {lowPriorityDays:[3]}), emp('bob')];
const result = generatePlan(['2026-04-01'], employees, [], fixed);
assert.equal(result.assignments[0].employeeId, 'bob', 'alice low priority on Wednesday → bob wins');
}
// manual: false on auto-assigned entries
{
const result = generatePlan(['2026-04-01'], [emp('alice')], [], fixed);
assert.equal(result.assignments[0].manual, false, 'auto-assignment sets manual: false');
}
// slotsPerDay=2 → 2 different employees per day
{
const employees = [emp('alice'), emp('bob'), emp('clara')];
const result = generatePlan(['2026-04-01'], employees, [], fixed, 2);
assert.equal(result.assignments.length, 2, 'slotsPerDay=2 → 2 entries');
const ids = result.assignments.map(a => a.employeeId);
assert.notEqual(ids[0], ids[1], 'two different employees on same day');
}
// slotsPerDay=2 across multiple days, maxPerMonth still respected
{
const days = ['2026-04-01','2026-04-02','2026-04-03'];
const employees = [
emp('alice', { maxPerMonth: 1 }),
emp('bob'),
emp('clara'),
emp('dave')
];
const result = generatePlan(days, employees, [], fixed, 2);
const aliceCount = result.assignments.filter(a => a.employeeId === 'alice').length;
assert.ok(aliceCount <= 1, 'maxPerMonth=1 respected with slotsPerDay=2');
}
// slotsPerDay=2: per day, no employee picked twice
{
const employees = [emp('alice'), emp('bob'), emp('clara')];
const result = generatePlan(['2026-04-01','2026-04-02'], employees, [], fixed, 2);
for (const date of ['2026-04-01','2026-04-02']) {
const ids = result.assignments.filter(a => a.date === date).map(a => a.employeeId);
assert.equal(new Set(ids).size, ids.length, 'no employee picked twice on ' + date);
}
}
// slotsPerDay=2 with only 1 eligible employee → day skipped, listed in skippedDates
{
const employees = [emp('alice')];
const result = generatePlan(['2026-04-01'], employees, [], fixed, 2);
assert.equal(result.assignments.length, 0, 'no assignments when candidates < slotsPerDay');
assert.deepEqual(result.skippedDates, ['2026-04-01'], 'date listed in skippedDates');
}
// slotsPerDay=1 with 0 candidates → still skipped, listed
{
// 2026-04-01 = Wednesday (3), only employee blocks Wednesday
const employees = [emp('alice', { neverDays: [3] })];
const result = generatePlan(['2026-04-01'], employees, [], fixed, 1);
assert.equal(result.assignments.length, 0, 'no assignments when 0 candidates');
assert.deepEqual(result.skippedDates, ['2026-04-01'], 'date listed in skippedDates');
}
// Mixed: some days fully assigned, some skipped
{
// 2026-04-01 = Wed, 2026-04-02 = Thu
// alice can only Wed, bob can always work
const employees = [
emp('alice', { onlyDays: [3] }),
emp('bob')
];
const result = generatePlan(['2026-04-01','2026-04-02'], employees, [], fixed, 2);
// 2026-04-01: 2 candidates → 2 entries
assert.equal(result.assignments.filter(a => a.date === '2026-04-01').length, 2, 'Wed has 2 entries');
// 2026-04-02: only bob → skipped
assert.equal(result.assignments.filter(a => a.date === '2026-04-02').length, 0, 'Thu skipped');
assert.deepEqual(result.skippedDates, ['2026-04-02'], 'Thu in skippedDates');
}
// preAssigned: manual pick occupies a slot, only remaining slots auto-filled, no double-book
{
const employees = [emp('alice'), emp('bob'), emp('clara')];
const manual = [{ date: '2026-04-01', employeeId: 'alice', manual: true }];
const result = generatePlan(['2026-04-01'], employees, [], fixed, 2, manual);
assert.equal(result.assignments.length, 1, 'slotsPerDay=2 with 1 manual → 1 auto entry');
assert.notEqual(result.assignments[0].employeeId, 'alice', 'manual employee not double-booked');
assert.equal(result.assignments[0].manual, false, 'auto entry has manual:false');
}
// preAssigned: day fully staffed by manual entries → nothing generated, not skipped
{
const employees = [emp('alice'), emp('bob')];
const manual = [{ date: '2026-04-01', employeeId: 'alice', manual: true }];
const result = generatePlan(['2026-04-01'], employees, [], fixed, 1, manual);
assert.equal(result.assignments.length, 0, 'fully manual day → no auto entries');
assert.deepEqual(result.skippedDates, [], 'fully manual day not skipped');
}
// preAssigned counts toward maxPerMonth eligibility
{
const employees = [emp('alice', { maxPerMonth: 1 }), emp('bob'), emp('clara')];
const manual = [{ date: '2026-04-01', employeeId: 'alice', manual: true }];
const result = generatePlan(['2026-04-02'], employees, [], fixed, 1, manual);
assert.notEqual(result.assignments[0].employeeId, 'alice', 'manual entry counts toward maxPerMonth');
}
console.log('All algorithm tests passed.');