evOo-local/packages/db/src/media-production.postgres.test.ts
2026-10-03 17:55:49 +00:00

463 lines
17 KiB
TypeScript

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();
});
});