evOo-local/packages/adapters/src/media/video-operations.postgres.test.ts
Claude 97cede6c9c
test(media): run video edits and exports through the media runtime
A Postgres suite starts edit_video and export_video through the real
runtime and local artifact store: outputs become linked versions with
posters on the card, exports store one version per preset, appended clips
and subtitle files of another user are refused at start, and an
out-of-range cut fails the job with a readable error. It skips without
Postgres or ffmpeg.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MRtGG9ARfqqhma3t85Ro2u
2026-10-03 14:02:06 +00:00

215 lines
8.0 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<typeof createDb>;
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<MediaJobBlock | undefined> {
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: [],
});
});
});