const test = require("node:test"); const assert = require("node:assert/strict"); const { ELIGIBILITY_REASONS, evaluateEntryEligibility, resolveEntrySettings, } = require("../src/lifecycle"); const NOW = Date.parse("2035-05-06T07:08:09.000Z"); function readyAsset(overrides = {}) { return { status: "ready", expiration_at: null, ...overrides }; } function enabledEntry(overrides = {}) { return { enabled: 1, schedule_start_at: null, schedule_end_at: null, ...overrides, }; } test("an enabled, ready, permanent, unscheduled entry is playable", () => { assert.deepEqual( evaluateEntryEligibility(enabledEntry(), readyAsset(), NOW), { playable: true, reason: null }, ); }); test("disabled entries are not playable", () => { assert.deepEqual( evaluateEntryEligibility(enabledEntry({ enabled: 0 }), readyAsset(), NOW), { playable: false, reason: ELIGIBILITY_REASONS.DISABLED }, ); }); test("schedule start is inclusive", () => { const entry = enabledEntry({ schedule_start_at: NOW }); assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW - 1), { playable: false, reason: ELIGIBILITY_REASONS.SCHEDULED_FUTURE, }); assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW), { playable: true, reason: null, }); }); test("schedule end is exclusive", () => { const entry = enabledEntry({ schedule_end_at: NOW }); assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW - 1), { playable: true, reason: null, }); assert.deepEqual(evaluateEntryEligibility(entry, readyAsset(), NOW), { playable: false, reason: ELIGIBILITY_REASONS.SCHEDULED_ENDED, }); }); test("start-only, end-only, and bounded schedules use UTC epoch milliseconds", () => { assert.equal( evaluateEntryEligibility( enabledEntry({ schedule_start_at: NOW - 1 }), readyAsset(), NOW, ).playable, true, ); assert.equal( evaluateEntryEligibility( enabledEntry({ schedule_end_at: NOW + 1 }), readyAsset(), NOW, ).playable, true, ); assert.equal( evaluateEntryEligibility( enabledEntry({ schedule_start_at: NOW, schedule_end_at: NOW + 1 }), readyAsset(), NOW, ).playable, true, ); }); test("only ready assets are playable", () => { for (const status of ["processing", "failed", "deleting"]) { assert.deepEqual( evaluateEntryEligibility(enabledEntry(), readyAsset({ status }), NOW), { playable: false, reason: ELIGIBILITY_REASONS.ASSET_NOT_READY }, ); } }); test("permanent assets stay playable and expiration is exclusive", () => { assert.equal( evaluateEntryEligibility(enabledEntry(), readyAsset(), NOW).playable, true, ); assert.equal( evaluateEntryEligibility( enabledEntry(), readyAsset({ expiration_at: NOW + 1 }), NOW, ).playable, true, ); assert.deepEqual( evaluateEntryEligibility( enabledEntry(), readyAsset({ expiration_at: NOW }), NOW, ), { playable: false, reason: ELIGIBILITY_REASONS.ASSET_EXPIRED }, ); }); test("not-playable reasons follow the documented precedence", () => { const asset = readyAsset({ status: "failed", expiration_at: NOW }); assert.equal( evaluateEntryEligibility( enabledEntry({ enabled: 0, schedule_start_at: NOW + 1 }), asset, NOW, ).reason, ELIGIBILITY_REASONS.DISABLED, ); assert.equal( evaluateEntryEligibility( enabledEntry({ schedule_start_at: NOW + 1, schedule_end_at: NOW }), asset, NOW, ).reason, ELIGIBILITY_REASONS.SCHEDULED_FUTURE, ); assert.equal( evaluateEntryEligibility(enabledEntry({ schedule_end_at: NOW }), asset, NOW) .reason, ELIGIBILITY_REASONS.SCHEDULED_ENDED, ); assert.equal( evaluateEntryEligibility(enabledEntry(), asset, NOW).reason, ELIGIBILITY_REASONS.ASSET_NOT_READY, ); }); test("mapped camel-case domain values are supported", () => { assert.deepEqual( evaluateEntryEligibility( { enabled: true, scheduleStartAt: NOW, scheduleEndAt: NOW + 1 }, { status: "ready", expirationAt: NOW + 1 }, NOW, ), { playable: true, reason: null }, ); }); test("eligibility rejects invalid evaluation clocks", () => { for (const invalid of [-1, 1.5, Number.MAX_SAFE_INTEGER + 1, "2035-01-01"]) { assert.throws( () => evaluateEntryEligibility(enabledEntry(), readyAsset(), invalid), /nowMs must be a non-negative, safe epoch-millisecond integer/, ); } }); test("resolved settings inherit global duration and presentation", () => { const settings = { default_duration_ms: 7_000, default_background_mode: "blurred", default_solid_background_color: "#123456", default_background_darkened: 1, default_blur_px: 20, }; assert.deepEqual(resolveEntrySettings({}, settings), { durationMs: 7_000, presentation: { backgroundMode: "blurred", backgroundColor: "#123456", backgroundDarkened: true, blurPx: 20, }, }); }); test("entry overrides are independent and blur always remains global", () => { const settings = { defaultDurationMs: 7_000, defaultBackgroundMode: "blurred", defaultSolidBackgroundColor: "#123456", defaultBackgroundDarkened: true, defaultBlurPx: 20, }; const entry = { durationOverrideMs: 9_000, backgroundModeOverride: "solid", backgroundColorOverride: "#abcdef", backgroundDarkenedOverride: false, blurPx: 999, }; assert.deepEqual(resolveEntrySettings(entry, settings), { durationMs: 9_000, presentation: { backgroundMode: "solid", backgroundColor: "#abcdef", backgroundDarkened: false, blurPx: 20, }, }); });