import { randomUUID } from "node:crypto"; import { mkdtemp, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { JobPublisher } from "@rakazo/adapter-kit"; import type { MediaJobBlock, MessageBlock } from "@rakazo/contracts"; import type { PrismaClient } from "@rakazo/db"; import { createDb } from "@rakazo/db"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { storeArtifactFromFile } from "../artifact-files.js"; import { LocalArtifactStore } from "../artifacts.js"; import { createMediaRuntime, MediaJobRejectedError } from "../media-runtime.js"; import { detectFfmpeg } from "./ffmpeg.js"; import type { TestClips } from "./media-test-support.js"; import { createTestClips } from "./media-test-support.js"; import { createVideoOperations } from "./video-operations.js"; // CI runners may not have ffmpeg; the suite runs wherever both it and Postgres are available. const describePostgres = process.env.VERIFY_DATABASE && process.env.DATABASE_URL && (await detectFfmpeg()) ? describe.sequential : describe.skip; describePostgres("video operations in the media runtime (PostgreSQL + ffmpeg)", () => { let db: ReturnType; let prisma: PrismaClient; let root: string; let clips: TestClips; const users: string[] = []; beforeAll(async () => { db = createDb(process.env.DATABASE_URL!); prisma = db.prisma; root = await mkdtemp(join(tmpdir(), "video-operations-pg-")); clips = await createTestClips(); }); afterAll(async () => { if (!db) return; await prisma.mediaJob.deleteMany({ where: { userId: { in: users } } }); await prisma.artifact.deleteMany({ where: { userId: { in: users } } }); await prisma.organization.deleteMany({ where: { id: { in: users } } }); await prisma.user.deleteMany({ where: { id: { in: users } } }); await prisma.$disconnect(); await db.pool.end(); await rm(root, { recursive: true, force: true }); await rm(clips.dir, { recursive: true, force: true }); }); async function setup() { const id = randomUUID(); users.push(id); await prisma.user.create({ data: { id, name: "Video test", email: `${id}@example.test`, emailVerified: true }, }); await prisma.organization.create({ data: { id, name: "Video test", slug: id, createdAt: new Date() }, }); await prisma.space.create({ data: { id, organizationId: id, name: "Media", isDefault: true } }); await prisma.member.create({ data: { id, organizationId: id, userId: id, role: "owner", createdAt: new Date() }, }); const bot = await prisma.bot.create({ data: { spaceId: id, userId: id, name: "Studio", color: "test-color" }, }); const thread = await prisma.thread.create({ data: { spaceId: id, userId: id, botId: bot.id } }); const jobs: JobPublisher = { enqueue: async () => undefined, cancel: async () => undefined, close: async () => undefined, }; const artifacts = new LocalArtifactStore(join(root, id, "artifacts")); const runtime = createMediaRuntime( { prisma, events: { notify: async () => undefined }, jobs, artifacts, registrations: createVideoOperations(), }, { tempRoot: join(root, id, "jobs") }, ); const scope = { spaceId: id, userId: id, botId: bot.id, groupId: null, threadId: thread.id, runId: null, }; const store = async (path: string, name: string, mimeType: string) => ( await storeArtifactFromFile( { prisma, artifacts }, { path, name, mimeType, spaceId: id, userId: id, botId: bot.id, groupId: null, runId: null, }, ) ).id; return { runtime, scope, store }; } async function card(messageId: string, jobId: string): Promise { const message = await prisma.message.findUniqueOrThrow({ where: { id: messageId } }); return (message.blocks as MessageBlock[]).find( (block): block is MediaJobBlock => block.kind === "media" && block.jobId === jobId, ); } it("edits a stored video into a linked 9:16 version with a poster on the card", async () => { const { runtime, scope, store } = await setup(); const source = await store(clips.landscape, "holiday.mp4", "video/mp4"); const subtitles = await store(clips.srt, "holiday.srt", "application/x-subrip"); const started = await runtime.start({ ...scope, operation: "edit_video", input: { artifact_id: source, title: "Urlaub 9:16", steps: [ { op: "trim", start: 0.5, end: 2.5 }, { op: "reframe", preset: "vertical", fit: "blur" }, { op: "subtitles", artifact_id: subtitles }, ], }, }); await runtime.runJob(started.jobId); const job = await runtime.get(started.jobId); expect(job).toMatchObject({ status: "done", title: "Urlaub 9:16", progress: 1 }); const [output] = job!.outputs; expect(output).toMatchObject({ kind: "video", mimeType: "video/mp4", name: "Urlaub 9 16.mp4", width: 1080, height: 1920, parentArtifactId: source, }); expect(output?.posterArtifactId).toBeDefined(); const poster = await prisma.artifact.findUniqueOrThrow({ where: { id: output!.posterArtifactId! }, }); expect(poster.mimeType).toBe("image/jpeg"); const stored = await prisma.artifact.findUniqueOrThrow({ where: { id: output!.artifactId } }); expect(stored).toMatchObject({ parentArtifactId: source, width: 1080, height: 1920 }); expect(Math.abs((stored.durationMs ?? 0) - 2000)).toBeLessThan(150); expect(await card(started.messageId, started.jobId)).toMatchObject({ status: "done", outputs: job?.outputs, }); }); it("exports one stored version per preset", async () => { const { runtime, scope, store } = await setup(); const source = await store(clips.landscape, "holiday.mp4", "video/mp4"); const started = await runtime.start({ ...scope, operation: "export_video", input: { artifact_id: source, presets: ["square", "portrait"] }, }); await runtime.runJob(started.jobId); const job = await runtime.get(started.jobId); expect(job).toMatchObject({ status: "done", title: "1:1 · 4:5" }); expect( job?.outputs.map((output) => [output.name, output.preset, output.width, output.height]), ).toEqual([ ["holiday-1x1.mp4", "square", 1080, 1080], ["holiday-4x5.mp4", "portrait", 1080, 1350], ]); }); it("refuses appended clips and subtitles the user cannot read", async () => { const { runtime, scope, store } = await setup(); const other = await setup(); const source = await store(clips.landscape, "mine.mp4", "video/mp4"); const foreignClip = await other.store(clips.silentPortrait, "theirs.mp4", "video/mp4"); const foreignSubtitles = await other.store(clips.srt, "theirs.srt", "application/x-subrip"); for (const step of [ { op: "concat", artifact_ids: [foreignClip] }, { op: "subtitles", artifact_id: foreignSubtitles }, ]) { await expect( runtime.start({ ...scope, operation: "edit_video", input: { artifact_id: source, title: "x", steps: [step] }, }), ).rejects.toThrow(MediaJobRejectedError); } }); it("fails the job with a readable error when a range is outside the video", async () => { const { runtime, scope, store } = await setup(); const source = await store(clips.landscape, "short.mp4", "video/mp4"); const started = await runtime.start({ ...scope, operation: "edit_video", input: { artifact_id: source, title: "x", steps: [{ op: "trim", start: 10, end: 20 }] }, }); await runtime.runJob(started.jobId); expect(await runtime.get(started.jobId)).toMatchObject({ status: "failed", error: "trim 10–20 s is outside the 3 s video.", outputs: [], }); }); });