diff --git a/tests/domain/runtime-project-scheduler.test.ts b/tests/domain/runtime-project-scheduler.test.ts new file mode 100644 index 00000000..26ffeee2 --- /dev/null +++ b/tests/domain/runtime-project-scheduler.test.ts @@ -0,0 +1,156 @@ +import { describe, expect, it } from "vitest"; + +import { + createActorControlTargetRef, + createEmptyRuntimeResolvedControlState, + createSetActorPresenceControlEffect +} from "../../src/controls/control-surface"; +import { + createEmptyProjectScheduler, + createProjectScheduleRoutine, + createProjectScheduleSelectedDaysSelection +} from "../../src/scheduler/project-scheduler"; +import { + applyRuntimeProjectScheduleToControlState, + resolveRuntimeProjectScheduleState +} from "../../src/runtime-three/runtime-project-scheduler"; + +describe("runtime project scheduler", () => { + it("resolves cross-midnight actor routines and keeps unscheduled actors implicitly active", () => { + const actorTarget = createActorControlTargetRef("actor-night-watch"); + const scheduler = createEmptyProjectScheduler(); + scheduler.routines["routine-night-watch"] = createProjectScheduleRoutine({ + id: "routine-night-watch", + title: "Night Watch", + target: actorTarget, + days: createProjectScheduleSelectedDaysSelection(["monday"]), + startHour: 22, + endHour: 2, + effect: createSetActorPresenceControlEffect({ + target: actorTarget, + active: true + }) + }); + + expect( + resolveRuntimeProjectScheduleState({ + scheduler, + actorIds: ["actor-night-watch", "actor-always-on"], + dayNumber: 1, + timeOfDayHours: 23 + }).actors + ).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + actorId: "actor-night-watch", + hasRules: true, + active: true, + activeRoutineId: "routine-night-watch", + activeRoutineTitle: "Night Watch" + }), + expect.objectContaining({ + actorId: "actor-always-on", + hasRules: false, + active: true, + activeRoutineId: null, + activeRoutineTitle: null + }) + ]) + ); + + expect( + resolveRuntimeProjectScheduleState({ + scheduler, + actorIds: ["actor-night-watch"], + dayNumber: 2, + timeOfDayHours: 1.5 + }).actors[0] + ).toEqual( + expect.objectContaining({ + actorId: "actor-night-watch", + active: true, + activeRoutineTitle: "Night Watch" + }) + ); + + expect( + resolveRuntimeProjectScheduleState({ + scheduler, + actorIds: ["actor-night-watch"], + dayNumber: 2, + timeOfDayHours: 3 + }).actors[0] + ).toEqual( + expect.objectContaining({ + actorId: "actor-night-watch", + hasRules: true, + active: false, + activeRoutineId: null, + activeRoutineTitle: null + }) + ); + }); + + it("prefers the highest-priority active routine and writes actor presence into resolved control state", () => { + const actorTarget = createActorControlTargetRef("actor-market-vendor"); + const scheduler = createEmptyProjectScheduler(); + scheduler.routines["routine-open"] = createProjectScheduleRoutine({ + id: "routine-open", + title: "Open Stall", + target: actorTarget, + startHour: 9, + endHour: 18, + priority: 0, + effect: createSetActorPresenceControlEffect({ + target: actorTarget, + active: true + }) + }); + scheduler.routines["routine-break"] = createProjectScheduleRoutine({ + id: "routine-break", + title: "Lunch Break", + target: actorTarget, + startHour: 12, + endHour: 13.5, + priority: 5, + effect: createSetActorPresenceControlEffect({ + target: actorTarget, + active: false + }) + }); + + const resolvedSchedule = resolveRuntimeProjectScheduleState({ + scheduler, + actorIds: ["actor-market-vendor"], + dayNumber: 1, + timeOfDayHours: 12.25 + }); + const resolvedControl = applyRuntimeProjectScheduleToControlState( + createEmptyRuntimeResolvedControlState(), + resolvedSchedule + ); + + expect(resolvedSchedule.actors[0]).toEqual( + expect.objectContaining({ + actorId: "actor-market-vendor", + active: false, + activeRoutineId: "routine-break", + activeRoutineTitle: "Lunch Break" + }) + ); + expect(resolvedControl.discrete).toEqual([ + expect.objectContaining({ + type: "actorPresence", + target: { + kind: "actor", + actorId: "actor-market-vendor" + }, + value: false, + source: { + kind: "scheduler", + scheduleId: "routine-break" + } + }) + ]); + }); +});