// server/lib/scheduling.test.ts import { test } from "node:test"; import assert from "node:assert/strict"; import { parseWorkingHours, isoDayOfWeek, getWorkingHoursForDate, normalizeText, tokens, specialtyMatch, overlaps, hasConflict, scoreCandidate, } from "./scheduling.ts"; test("parseWorkingHours: json válido → mapa 1..7", () => { const m = parseWorkingHours(JSON.stringify({ 1: { start: "09:00", end: "20:00" }, 6: null, 7: null })); assert.equal(m?.[1]?.start, "09:00"); assert.equal(m?.[6], null); assert.equal(m?.[7], null); assert.equal(m?.[2], null); // ausente → null }); test("parseWorkingHours: null/invalid → null", () => { assert.equal(parseWorkingHours(null), null); assert.equal(parseWorkingHours("no-json"), null); }); test("isoDayOfWeek: lunes=1, domingo=7", () => { assert.equal(isoDayOfWeek(new Date("2026-07-27T12:00:00")), 1); // lunes assert.equal(isoDayOfWeek(new Date("2026-07-26T12:00:00")), 7); // domingo }); test("getWorkingHoursForDate: empleado tiene prioridad sobre negocio", () => { const emp = parseWorkingHours(JSON.stringify({ 1: { start: "10:00", end: "14:00" } })); const biz = parseWorkingHours(JSON.stringify({ 1: { start: "09:00", end: "20:00" } })); const r = getWorkingHoursForDate(emp, biz, new Date("2026-07-27T12:00:00")); // lunes assert.equal(r?.startMs, new Date(2026, 6, 27, 10, 0).getTime()); }); test("getWorkingHoursForDate: día cerrado → null", () => { const biz = parseWorkingHours(JSON.stringify({ 6: null, 7: null })); // fin de semana cerrado assert.equal(getWorkingHoursForDate(null, biz, new Date("2026-07-25T12:00:00")), null); // sábado }); test("normalizeText/tokens: quita acentos y lowercase", () => { assert.equal(normalizeText("Coloración"), "coloracion"); assert.deepEqual(tokens("Corte de cabello"), ["corte", "de", "cabello"]); }); test("specialtyMatch: coincidencia exacta de etiqueta → 1", () => { assert.equal(specialtyMatch(["corte"], { name: "Corte de cabello", category: "Cabello" }), 1); }); test("specialtyMatch: etiqueta con acento/case → 1", () => { assert.equal(specialtyMatch(["Coloración"], { name: "Tinte", category: "Coloración" }), 1); }); test("specialtyMatch: sin etiquetas → 0.5", () => { assert.equal(specialtyMatch([], { name: "Corte", category: "Cabello" }), 0.5); }); test("specialtyMatch: etiqueta no relacionada → 0.5", () => { assert.equal(specialtyMatch(["barba"], { name: "Manicura", category: "Uñas" }), 0.5); }); test("overlaps: bordes inclusivos de no-traslape", () => { // una cita termina exactamente cuando empieza otra → NO traslape assert.equal(overlaps(100, 200, 200, 300), false); // traslape real assert.equal(overlaps(100, 250, 200, 300), true); assert.equal(overlaps(200, 300, 100, 250), true); }); test("hasConflict: lista vacía → false", () => { assert.equal(hasConflict([], 100, 200), false); }); test("scoreCandidate: pesos 50/30/20", () => { // todo al máximo → 100 assert.equal(scoreCandidate({ specialtyMatch: 1, efficiency: 1, loadBalance: 0 }), 100); // todo al mínimo → 0 assert.equal(scoreCandidate({ specialtyMatch: 0, efficiency: 0, loadBalance: 1 }), 0); // sólo specialty (0.5) → 25 assert.equal(scoreCandidate({ specialtyMatch: 0.5, efficiency: 0, loadBalance: 1 }), 25); }); test("scoreCandidate: mayor efficiency → mayor score", () => { const low = scoreCandidate({ specialtyMatch: 0.5, efficiency: 0.2, loadBalance: 0.5 }); const high = scoreCandidate({ specialtyMatch: 0.5, efficiency: 0.9, loadBalance: 0.5 }); assert.ok(high > low); }); test("scoreCandidate: menor load (más disponible) → mayor score", () => { const busy = scoreCandidate({ specialtyMatch: 0.5, efficiency: 0.5, loadBalance: 0.9 }); const free = scoreCandidate({ specialtyMatch: 0.5, efficiency: 0.5, loadBalance: 0.1 }); assert.ok(free > busy); });