// Shared schedule profiles (the listener pattern): one derivation per // Schedule, read by every scorer. import { describe, expect, test } from "bun:test"; import { openUnitCount, scheduleProfiles, } from "../../src/level-core/profiles.ts"; import { buildResourceLoad } from "../../src/level-core/weeklyProfile.ts"; import type { Schedule } from "../../src/level-core/types.ts"; import { MON_JAN_5, SAT_JAN_10, makeAssignment, makeProject, makeResource, makeTask, runOnce, } from "./fixtures.ts"; async function crewSchedule(): Promise { const t1 = makeTask({ uniqueId: 1, start: MON_JAN_5, finish: SAT_JAN_10 }); const t2 = makeTask({ uniqueId: 2, start: MON_JAN_5, finish: SAT_JAN_10 }); const project = makeProject([t1, t2], { resources: [makeResource(100, "Crew"), makeResource(200, "Electrical")], assignments: [makeAssignment(1, 100), makeAssignment(2, 200)], }); return runOnce( project, [], [ { id: 10, taskUniqueIds: [1] }, { id: 20, taskUniqueIds: [2] }, { id: 30, taskUniqueIds: [] }, ], ); } describe("scheduleProfiles", () => { test("same Schedule object yields the same profiles instance and load map", async () => { const schedule = await crewSchedule(); const first = scheduleProfiles(schedule); const second = scheduleProfiles(schedule); expect(second).toBe(first); // The derivation itself is memoized, not recomputed per read. expect(second.resourceLoad).toBe(first.resourceLoad); expect(second.unitSpans()).toBe(first.unitSpans()); }); test("resourceLoad matches buildResourceLoad", async () => { const schedule = await crewSchedule(); const shared = scheduleProfiles(schedule).resourceLoad; const direct = buildResourceLoad(schedule); const byId = (a: number, b: number) => a - b; expect([...shared.keys()].sort(byId)).toEqual( [...direct.keys()].sort(byId), ); for (const [resourceId, arr] of direct) { expect([...shared.get(resourceId)!]).toEqual([...arr]); } }); test("unitSpans covers scheduled units and omits empty ones", async () => { const schedule = await crewSchedule(); const spans = scheduleProfiles(schedule).unitSpans(); expect(spans.get(10)).toEqual({ startDay: 0, finishDay: 5 }); expect(spans.get(20)).toEqual({ startDay: 0, finishDay: 5 }); expect(spans.has(30)).toBe(false); // no tasks → no span }); test("unitSpans with a discipline only counts that resource's tasks", async () => { const schedule = await crewSchedule(); const spans = scheduleProfiles(schedule).unitSpans(200); expect(spans.has(10)).toBe(false); // unit 10's task is on resource 100 expect(spans.get(20)).toEqual({ startDay: 0, finishDay: 5 }); }); test("openUnitCount counts open units per working day, deduplicating ids", async () => { const schedule = await crewSchedule(); const counts = openUnitCount(schedule, [10, 20, 10]); // Both units open on working days 0–4; weekend days (5, 6) are zero. expect(counts[0]).toBe(2); expect(counts[4]).toBe(2); expect(counts[5]).toBe(0); }); });