96 lines
3.0 KiB
TypeScript
96 lines
3.0 KiB
TypeScript
import type { ModuleDatabase } from "@ekklesia/module-sdk";
|
|
import { describe, expect, it } from "vitest";
|
|
import { createTimelineStore } from "../src/server/store/drizzle-timelines.js";
|
|
import type { NewStoredSlot } from "../src/server/store/types.js";
|
|
|
|
const TIMELINE_ID = "00000000-0000-4000-8000-000000000001";
|
|
|
|
function slot(id: string): NewStoredSlot {
|
|
return {
|
|
durationOverrideMs: null,
|
|
eligibleFrom: null,
|
|
eligibleUntil: null,
|
|
enabled: true,
|
|
id,
|
|
itemId: "image-1",
|
|
kind: "pinned-item",
|
|
presentation: {
|
|
backgroundColor: null,
|
|
backgroundDarkened: null,
|
|
backgroundMode: null,
|
|
blurPixels: null,
|
|
foregroundShadow: null,
|
|
},
|
|
providerId: "signage.native-images",
|
|
sourceId: null,
|
|
timelineId: TIMELINE_ID,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* A narrow database contract double for the production timeline store.
|
|
*
|
|
* Unlike the service-level memory store, it enforces the shipped PostgreSQL
|
|
* `signage_timeline_slots_position_check` on every insert and update. That is the boundary the
|
|
* deployed bug crossed: the old collision-avoidance algorithm used negative positions, so the
|
|
* actual database rejected a native-image slot before playback ever had content to resolve.
|
|
*/
|
|
function constraintDatabase(existing: readonly { id: string; position: number }[]): {
|
|
readonly database: ModuleDatabase;
|
|
readonly positions: number[];
|
|
} {
|
|
const positions: number[] = [];
|
|
const enforce = (values: { position?: unknown }): void => {
|
|
if (typeof values.position === "number") {
|
|
expect(values.position).toBeGreaterThanOrEqual(0);
|
|
positions.push(values.position);
|
|
}
|
|
};
|
|
const transaction = {
|
|
insert: () => ({
|
|
values: async (values: { position?: unknown }) => enforce(values),
|
|
}),
|
|
select: () => ({
|
|
from: () => ({
|
|
where: () => ({
|
|
orderBy: async () => existing,
|
|
}),
|
|
}),
|
|
}),
|
|
update: () => ({
|
|
set: (values: { position?: unknown }) => ({
|
|
where: async () => enforce(values),
|
|
}),
|
|
}),
|
|
};
|
|
return {
|
|
database: {
|
|
transaction: async (operation: (value: typeof transaction) => Promise<unknown>) => operation(transaction),
|
|
} as unknown as ModuleDatabase,
|
|
positions,
|
|
};
|
|
}
|
|
|
|
describe("production timeline slot ordering", () => {
|
|
it("creates the first pinned native image without violating the database position constraint", async () => {
|
|
const { database, positions } = constraintDatabase([]);
|
|
|
|
await createTimelineStore(database).addSlot(slot("00000000-0000-4000-8000-000000000002"));
|
|
|
|
expect(positions).toEqual([1, 2, 0]);
|
|
});
|
|
|
|
it("reorders through a non-negative temporary range before writing the final order", async () => {
|
|
const first = "00000000-0000-4000-8000-000000000002";
|
|
const second = "00000000-0000-4000-8000-000000000003";
|
|
const { database, positions } = constraintDatabase([
|
|
{ id: first, position: 0 },
|
|
{ id: second, position: 1 },
|
|
]);
|
|
|
|
await createTimelineStore(database).reorderSlots(TIMELINE_ID, [second, first]);
|
|
|
|
expect(positions).toEqual([3, 4, 0, 1]);
|
|
});
|
|
});
|