import { afterAll, beforeAll, describe, expect, it } from "vitest"; import type { MediaRepos, NewMediaPublication } from "./media.js"; import { createMediaRepos } from "./media.js"; import type { MediaTestDb } from "./media-fixtures.js"; import { BASE_TIME, openMediaTestDb, sourceItem, testBrandKit, testEdl, testTeaserWeights, } from "./media-fixtures.js"; import { InvalidMediaInputError, MediaStateConflictError } from "./media-shared.js"; import { IsolationError } from "./scope.js"; const databaseUrl = process.env.DATABASE_URL; const describePostgres = process.env.VERIFY_DATABASE && databaseUrl ? describe.sequential : describe.skip; const PROFILE = "example-short-vertical"; describePostgres("media production, publishing and metrics (PostgreSQL)", () => { let db: MediaTestDb; let media: MediaRepos; beforeAll(() => { db = openMediaTestDb(databaseUrl!); media = createMediaRepos(db.prisma); }); afterAll(async () => { await db?.cleanup(); }); async function newProject() { const { spaceId, userId } = await db.newUser(); const source = await media.createSource(spaceId, { kind: "emulator", label: "Emulator", ownerUserId: userId, config: {}, }); const { changedAssetIds } = await media.upsertSourceItems(spaceId, source.id, [sourceItem(1)]); const assetId = changedAssetIds[0] as string; const project = await media.createProject(spaceId, userId, { title: "Launch", goal: "teaser", profileIds: [PROFILE, "example-paid-video"], assetIds: [assetId], }); return { spaceId, userId, assetId, projectId: project.id }; } async function doneRender(spaceId: string, userId: string, projectId: string, assetId: string) { const plan = await media.createPlanVersion(spaceId, { projectId, profileId: PROFILE, edl: testEdl(assetId), createdBy: "user", rationale: null, }); const render = await media.createRender(spaceId, { planId: plan.id, quality: "final", createdBy: userId, }); await media.claimRender(spaceId, render.id); await media.updateRender(spaceId, render.id, { status: "done", progress: 1 }); return render.id; } const publicationInput = (renderId: string, content: string): NewMediaPublication => ({ renderId, destination: "bundle", targetUrl: "https://example.com/offer", trackingUrl: `https://example.com/offer?utm_content=${content}`, utm: { source: "shorts", medium: "video", campaign: "launch", content }, title: null, caption: null, tags: [], thumbnailVariant: `thumb-${content}`, scheduledAt: null, }); it("validates and stores brand kits", async () => { const { spaceId } = await db.newUser(); await expect( media.upsertBrandKit(spaceId, { kit: { ...testBrandKit, colors: { primary: "red" } } }), ).rejects.toBeInstanceOf(InvalidMediaInputError); const kit = await media.upsertBrandKit(spaceId, { kit: testBrandKit }); const renamed = await media.upsertBrandKit(spaceId, { id: kit.id, kit: { ...testBrandKit, name: "Renamed" }, }); expect(renamed).toMatchObject({ id: kit.id, kit: { name: "Renamed" } }); expect(await media.listBrandKits(spaceId)).toHaveLength(1); await media.removeBrandKit(spaceId, kit.id); expect(await media.listBrandKits(spaceId)).toEqual([]); }); it("creates, pages, updates and removes projects", async () => { const { spaceId, userId } = await db.newUser(); await expect( media.createProject(spaceId, userId, { title: "Bad", goal: "teaser", profileIds: [PROFILE], assetIds: ["missing"], }), ).rejects.toBeInstanceOf(IsolationError); const created = []; for (let index = 0; index < 5; index++) { created.push( await media.createProject(spaceId, userId, { title: `Project ${index}`, goal: "highlight", profileIds: [PROFILE, PROFILE], }), ); } expect(created[0]?.profileIds).toEqual([PROFILE]); const seen: string[] = []; let cursor: string | null = null; do { const page = await media.listProjects(spaceId, { cursor, limit: 2 }); seen.push(...page.items.map((project) => project.title)); cursor = page.nextCursor; } while (cursor); expect(seen).toEqual(["Project 4", "Project 3", "Project 2", "Project 1", "Project 0"]); const updated = await media.updateProject(spaceId, created[0]?.id as string, { title: "Renamed", status: "review", }); expect(updated).toMatchObject({ title: "Renamed", status: "review" }); expect((await media.listProjects(spaceId, { limit: 1 })).items[0]?.id).toBe(created[0]?.id); await media.removeProject(spaceId, created[0]?.id as string); await expect(media.getProject(spaceId, created[0]?.id as string)).rejects.toBeInstanceOf( IsolationError, ); }); it("numbers concurrent plan versions without gaps or duplicates", async () => { const { spaceId, assetId, projectId } = await newProject(); await expect( media.createPlanVersion(spaceId, { projectId, profileId: PROFILE, edl: { edlVersion: 1 }, createdBy: "agent", rationale: null, }), ).rejects.toBeInstanceOf(InvalidMediaInputError); await expect( media.createPlanVersion(spaceId, { projectId, profileId: "example-paid-video", edl: testEdl(assetId), createdBy: "agent", rationale: null, }), ).rejects.toBeInstanceOf(InvalidMediaInputError); const plans = await Promise.all( Array.from({ length: 12 }, (_, index) => media.createPlanVersion(spaceId, { projectId, profileId: PROFILE, edl: testEdl(assetId), createdBy: index % 2 ? "agent" : "user", rationale: `try ${index}`, }), ), ); expect(plans.map((plan) => plan.version).sort((a, b) => a - b)).toEqual( Array.from({ length: 12 }, (_, index) => index + 1), ); await media.createPlanVersion(spaceId, { projectId, profileId: "example-paid-video", edl: testEdl(assetId, "example-paid-video"), createdBy: "agent", rationale: null, }); const latest = await media.latestPlans(spaceId, projectId); expect(latest.map((plan) => [plan.profileId, plan.version])).toEqual([ ["example-paid-video", 1], [PROFILE, 12], ]); expect((await media.latestPlan(spaceId, projectId, PROFILE))?.version).toBe(12); expect(await media.listPlanVersions(spaceId, projectId, PROFILE)).toHaveLength(12); expect(latest[1]?.edl.tracks.video[0]?.speed).toBe(1); }); it("lets exactly one worker claim a render and never revives a canceled one", async () => { const { spaceId, userId, assetId, projectId } = await newProject(); const plan = await media.createPlanVersion(spaceId, { projectId, profileId: PROFILE, edl: testEdl(assetId), createdBy: "agent", rationale: null, }); const render = await media.createRender(spaceId, { planId: plan.id, quality: "draft", createdBy: userId, }); expect(render).toMatchObject({ status: "queued", projectId, profileId: PROFILE, progress: 0 }); const claims = await Promise.all( Array.from({ length: 5 }, () => media.claimRender(spaceId, render.id)), ); expect(claims.filter((claim) => claim !== null)).toHaveLength(1); const progressed = await media.updateRender(spaceId, render.id, { progress: 0.4 }); expect(progressed?.progress).toBe(0.4); await expect( media.updateRender(spaceId, render.id, { autoFixRound: 4 }), ).rejects.toBeInstanceOf(InvalidMediaInputError); const canceled = await media.cancelRender(spaceId, render.id); expect(canceled.status).toBe("canceled"); expect(await media.updateRender(spaceId, render.id, { status: "done" })).toBeNull(); expect((await media.getRender(spaceId, render.id)).status).toBe("canceled"); await expect(media.cancelRender(spaceId, render.id)).rejects.toBeInstanceOf( MediaStateConflictError, ); const second = await media.createRender(spaceId, { planId: plan.id, quality: "final", createdBy: userId, }); await media.claimRender(spaceId, second.id); const gated = await media.updateRender(spaceId, second.id, { status: "blocked", progress: 1, autoFixRound: 3, outputs: [ { kind: "video", artifactId: null, fileName: "out.mp4", sizeBytes: 1234, path: "renders/x/out.mp4", }, ], gateReport: { profileId: PROFILE, passed: false, checkedAt: new Date(BASE_TIME).toISOString(), sampledFrames: 10, issues: [{ check: "consent", severity: "block", code: "missing_consent", message: "x" }], autoFixRound: 3, }, ffprobe: { durationMs: 5000, width: 1080, height: 1920, fps: 30, videoCodec: "h264", audioCodec: "aac", sizeBytes: 1234, integratedLufs: -14, truePeakDb: -1, tags: {}, }, }); expect(gated).toMatchObject({ status: "blocked", outputs: [{ kind: "video", fileName: "out.mp4", sizeBytes: 1234, artifactId: null }], gateReport: { passed: false }, ffprobe: { width: 1080 }, }); expect(gated?.outputs[0]).not.toHaveProperty("path"); const record = await media.getRenderRecord(spaceId, second.id); expect(record.outputs).toEqual([expect.objectContaining({ path: "renders/x/out.mp4" })]); expect(record.autoFixRound).toBe(3); await expect(media.cancelRender(spaceId, second.id)).rejects.toBeInstanceOf( MediaStateConflictError, ); expect((await media.listRenders(spaceId, projectId)).map((r) => r.id)).toEqual([ second.id, render.id, ]); const detail = await media.getProject(spaceId, projectId); expect(detail.plans).toHaveLength(1); expect(detail.renders).toHaveLength(2); }); it("publishes only finished renders and moves publications through their states", async () => { const { spaceId, userId, assetId, projectId } = await newProject(); const plan = await media.createPlanVersion(spaceId, { projectId, profileId: PROFILE, edl: testEdl(assetId), createdBy: "agent", rationale: null, }); const queued = await media.createRender(spaceId, { planId: plan.id, quality: "final", createdBy: userId, }); await expect( media.createPublication(spaceId, userId, publicationInput(queued.id, "a")), ).rejects.toBeInstanceOf(MediaStateConflictError); const renderId = await doneRender(spaceId, userId, projectId, assetId); const draft = await media.createPublication(spaceId, userId, publicationInput(renderId, "a")); expect(draft).toMatchObject({ status: "draft", profileId: PROFILE, utm: { content: "a" } }); const scheduledAt = new Date(BASE_TIME + 86_400_000); const scheduled = await media.schedulePublication(spaceId, draft.id, scheduledAt); expect(scheduled).toMatchObject({ status: "scheduled", scheduledAt: scheduledAt.toISOString(), }); const bundled = await media.markPublicationBundled(spaceId, draft.id, "artifact-bundle"); expect(bundled).toMatchObject({ status: "bundled", bundleArtifactId: "artifact-bundle" }); const published = await media.markPublicationPublished( spaceId, draft.id, "https://video.example.com/v/1", ); expect(published.status).toBe("published"); expect(published.publishedAt).not.toBeNull(); await expect(media.cancelPublication(spaceId, draft.id)).rejects.toBeInstanceOf( MediaStateConflictError, ); const other = await media.createPublication(spaceId, userId, { ...publicationInput(renderId, "b"), scheduledAt, }); expect(other.status).toBe("scheduled"); expect((await media.markPublicationFailed(spaceId, other.id)).status).toBe("failed"); expect((await media.cancelPublication(spaceId, other.id)).status).toBe("canceled"); const page1 = await media.listPublications(spaceId, { projectId, limit: 1 }); expect(page1.items.map((item) => item.id)).toEqual([other.id]); const page2 = await media.listPublications(spaceId, { projectId, cursor: page1.nextCursor }); expect(page2.items.map((item) => item.id)).toEqual([draft.id]); expect(page2.nextCursor).toBeNull(); const elsewhere = await newProject(); expect( (await media.listPublications(spaceId, { projectId: elsewhere.projectId })).items, ).toEqual([]); const byContent = await media.resolvePublicationKeys(spaceId, "utm_content", ["a", "b", "z"]); expect(Object.fromEntries(byContent)).toEqual({ a: draft.id, b: other.id }); const byUrl = await media.resolvePublicationKeys(spaceId, "external_url", [ "https://video.example.com/v/1", ]); expect(byUrl.get("https://video.example.com/v/1")).toBe(draft.id); const byTracking = await media.resolvePublicationKeys(spaceId, "tracking_url", [ "https://example.com/offer?utm_content=b", ]); expect([...byTracking.values()]).toEqual([other.id]); }); it("counts the funnel from the latest observation per source without double counting", async () => { const { spaceId, userId, assetId, projectId } = await newProject(); const renderId = await doneRender(spaceId, userId, projectId, assetId); const a = await media.createPublication(spaceId, userId, publicationInput(renderId, "a")); const b = await media.createPublication(spaceId, userId, publicationInput(renderId, "b")); const day = (n: number) => new Date(BASE_TIME + n * 86_400_000).toISOString(); const report = [ { publicationId: a.id, observedAt: day(1), views: 100, clicks: 10, conversions: 1, revenueCents: 500, }, { publicationId: a.id, observedAt: day(2), views: 250, clicks: 30, conversions: 3, revenueCents: 1500, }, { publicationId: b.id, observedAt: day(2), views: 40, clicks: 4, conversions: 0, revenueCents: 0, }, ].map((row) => ({ ...row, currency: "EUR", source: "csv_import" as const })); expect(await media.recordMetrics(spaceId, report)).toBe(3); expect(await media.recordMetrics(spaceId, report)).toBe(3); await media.recordMetric(spaceId, { publicationId: a.id, observedAt: day(1), views: 0, clicks: 0, conversions: 2, revenueCents: 1000, currency: "EUR", source: "manual", }); expect(await media.listMetrics(spaceId, a.id)).toHaveLength(3); const rows = await media.funnel(spaceId, projectId); const byId = new Map(rows.map((row) => [row.publicationId, row])); expect(byId.get(a.id)).toEqual({ publicationId: a.id, projectId, profileId: PROFILE, variant: "a", thumbnailVariant: "thumb-a", views: 250, clicks: 30, conversions: 5, revenueCents: 2500, currency: "EUR", }); expect(byId.get(b.id)).toMatchObject({ views: 40, clicks: 4, conversions: 0 }); expect(await media.funnel(spaceId)).toHaveLength(2); expect(await media.publicationsWithMetricsCount(spaceId)).toBe(2); await expect(media.recordMetric(spaceId, { ...report[0]!, views: -1 })).rejects.toBeInstanceOf( InvalidMediaInputError, ); }); it("saves CSV mappings and the teaser model", async () => { const { spaceId } = await db.newUser(); const mapping = { name: "Platform export", delimiter: ";" as const, columns: { publication: "Link", observedAt: "Date", views: "Views", revenue: "Revenue" }, publicationKey: "tracking_url" as const, revenueUnit: "major" as const, defaultCurrency: "USD", }; const saved = await media.saveMetricMapping(spaceId, mapping); expect(saved).toMatchObject({ ...mapping, id: expect.any(String) }); const again = await media.saveMetricMapping(spaceId, { ...mapping, delimiter: "," }); expect(again.id).toBe(saved.id); const other = await media.saveMetricMapping(spaceId, { ...mapping, name: "Other" }); await expect( media.saveMetricMapping(spaceId, { ...mapping, id: other.id, name: "Platform export" }), ).rejects.toBeInstanceOf(InvalidMediaInputError); expect((await media.listMetricMappings(spaceId)).map((m) => m.name)).toEqual([ "Other", "Platform export", ]); expect((await media.getMetricMapping(spaceId, saved.id as string)).delimiter).toBe(","); expect(await media.getTeaserModel(spaceId)).toBeNull(); const model = await media.putTeaserModel(spaceId, testTeaserWeights, 21); expect(model).toMatchObject({ sampleCount: 21, weights: testTeaserWeights }); expect((await media.getTeaserModel(spaceId))?.sampleCount).toBe(21); await media.resetTeaserModel(spaceId); expect(await media.getTeaserModel(spaceId)).toBeNull(); }); });