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.
 
 

341 lines
9.4 KiB

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import type { NextRequest } from "next/server";
import {
POPI_IMAGE_SUBTYPE_REPAINT,
POPI_VIDEO_SUBTYPE_ENHANCE,
POPI_VIDEO_SUBTYPES,
} from "@/constants/generationTask";
import {
buildPopiModelParameters,
fetchPopiModelDetail,
fetchPopiModels,
getPopiModelExtensionFields,
getPopiModelSupportParameterDimensions,
popiModelToProviderModel,
} from "../_popiserverModels";
const originalFetch = global.fetch;
const originalPopiartApiBaseUrl = process.env.POPIART_API_BASE_URL;
function makeRequest(): NextRequest {
return {
nextUrl: new URL("http://localhost:3000/api/models"),
headers: new Headers({ Authorization: "Bearer login-token" }),
} as unknown as NextRequest;
}
describe("popiserver model helpers", () => {
beforeEach(() => {
process.env.POPIART_API_BASE_URL = "http://localhost:3000";
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({
status: "0000",
message: "ok",
data: {
list: [
{
id: 15,
code: "viduq2-pro",
name: "Vidu Q2 Pro",
aiModelCodeAlias: "viduq2-pro",
categories: [{ taskSubType: 203 }],
},
],
},
}),
}) as unknown as typeof fetch;
});
afterEach(() => {
global.fetch = originalFetch;
process.env.POPIART_API_BASE_URL = originalPopiartApiBaseUrl;
});
it("passes only the model subType filter when listing models", async () => {
const params = new URLSearchParams({ subType: `${POPI_VIDEO_SUBTYPES.join(",")},205` });
await fetchPopiModels(makeRequest(), "login-token", undefined, params);
expect(global.fetch).toHaveBeenCalledTimes(1);
const url = String((global.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0]);
const searchParams = new URL(url).searchParams;
expect(searchParams.get("subType")).toBe("203,202,204,206,205");
expect(searchParams.get("origin")).toBe("canvas");
expect(searchParams.has("type")).toBe(false);
expect(searchParams.has("subtype")).toBe(false);
});
it("infers image-to-video capability for video enhance subtype", () => {
const providerModel = popiModelToProviderModel({
id: 109,
code: "video-enhance",
name: "Video Enhance",
categories: [{ taskSubType: POPI_VIDEO_SUBTYPE_ENHANCE }],
});
expect(providerModel.capabilities).toContain("image-to-video");
expect(providerModel.metadata?.subType).toBe(POPI_VIDEO_SUBTYPE_ENHANCE);
});
it("infers image-to-image capability for repaint subtype", () => {
const providerModel = popiModelToProviderModel({
id: 116,
code: "image-repaint",
name: "Image Repaint",
categories: [{ taskSubType: POPI_IMAGE_SUBTYPE_REPAINT }],
});
expect(providerModel.capabilities).toContain("image-to-image");
expect(providerModel.metadata?.subType).toBe(POPI_IMAGE_SUBTYPE_REPAINT);
});
it("passes id when fetching model detail", async () => {
vi.mocked(global.fetch).mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({
status: "0000",
message: "ok",
data: {
id: 37,
code: "speech-2.8-hd",
name: "Speech 2.8 HD",
categories: [],
isSupportAudios: true,
},
}),
} as Response);
await expect(fetchPopiModelDetail(makeRequest(), "login-token", "37")).resolves.toMatchObject({
id: 37,
code: "speech-2.8-hd",
});
const url = String((global.fetch as unknown as ReturnType<typeof vi.fn>).mock.calls[0][0]);
const searchParams = new URL(url).searchParams;
expect(searchParams.get("id")).toBe("37");
expect(searchParams.has("aiModelId")).toBe(false);
});
it("preserves popiserver task metadata on provider models", () => {
const model = popiModelToProviderModel({
id: 30,
code: "speech-2.8-hd",
name: "Speech 2.8 HD",
aiModelCodeAlias: "speech-2.8-hd",
billingMethod: 1,
categories: [{ taskSubType: 301 }],
});
expect(model.provider).toBe("popiserver");
expect(model.metadata).toMatchObject({
type: 3,
subType: 301,
aiModelId: 30,
aiModelCode: "speech-2.8-hd",
aiModelCodeAlias: "speech-2.8-hd",
billingMethod: 1,
});
});
it("does not read popiserver extension fields from metadata", () => {
const model = popiModelToProviderModel({
id: 65,
code: "kling-video-o1",
name: "Kling Video O1",
categories: [{ taskSubType: 202 }],
metadata: JSON.stringify({
generate_audio: {
default: true,
label: "生成音频",
name: "generate_audio",
required: true,
type: "boolean",
},
}),
});
expect(model.metadata).not.toHaveProperty("extensionFields");
});
it("reads popiserver extension fields from otherShowData arrays", () => {
expect(getPopiModelExtensionFields({
id: 65,
otherShowData: JSON.stringify([
{
default: true,
label: "生成音频",
name: "generate_audio",
options: [
{ value: true, label: "开启" },
{ value: false, label: "关闭" },
],
required: true,
type: "boolean",
},
]),
})).toEqual([
{
default: true,
label: "生成音频",
name: "generate_audio",
options: [
{ value: true, label: "开启" },
{ value: false, label: "关闭" },
],
required: true,
type: "boolean",
},
]);
});
it("parses support parameter dimensions from JSON strings", () => {
expect(getPopiModelSupportParameterDimensions({
id: 59,
uploadImageLimit: [1, 10],
uploadVideoLimit: [],
uploadAudioLimit: [1],
supportParameterDimensions: JSON.stringify({
image: {
formats: ["png", "jpg"],
aspectRatio: { min: null, max: null },
width: { min: 300, max: 1500 },
height: { min: "300", max: "1500" },
totalPixels: { min: null, max: null },
},
video: {
formats: [""],
width: { min: null, max: null },
},
audio: {
formats: ["", "MP3"],
},
}),
})).toEqual({
image: {
formats: ["png", "jpg"],
count: { min: 1, max: 10 },
width: { min: 300, max: 1500 },
height: { min: 300, max: 1500 },
},
audio: {
formats: ["mp3"],
count: { min: 1 },
},
});
});
it("creates count-only media rules from upload limits", () => {
expect(getPopiModelSupportParameterDimensions({
id: 60,
uploadVideoLimit: [2, 4],
uploadAudioLimit: [],
supportParameterDimensions: JSON.stringify({
image: {
formats: [""],
},
}),
})).toEqual({
video: {
count: { min: 2, max: 4 },
},
});
});
it("ignores invalid support parameter dimensions JSON", () => {
expect(getPopiModelSupportParameterDimensions({
id: 59,
supportParameterDimensions: "{invalid",
})).toBeUndefined();
});
it("uses displayDimensions label/value options for model parameters", () => {
const parameters = buildPopiModelParameters({
id: 37,
code: "ernie-image-turbo",
displayDimensions: JSON.stringify({
dimensions: {
ratio: {
name: "Ratio",
options: [
{ value: "16:9", label: "16:9" },
{ value: "9:16", label: "9:16" },
],
},
resolution: {
name: "Resolution",
options: [
{ value: "720", label: "720p" },
{ value: "1024", label: "1024p" },
],
},
duration: {
name: "Duration",
options: [
{ value: 5, label: "5" },
{ value: 4, label: "4" },
],
},
},
}),
ratio: ["1:1"],
resolution: ["480"],
duration: [8],
});
expect(parameters).toEqual([
expect.objectContaining({
name: "ratio",
label: "Ratio",
options: [
{ value: "16:9", label: "16:9" },
{ value: "9:16", label: "9:16" },
],
default: "16:9",
}),
expect.objectContaining({
name: "resolution",
label: "Resolution",
options: [
{ value: "720", label: "720p" },
{ value: "1024", label: "1024p" },
],
default: "720",
}),
expect.objectContaining({
name: "duration",
label: "Duration",
options: [
{ value: 5, label: "5" },
{ value: 4, label: "4" },
],
default: 5,
}),
]);
});
it("uses displayDimensions name as the request parameter name", () => {
const parameters = buildPopiModelParameters({
id: 38,
code: "model-with-aspect-ratio-key",
displayDimensions: JSON.stringify({
dimensions: {
aspectRatio: {
name: "ratio",
options: [
{ value: "16:9", label: "16:9" },
],
},
},
}),
});
expect(parameters).toEqual([
expect.objectContaining({
name: "ratio",
options: [{ value: "16:9", label: "16:9" }],
default: "16:9",
}),
]);
});
});