import { afterEach, describe, expect, it, vi } from "vitest"; import { createSeedanceTask, extractSeedanceUsage, querySeedanceTask } from "@/lib/seedance/client"; describe("Seedance usage extraction", () => { afterEach(() => { vi.unstubAllGlobals(); delete process.env.SEEDANCE_API_KEY; delete process.env.SEEDANCE_BASE_URL; delete process.env.SEEDANCE_MODEL; }); it("reads completion_tokens from top-level and nested provider responses", () => { expect(extractSeedanceUsage({ usage: { completion_tokens: 12345 } })).toEqual({ completionTokens: 12345 }); expect(extractSeedanceUsage({ data: { usage: { completionTokens: 67890 } } })).toEqual({ completionTokens: 67890 }); expect(extractSeedanceUsage({ usage: { prompt_tokens: 100 } })).toBeUndefined(); }); it("sends the official Seedance 2.0 multimodal payload without UI labels", async () => { process.env.SEEDANCE_API_KEY = "test-key"; process.env.SEEDANCE_BASE_URL = "https://ark.test/api/v3"; process.env.SEEDANCE_MODEL = "doubao-seedance-2-0-260128"; let body: Record | undefined; vi.stubGlobal("fetch", vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { body = JSON.parse(String(init?.body)); return new Response(JSON.stringify({ id: "task-1" }), { status: 200, headers: { "Content-Type": "application/json" } }); })); await createSeedanceTask({ prompt: "combine", settings: { ratio: "16:9", duration: 8, resolution: "1080p" }, materials: [ { type: "image", url: "/image.png", label: "@图片1" }, { type: "video", url: "/video.mp4", label: "@视频1" }, { type: "audio", url: "/audio.mp3", label: "@音频1" } ], origin: "https://app.test" }); expect(body).toMatchObject({ model: "doubao-seedance-2-0-260128", generate_audio: true, ratio: "16:9", duration: 8, resolution: "1080p", watermark: false }); const content = body?.content as Array>; expect(content).toHaveLength(4); expect(content.slice(1).every((item) => !("label" in item))).toBe(true); }); it("keeps the provider expired status distinct from a failed task", async () => { process.env.SEEDANCE_API_KEY = "test-key"; process.env.SEEDANCE_BASE_URL = "https://ark.test/api/v3"; vi.stubGlobal("fetch", vi.fn(async () => new Response(JSON.stringify({ id: "task-1", status: "expired" }), { status: 200, headers: { "Content-Type": "application/json" } }))); await expect(querySeedanceTask("task-1")).resolves.toMatchObject({ status: "expired" }); }); it("sends the selected Seedance 2.5 model and accepts its extended limits", async () => { process.env.SEEDANCE_API_KEY = "test-key"; let body: Record | undefined; vi.stubGlobal("fetch", vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { body = JSON.parse(String(init?.body)); return new Response(JSON.stringify({ id: "task-25" }), { status: 200, headers: { "Content-Type": "application/json" } }); })); await createSeedanceTask({ model: "doubao-seedance-2-5-260628", prompt: "combine", settings: { ratio: "16:9", duration: 30, resolution: "720p" }, materials: Array.from({ length: 5 }, (_, index) => ({ type: "image" as const, url: `/image-${index + 1}.png`, label: `@图片${index + 1}` })), origin: "https://app.test" }); expect(body).toMatchObject({ model: "doubao-seedance-2-5-260628", duration: 30, omni_reference_task_type: "auto" }); expect(body?.content).toHaveLength(6); }); it("rejects more materials than the Seedance 2.0 content limit before calling the provider", async () => { process.env.SEEDANCE_API_KEY = "test-key"; const fetchMock = vi.fn(); vi.stubGlobal("fetch", fetchMock); await expect(createSeedanceTask({ prompt: "combine", settings: {}, materials: Array.from({ length: 5 }, (_, index) => ({ type: "image" as const, url: `/image-${index + 1}.png`, label: `@图片${index + 1}` })), origin: "https://app.test" })).rejects.toThrow("最多支持 4 个素材"); expect(fetchMock).not.toHaveBeenCalled(); }); });