You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
347 lines
10 KiB
347 lines
10 KiB
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import type { NextRequest } from "next/server";
|
|
import { checkPopiTaskOnce, submitPopiTask } from "../popiserver";
|
|
import type { GenerationInput } from "@/lib/providers/types";
|
|
|
|
const originalFetch = global.fetch;
|
|
|
|
function makeRequest(): NextRequest {
|
|
return {
|
|
nextUrl: new URL("http://localhost:3000/api/generate"),
|
|
headers: new Headers({ Authorization: "Bearer login-token" }),
|
|
} as unknown as NextRequest;
|
|
}
|
|
|
|
function makeInput(): GenerationInput {
|
|
return {
|
|
model: {
|
|
id: "15",
|
|
name: "Vidu Q2 Pro",
|
|
provider: "popiserver",
|
|
capabilities: ["image-to-video"],
|
|
description: null,
|
|
metadata: {
|
|
type: 2,
|
|
subType: 203,
|
|
aiModelId: 15,
|
|
aiModelCode: "viduq2-pro",
|
|
aiModelCodeAlias: "viduq2-pro",
|
|
},
|
|
},
|
|
prompt: "@角色1 和 @角色2 在一起吃火锅",
|
|
images: ["https://example.com/1.png", "https://example.com/2.png"],
|
|
parameters: {
|
|
ratio: "16:9",
|
|
resolution: "720",
|
|
videoRatio: 10,
|
|
},
|
|
};
|
|
}
|
|
|
|
function makeAudioInput(): GenerationInput {
|
|
return {
|
|
model: {
|
|
id: "30",
|
|
name: "Speech 2.8 HD",
|
|
provider: "popiserver",
|
|
capabilities: ["text-to-audio"],
|
|
description: null,
|
|
metadata: {
|
|
type: 3,
|
|
subType: 301,
|
|
aiModelId: 30,
|
|
aiModelCode: "speech-2.8-hd",
|
|
aiModelCodeAlias: "speech-2.8-hd",
|
|
},
|
|
},
|
|
prompt: "我是一个小白兔,白白的小白兔",
|
|
parameters: {},
|
|
};
|
|
}
|
|
|
|
describe("popiserver generation provider", () => {
|
|
beforeEach(() => {
|
|
global.fetch = vi.fn() as unknown as typeof fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
global.fetch = originalFetch;
|
|
});
|
|
|
|
it("submits a unified async task create request", async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: { id: 1189 },
|
|
}),
|
|
} as Response);
|
|
|
|
const result = await submitPopiTask(makeRequest(), "login-token", makeInput());
|
|
|
|
expect(result.taskId).toBe("1189");
|
|
const [, init] = vi.mocked(global.fetch).mock.calls[0];
|
|
expect(init?.method).toBe("POST");
|
|
expect(JSON.parse(String(init?.body))).toMatchObject({
|
|
type: 2,
|
|
subType: 203,
|
|
aiPlatform: "GATEWAY",
|
|
origin: "canvas",
|
|
aiModelId: 15,
|
|
aiModelCode: "viduq2-pro",
|
|
aiModelCodeAlias: "viduq2-pro",
|
|
batchSize: 1,
|
|
ratio: "16:9",
|
|
resolution: "720",
|
|
videoRatio: 10,
|
|
});
|
|
});
|
|
|
|
it("submits audio task create with the gateway audio request shape", async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: { id: 1190 },
|
|
}),
|
|
} as Response);
|
|
|
|
const result = await submitPopiTask(makeRequest(), "login-token", makeAudioInput());
|
|
|
|
expect(result.taskId).toBe("1190");
|
|
const [, init] = vi.mocked(global.fetch).mock.calls[0];
|
|
expect(JSON.parse(String(init?.body))).toEqual({
|
|
type: 3,
|
|
chatPrompt: "我是一个小白兔,白白的小白兔",
|
|
model: "speech-2.8-hd",
|
|
origin: "canvas",
|
|
aiPlatform: "GATEWAY",
|
|
aiModelId: 30,
|
|
aiModelCode: "speech-2.8-hd",
|
|
aiModelCodeAlias: "speech-2.8-hd",
|
|
subType: 301,
|
|
batchSize: 1,
|
|
});
|
|
});
|
|
|
|
it("uploads task input media in one batch before creating the task", async () => {
|
|
vi.mocked(global.fetch)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: [
|
|
{ url: "https://cdn.popi.art/input-image.png" },
|
|
{ url: "https://cdn.popi.art/input-video.mp4" },
|
|
{ url: "https://cdn.popi.art/input-audio.mp3" },
|
|
],
|
|
}),
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: { id: 1191 },
|
|
}),
|
|
} as Response);
|
|
|
|
const input = makeInput();
|
|
input.images = ["data:image/png;base64,aW1hZ2U="];
|
|
input.dynamicInputs = {
|
|
videos: ["data:video/mp4;base64,dmlkZW8="],
|
|
audios: ["data:audio/mpeg;base64,YXVkaW8="],
|
|
};
|
|
|
|
const result = await submitPopiTask(makeRequest(), "login-token", input);
|
|
|
|
expect(result.taskId).toBe("1191");
|
|
expect(vi.mocked(global.fetch)).toHaveBeenCalledTimes(2);
|
|
|
|
const [uploadUrl, uploadInit] = vi.mocked(global.fetch).mock.calls[0];
|
|
expect(String(uploadUrl)).toContain("/api_client/media/upload");
|
|
expect(uploadInit?.method).toBe("POST");
|
|
expect(uploadInit?.body).toBeInstanceOf(FormData);
|
|
const uploadBody = uploadInit?.body as FormData;
|
|
expect(uploadBody.get("multiple")).toBe("true");
|
|
expect(uploadBody.getAll("file")).toHaveLength(3);
|
|
|
|
const [createUrl, createInit] = vi.mocked(global.fetch).mock.calls[1];
|
|
expect(String(createUrl)).toContain("/api_client/anime/task/create");
|
|
const createBody = JSON.parse(String(createInit?.body));
|
|
expect(createBody.images).toEqual(["https://cdn.popi.art/input-image.png"]);
|
|
expect(createBody.videos).toEqual(["https://cdn.popi.art/input-video.mp4"]);
|
|
expect(createBody.audios).toEqual(["https://cdn.popi.art/input-audio.mp3"]);
|
|
});
|
|
|
|
it("accepts single-object media upload responses and reuses duplicate inputs", async () => {
|
|
vi.mocked(global.fetch)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: {
|
|
id: 5120,
|
|
path: "/media/2026/0527/5120.png",
|
|
url: "https://cdn.popi.art/5120.png",
|
|
},
|
|
}),
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: { id: 1192 },
|
|
}),
|
|
} as Response);
|
|
|
|
const duplicateImage = "data:image/png;base64,aW1hZ2U=";
|
|
const input = makeInput();
|
|
input.images = [duplicateImage];
|
|
input.dynamicInputs = {
|
|
images: [duplicateImage],
|
|
};
|
|
|
|
const result = await submitPopiTask(makeRequest(), "login-token", input);
|
|
|
|
expect(result.taskId).toBe("1192");
|
|
expect(vi.mocked(global.fetch)).toHaveBeenCalledTimes(2);
|
|
|
|
const [, uploadInit] = vi.mocked(global.fetch).mock.calls[0];
|
|
const uploadBody = uploadInit?.body as FormData;
|
|
expect(uploadBody.getAll("file")).toHaveLength(1);
|
|
|
|
const [, createInit] = vi.mocked(global.fetch).mock.calls[1];
|
|
const createBody = JSON.parse(String(createInit?.body));
|
|
expect(createBody.images).toEqual(["https://cdn.popi.art/5120.png"]);
|
|
});
|
|
|
|
it("preserves direct URL positions when uploading mixed media inputs", async () => {
|
|
vi.mocked(global.fetch)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: [
|
|
{ url: "https://cdn.popi.art/uploaded-image.png" },
|
|
{ url: "https://cdn.popi.art/uploaded-video.mp4" },
|
|
],
|
|
}),
|
|
} as Response)
|
|
.mockResolvedValueOnce({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: { id: 1193 },
|
|
}),
|
|
} as Response);
|
|
|
|
const input = makeInput();
|
|
input.images = [
|
|
"https://already.cdn/source.png",
|
|
"data:image/png;base64,aW1hZ2U=",
|
|
];
|
|
input.dynamicInputs = {
|
|
videos: [
|
|
"https://already.cdn/source.mp4",
|
|
"data:video/mp4;base64,dmlkZW8=",
|
|
],
|
|
};
|
|
|
|
const result = await submitPopiTask(makeRequest(), "login-token", input);
|
|
|
|
expect(result.taskId).toBe("1193");
|
|
const [, uploadInit] = vi.mocked(global.fetch).mock.calls[0];
|
|
const uploadBody = uploadInit?.body as FormData;
|
|
expect(uploadBody.getAll("file")).toHaveLength(2);
|
|
|
|
const [, createInit] = vi.mocked(global.fetch).mock.calls[1];
|
|
const createBody = JSON.parse(String(createInit?.body));
|
|
expect(createBody.images).toEqual([
|
|
"https://already.cdn/source.png",
|
|
"https://cdn.popi.art/uploaded-image.png",
|
|
]);
|
|
expect(createBody.videos).toEqual([
|
|
"https://already.cdn/source.mp4",
|
|
"https://cdn.popi.art/uploaded-video.mp4",
|
|
]);
|
|
});
|
|
|
|
it("extracts completed image task output from task list", async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: {
|
|
list: [
|
|
{
|
|
id: 1189,
|
|
status: 2,
|
|
resultList: [
|
|
{
|
|
images: ["https://example.com/result.png"],
|
|
},
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
} as Response);
|
|
|
|
const result = await checkPopiTaskOnce(makeRequest(), "login-token", "1189", "image");
|
|
|
|
expect(result.status).toBe("completed");
|
|
const [url] = vi.mocked(global.fetch).mock.calls[0];
|
|
const searchParams = new URL(String(url)).searchParams;
|
|
expect(searchParams.get("ids")).toBe("1189");
|
|
expect(searchParams.get("origin")).toBe("canvas");
|
|
if (result.status === "completed") {
|
|
expect(result.output.outputs?.[0]).toMatchObject({
|
|
type: "image",
|
|
url: "https://example.com/result.png",
|
|
});
|
|
}
|
|
});
|
|
|
|
it("extracts all completed image task outputs from batched result list", async () => {
|
|
vi.mocked(global.fetch).mockResolvedValue({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
status: "0000",
|
|
message: "ok",
|
|
data: {
|
|
list: [
|
|
{
|
|
id: 1191,
|
|
status: 2,
|
|
resultList: [
|
|
{ images: ["https://example.com/result-1.png"] },
|
|
{ images: ["https://example.com/result-2.png"] },
|
|
{ images: ["https://example.com/result-3.png"] },
|
|
],
|
|
},
|
|
],
|
|
},
|
|
}),
|
|
} as Response);
|
|
|
|
const result = await checkPopiTaskOnce(makeRequest(), "login-token", "1191", "image");
|
|
|
|
expect(result.status).toBe("completed");
|
|
if (result.status === "completed") {
|
|
expect(result.output.outputs).toEqual([
|
|
{ type: "image", data: "", url: "https://example.com/result-1.png" },
|
|
{ type: "image", data: "", url: "https://example.com/result-2.png" },
|
|
{ type: "image", data: "", url: "https://example.com/result-3.png" },
|
|
]);
|
|
}
|
|
});
|
|
});
|
|
|