Implement Phase 3A playlist management

This commit is contained in:
Media
2026-08-16 20:34:36 -05:00
parent 722c6cf77d
commit e2ecdc97e2
15 changed files with 2955 additions and 7 deletions
+218
View File
@@ -0,0 +1,218 @@
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,
},
});
});