import type { ModuleDrizzleDatabase } from "@ekklesia/module-database/drizzle"; 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: ModuleDrizzleDatabase; 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) => operation(transaction), } as unknown as ModuleDrizzleDatabase, 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]); }); });