import { writeFileSync } from "node:fs"; import { join } from "node:path"; import { beforeAll, beforeEach, describe, expect, mock, test } from "bun:test"; const testDir = process.env.VELLUM_WORKSPACE_DIR!; // Track calls to embedAndUpsert const embedAndUpsertCalls: Array<{ config: unknown; targetType: string; targetId: string; input: unknown; extraPayload: unknown; }> = []; mock.module("../../../../../persistence/job-utils.js", () => ({ asString: (value: unknown) => typeof value === "string" && value.length > 0 ? value : null, embedAndUpsert: async ( config: unknown, targetType: string, targetId: string, input: unknown, extraPayload: unknown, ) => { embedAndUpsertCalls.push({ config, targetType, targetId, input, extraPayload, }); }, })); import { resetDbForTesting } from "../../../../../__tests__/db-test-helpers.js"; import { getDb } from "../../../../../persistence/db-connection.js"; import { initializeDb } from "../../../../../persistence/db-init.js"; import type { MemoryJob } from "../../../../../persistence/jobs-store.js"; import { mediaAssets } from "../../../../../persistence/schema/index.js"; import { embedMediaJob } from "./embedding.js"; describe("embedMediaJob", () => { // initializeDb runs the full migration chain (hundreds of steps); under // parallel CI load it can exceed bun's default 5s hook timeout, so allow more. beforeAll(async () => { await initializeDb(); }, 30_000); beforeEach(async () => { embedAndUpsertCalls.length = 0; resetDbForTesting(); await initializeDb(); }, 30_000); function makeJob(payload: Record): MemoryJob { return { id: "job-1", type: "embed_media", payload, status: "running", attempts: 0, deferrals: 0, runAfter: 0, lastError: null, startedAt: Date.now(), createdAt: Date.now(), updatedAt: Date.now(), }; } test("skips when assetId is missing", async () => { await embedMediaJob(makeJob({})); expect(embedAndUpsertCalls).toHaveLength(0); }); test("skips when asset is not found", async () => { await embedMediaJob(makeJob({ assetId: "nonexistent" })); expect(embedAndUpsertCalls).toHaveLength(0); }); test("skips when asset status is not indexed", async () => { const db = getDb(); const now = Date.now(); // Create a temp file const filePath = join(testDir, "test-registered.png"); writeFileSync(filePath, Buffer.from("fake image data")); db.insert(mediaAssets) .values({ id: "asset-registered", title: "Test Image", filePath, mimeType: "image/png", durationSeconds: null, fileHash: "hash-registered", status: "registered", mediaType: "image", metadata: null, createdAt: now, updatedAt: now, }) .run(); await embedMediaJob(makeJob({ assetId: "asset-registered" })); expect(embedAndUpsertCalls).toHaveLength(0); }); test("embeds indexed image asset with correct input type and target", async () => { const db = getDb(); const now = Date.now(); const filePath = join(testDir, "test-image.png"); const imageData = Buffer.from("fake png data"); writeFileSync(filePath, imageData); db.insert(mediaAssets) .values({ id: "asset-image", title: "My Screenshot", filePath, mimeType: "image/png", durationSeconds: null, fileHash: "hash-image", status: "indexed", mediaType: "image", metadata: null, createdAt: now, updatedAt: now, }) .run(); await embedMediaJob(makeJob({ assetId: "asset-image" })); expect(embedAndUpsertCalls).toHaveLength(1); const call = embedAndUpsertCalls[0]; expect(call.targetType).toBe("media"); expect(call.targetId).toBe("asset-image"); expect(call.input).toEqual({ type: "image", data: imageData, mimeType: "image/png", }); expect(call.extraPayload).toEqual({ created_at: now, kind: "image", subject: "My Screenshot", }); }); test("embeds indexed audio asset with correct modality", async () => { const db = getDb(); const now = Date.now(); const filePath = join(testDir, "test-audio.mp3"); const audioData = Buffer.from("fake mp3 data"); writeFileSync(filePath, audioData); db.insert(mediaAssets) .values({ id: "asset-audio", title: "Podcast Episode", filePath, mimeType: "audio/mp3", durationSeconds: 120, fileHash: "hash-audio", status: "indexed", mediaType: "audio", metadata: null, createdAt: now, updatedAt: now, }) .run(); await embedMediaJob(makeJob({ assetId: "asset-audio" })); expect(embedAndUpsertCalls).toHaveLength(1); const call = embedAndUpsertCalls[0]; expect(call.targetType).toBe("media"); expect(call.targetId).toBe("asset-audio"); expect((call.input as { type: string }).type).toBe("audio"); expect((call.input as { mimeType: string }).mimeType).toBe("audio/mp3"); }); test("embeds indexed video asset with correct modality", async () => { const db = getDb(); const now = Date.now(); const filePath = join(testDir, "test-video.mp4"); const videoData = Buffer.from("fake mp4 data"); writeFileSync(filePath, videoData); db.insert(mediaAssets) .values({ id: "asset-video", title: "Tutorial Video", filePath, mimeType: "video/mp4", durationSeconds: 300, fileHash: "hash-video", status: "indexed", mediaType: "video", metadata: null, createdAt: now, updatedAt: now, }) .run(); await embedMediaJob(makeJob({ assetId: "asset-video" })); expect(embedAndUpsertCalls).toHaveLength(1); const call = embedAndUpsertCalls[0]; expect(call.targetType).toBe("media"); expect(call.targetId).toBe("asset-video"); expect((call.input as { type: string }).type).toBe("video"); expect((call.input as { mimeType: string }).mimeType).toBe("video/mp4"); }); });