import { describe, expect, it } from "vitest"; import { buildApp, type AppDependencies } from "../src/app.js"; import { ImageVideoService, FileAttachmentStore } from "@agent-studio/harness"; import type { ImageVideoPublicTask } from "@agent-studio/harness"; import { mkdtemp, mkdir } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { Readable } from "node:stream"; async function fixture() { const root = await mkdtemp(join(tmpdir(), "i2v-api-")); await mkdir(join(root, "ses_1")); const attachments = new FileAttachmentStore({ sessionsRoot: root }); const image = await attachments.add("ses_1", { name: "pixel.png", mediaType: "image/png", stream: Readable.from(Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAIAAAD91JpzAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAAE0lEQVQImWP4z8DwnwGM/zMwAAAf7gP9qS/A4gAAAABJRU5ErkJggg==", "base64")) }); const service = new ImageVideoService({ root: join(root, "tasks"), attachments, remote: {} as never, collect: () => Promise.reject(new Error("No external operations permitted")) }); const dependencies = { imageVideo: service, sessionRegistry: { get: (id: string) => id === "ses_1" ? { sessionId: id } : undefined, disposeAll: async () => {} }, skillCatalog: { list: () => [] }, modelStore: {}, attachmentStore: attachments, approvalBroker: {} } as unknown as AppDependencies; return { app: buildApp(dependencies), service, dependencies, draft: { attachmentId: image.attachmentId, title: "Reference", prompt: "Natural motion and ambient sound", durationSeconds: 10 } }; } describe("image video local API", () => { it("does not persist a target session that disappeared during continuation", async () => { const { app, service, dependencies, draft } = await fixture(); const task = await service.create("ses_1", draft); let lookups = 0; dependencies.sessionRegistry.get = id => id === "ses_new" && lookups++ === 0 ? { sessionId: id } as never : undefined; try { const result = await app.inject({ method: "POST", url: `/api/sessions/ses_new/image-video/tasks/${task.id}/continue`, payload: { confirmed: true, expectedRevision: 0 } }); expect(result.statusCode).toBe(404); expect((await service.owned("ses_1", task.id)).revision).toBe(0); } finally { await app.close(); } }); it("requires an explicit same-origin confirmation to continue an orphaned task in a live session", async () => { const { app, service, dependencies, draft } = await fixture(); const task = await service.create("ses_1", draft); const liveSession = { sessionId: "ses_new" } as never; dependencies.sessionRegistry.get = id => id === "ses_new" ? liveSession : undefined; const url = `/api/sessions/ses_new/image-video/tasks/${task.id}/continue`; const payload = { confirmed: true, expectedRevision: 0 }; try { expect((await app.inject({ method: "POST", url, payload: { expectedRevision: 0 } })).statusCode).toBe(400); expect((await app.inject({ method: "POST", url, payload: { ...payload, confirmed: false } })).statusCode).toBe(400); expect((await app.inject({ method: "POST", url, payload, headers: { origin: "https://evil.example" } })).statusCode).toBeGreaterThanOrEqual(400); expect((await app.inject({ method: "POST", url, payload, headers: { "sec-fetch-site": "cross-site" } })).statusCode).toBeGreaterThanOrEqual(400); expect((await app.inject({ method: "POST", url: url.replace("ses_new", "ses_unknown"), payload })).statusCode).toBe(404); expect((await app.inject({ method: "POST", url, payload: { ...payload, prompt: "changed" } })).statusCode).toBe(400); await expect(service.owned("ses_new", task.id)).rejects.toMatchObject({ code: "I2V_TASK_NOT_FOUND" }); const result = await app.inject({ method: "POST", url, payload }); expect(result.statusCode).toBe(200); expect(result.json<{ task: ImageVideoPublicTask }>().task).toMatchObject({ id: task.id, state: "draft", revision: 1 }); expect((await service.owned("ses_new", task.id)).prompt).toBe(draft.prompt); expect((await service.list())).toHaveLength(1); } finally { await app.close(); } }); it("creates locally, persists history independently from Sessions and serves registered reference ranges", async () => { const { app, service, draft } = await fixture(); try { const response = await app.inject({ method: "POST", url: "/api/sessions/ses_1/image-video/tasks", payload: draft }); expect(response.statusCode).toBe(200); const task = response.json<{ task: ImageVideoPublicTask }>().task; expect(task.state).toBe("draft"); expect((await app.inject({ method: "GET", url: "/api/image-video/tasks" })).json<{ tasks: ImageVideoPublicTask[] }>().tasks).toHaveLength(1); const ranged = await app.inject({ method: "GET", url: task.referenceUrl, headers: { range: "bytes=0-3" } }); expect(ranged.statusCode).toBe(206); expect(ranged.rawPayload.length).toBe(4); expect(ranged.headers["cache-control"]).toBe("no-store"); expect((await app.inject({ method: "GET", url: task.referenceUrl, headers: { range: "bytes=999999-" } })).statusCode).toBe(416); expect((await app.inject({ method: "GET", url: `/api/image-video/tasks/${task.id}/video` })).statusCode).toBe(404); expect((await service.list())[0]?.id).toBe(task.id); } finally { await app.close(); } }); it("rejects foreign origins, rebinding hosts, unknown sessions, arbitrary media paths and mutation types", async () => { const { app, draft } = await fixture(); try { for (const headers of [{ host: "evil.example", origin: "http://evil.example" }, { origin: "https://evil.example" }, { "sec-fetch-site": "cross-site" }]) expect((await app.inject({ method: "GET", url: "/api/image-video/tasks", headers })).statusCode).toBeGreaterThanOrEqual(400); expect((await app.inject({ method: "POST", url: "/api/sessions/ses_other/image-video/tasks", payload: draft })).statusCode).toBe(404); expect((await app.inject({ method: "POST", url: "/api/sessions/ses_1/image-video/tasks", payload: "text", headers: { "content-type": "text/plain" } })).statusCode).toBe(400); expect((await app.inject({ method: "GET", url: "/api/image-video/tasks/secret.env/reference" })).statusCode).toBe(400); expect((await app.inject({ method: "POST", url: "/api/sessions/ses_1/image-video/tasks", payload: { ...draft, path: "/etc/passwd" } })).statusCode).toBe(400); } finally { await app.close(); } }); });